🔥 Okibi

yuki / okibi

焚き火の仲間だけの自前Gitホスティング(このサイト自身)

git clone https://git.takibi.wtf/yuki/okibi.git

← コミット一覧

1614a4047481d0c24e3153142204dc96d722ed75
yuki
1781058565
web: 機能紹介ページ /about + トップにヒーロー&機能グリッド

---
 src/main.rs   |  1 +
 src/style.css | 26 +++++++++++++++++++
 src/web.rs    | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 106 insertions(+), 2 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f1a604e..d529c6b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -42,6 +42,7 @@ async fn main() -> anyhow::Result<()> {
 
     let app = Router::new()
         .route("/", get(web::index))
+        .route("/about", get(web::about))
         .route("/healthz", get(healthz))
         .route("/login", get(web::login_form).post(web::login_submit))
         .route("/auth/verify", get(web::verify))
diff --git a/src/style.css b/src/style.css
index 791d56f..40163ea 100644
--- a/src/style.css
+++ b/src/style.css
@@ -62,3 +62,29 @@ footer {
   border-top: 1px solid var(--line); color: var(--muted); font-size: 12px;
   text-align: center; padding: 24px;
 }
+.hero { text-align: center; padding: 32px 0 20px; }
+.hero .flame { font-size: 52px; filter: drop-shadow(0 0 24px rgba(226,88,34,.6)); }
+.htitle {
+  font-size: 44px; margin: 8px 0 4px; letter-spacing: 1px;
+  background: linear-gradient(90deg, var(--fire2), var(--fire));
+  -webkit-background-clip: text; background-clip: text; color: transparent;
+}
+.htag { color: var(--muted); font-size: 15px; margin: 0 0 20px; }
+.hcta { display: flex; gap: 12px; justify-content: center; }
+.btn {
+  display: inline-block; background: var(--fire); color: #fff !important;
+  padding: 11px 22px; border-radius: 10px; font-weight: 600; text-decoration: none;
+}
+.btn:hover { background: var(--fire2); text-decoration: none; }
+.btn.ghost { background: transparent; border: 1px solid var(--line); color: var(--link) !important; }
+.features {
+  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
+  gap: 14px; margin: 18px 0 28px;
+}
+.feature {
+  background: var(--panel); border: 1px solid var(--line);
+  border-radius: 12px; padding: 18px;
+}
+.feature .ficon { font-size: 26px; }
+.feature h3 { margin: 8px 0 6px; font-size: 16px; }
+.feature p { font-size: 13px; margin: 0; }
diff --git a/src/web.rs b/src/web.rs
index 2363703..8591aa6 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -22,6 +22,7 @@ fn layout(title: &str, user: &Option<User>, body: Markup) -> Markup {
                 header.top {
                     a.brand href="/" { "🔥 Okibi" }
                     nav {
+                        a href="/about" { "機能" }
                         @if let Some(u) = user {
                             a href="/new" { "+ 新規リポジトリ" }
                             a href={"/" (u.handle)} { "@" (u.handle) }
@@ -46,14 +47,66 @@ async fn current(state: &AppState, headers: &HeaderMap) -> Option<User> {
     auth::current_user(&state.db, headers).await
 }
 
+/// Feature list — single source of truth for the about page & home hero.
+fn features() -> [(&'static str, &'static str, &'static str); 8] {
+    [
+        ("🔥", "焚き火だけ", "atsm の仲間だけがログイン。magic-link と管理者の招待リンクで、メールサーバ無しでも入れる。"),
+        ("📦", "リポジトリ", "公開/非公開を選んで作成。git push / pull は HTTPS + アクセストークンで。"),
+        ("👀", "コード閲覧", "Web でファイルツリーを辿り、ファイルの中身まで読める。ログイン不要で公開リポを覗ける。"),
+        ("📥", "取り込み", "GitHub 等の URL を渡すと、Okibi マシンが直接 mirror clone。巨大リポも自宅回線を通さず取り込む。"),
+        ("⚙️", "簡易CI", "リポジトリに .okibi/ci.sh を置けば push 時に実行。テストやビルドを自動で。"),
+        ("🔑", "トークン", "用途ごとに PAT を発行。パスワード代わりに push、いつでも作り直せる。"),
+        ("👥", "メンバー", "管理者が allowlist を管理し、招待リンクを発行。焚き火の輪を広げる。"),
+        ("🛡", "安全より", "秘密はハッシュで保存。git 処理は実 git に委譲し、自前で pack を再実装しない。"),
+    ]
+}
+
+fn feature_grid() -> Markup {
+    html! {
+        div.features {
+            @for (icon, title, desc) in features() {
+                div.feature {
+                    div.ficon { (icon) }
+                    h3 { (title) }
+                    p.muted { (desc) }
+                }
+            }
+        }
+    }
+}
+
+fn hero() -> Markup {
+    html! {
+        section.hero {
+            div.flame { "🔥" }
+            h1.htitle { "Okibi" }
+            p.htag { "焚き火の仲間だけの、自前 Git ホスティング。" br;
+                     "おき火=燃え続ける熾火。コードが、残る場所。" }
+            div.hcta {
+                a.btn href="/about" { "機能を見る" }
+                a.btn.ghost href="/login" { "ログイン" }
+            }
+        }
+    }
+}
+
 // GET /
 pub async fn index(State(state): State<AppState>, headers: HeaderMap) -> Response {
     let user = current(&state, &headers).await;
     let repos = repos::list_visible(&state.db, user.as_ref()).await;
     let body = html! {
-        h1 { "リポジトリ" }
+        @if user.is_none() {
+            (hero())
+            (feature_grid())
+            h2 { "公開リポジトリ" }
+        } @else {
+            h1 { "リポジトリ" }
+        }
         @if repos.is_empty() {
-            p.muted { "まだ何もない。" a href="/new" { "最初の一本を建てる" } "。" }
+            p.muted {
+                @if user.is_some() { "まだ何もない。" a href="/new" { "最初の一本を建てる" } "。" }
+                @else { "まだ公開リポジトリはありません。" }
+            }
         }
         ul.repolist {
             @for (r, owner) in &repos {
@@ -68,6 +121,30 @@ pub async fn index(State(state): State<AppState>, headers: HeaderMap) -> Respons
     Html(layout("home", &user, body).into_string()).into_response()
 }
 
+// GET /about
+pub async fn about(State(state): State<AppState>, headers: HeaderMap) -> Response {
+    let user = current(&state, &headers).await;
+    let body = html! {
+        (hero())
+        h2 { "できること" }
+        (feature_grid())
+        section.card {
+            h2 { "使いはじめる" }
+            ol {
+                li { "焚き火のメンバーに招待リンクをもらう(または管理者に依頼)" }
+                li { "ログインしたら " code { "設定" } " で PAT を発行" }
+                li { code { "git clone https://<handle>:<PAT>@okibi.fly.dev/<handle>/<repo>.git" } }
+                li { "push すれば " code { ".okibi/ci.sh" } " が回る(CI 有効時)" }
+            }
+        }
+        section.card {
+            h2 { "技術" }
+            p.muted { "Rust + axum + SQLite + maud。git smart-HTTP は実 git-http-backend に委譲。" }
+        }
+    };
+    Html(layout("機能", &user, body).into_string()).into_response()
+}
+
 // GET /{owner}
 pub async fn user_view(
     State(state): State<AppState>,