Web (Dashboard) 仕様
apps/noisy-crow-app は React Router v7 (framework モード) の管理 Web を担う UI Worker noisy-crow-web。Cloudflare Workers 上で SSR し、UI 配信・データ操作 (loader / action) のみを担う。データアクセス層は共有コア @tied-workspace/noisy-crow-core に依存する。Slack Events 受信 (/slack/events) は持たない (専用の公開 Worker noisy-crow-events に分離済み)。旧構成 (Vite SPA on Pages + 別ドメインの Hono API + Bot Container + Queue Consumer) を整理した結果。管理 UI は better-auth (共有認証基盤 packages/auth) のセッションで保護 する (Sign in with Slack / OIDC。Cloudflare Access は使わない)。
技術スタック
- React Router v7 (
react-router+@react-router/dev、framework モード /ssr: true) @cloudflare/vite-plugin(dev で Miniflare がバインディングを供給、build で Worker を生成)- React 19 / Tailwind CSS v3 (PostCSS)
- データ層は共有パッケージ
@tied-workspace/noisy-crow-coreのexportsからcomposition/web-deps.tsのbuildWebDeps(env)+ ユースケース関数をapp/lib/deps.server.ts経由で import する (自身への self-reference ではなく core パッケージへの依存)。lib/api.tsのような fetch ラッパは無い。 - 可視化系ライブラリ (recharts / Three.js / d3) は使わない。描画は SVG、PCA / t-SNE / k-means は共有コアの自前実装 (
packages/noisy-crow-coreのdomain/vector-projection.ts/domain/clustering.ts) を サーバー側 (action) で実行する。
ディレクトリ
apps/noisy-crow-app/ # UI Worker noisy-crow-web。Web (app/) + 同期 Workflow。src/ は無い (コアは packages/noisy-crow-core)
├── workers/app.ts # Worker エントリ。createRequestHandler + scheduled (cron) + Workflow export
├── workers/sync-workflow.ts # Workflow noisy-crow-sync (絵文字インデックス同期のオーケストレーション)
├── app/
│ ├── root.tsx # <html> / nav / Links / Scripts / ErrorBoundary
│ ├── routes.ts # ルート定義 (UI ルートのみ)
│ ├── lib/deps.server.ts # @tied-workspace/noisy-crow-core の buildWebDeps + usecases を再 export (サーバー専用)
│ └── routes/
│ ├── index.tsx # "/" /emojis へ redirect (Home は廃止)
│ ├── emojis.tsx # "/emojis" loader: 一覧 (?q&page でページング) / action: exclude・include
│ ├── emoji-detail.tsx # "/emojis/:name" loader: 1 件 / action: update-caption・reanalyze
│ ├── syncs.tsx # "/syncs" loader: 進捗 + 履歴 (?page) / action: sync・rebuild
│ ├── search.tsx # "/search" loader: ?q&limit&threshold で検索
│ └── visualization.tsx # "/visualization" action: generate (サーバーで投影 + クラスタリング + AI ラベル)
├── react-router.config.ts # ssr: true
├── vite.config.ts # cloudflare() + reactRouter() + tsconfigPaths()
├── wrangler.toml # name=noisy-crow-web / KV・D1・R2・Vectorize バインディング / assets (D1 スキーマは packages/d1/ で統一管理)
└── worker-configuration.d.ts # Env 型 (バインディング + secret)Slack Events 受信 (
/slack/events、署名検証) は UI Worker には無く、別 Workernoisy-crow-events(apps/noisy-crow-events/src/index.ts+verify-signature.ts) が担う。
ルーティング
app/routes.ts で定義。app/root.tsx のヘッダ nav に NavLink を並べる。
| パス | ルートファイル | 役割 |
|---|---|---|
/ | routes/index.tsx | /emojis へ redirect (Home / Setup ページは廃止) |
/emojis | routes/emojis.tsx | 絵文字一覧 (ページング + サーバー側フィルタ)、除外/含める |
/emojis/:name | routes/emoji-detail.tsx | 絵文字詳細。キャプション手動修正 / AI 再解析 (追加指示つき) |
/syncs | routes/syncs.tsx | 同期の起動 (Sync / Rebuild)、進捗パネル、ジョブ履歴 |
/search | routes/search.tsx | クエリでベクトル検索 |
/visualization | routes/visualization.tsx | 絵文字 + クエリの 2D 可視化 (PCA / t-SNE + 事前/事後クラスタリング + AI カテゴリラベル) |
Home (ダッシュボード) と Setup ページは利用されていなかったため 2026-06 に廃止した。
/は/emojisへ redirect する。
ページ別の仕様
Emojis (/emojis)
loader: URL クエリ?q&pageを読み、listEmojis({ page, perPage: 60, filter: q })で 1 ページ分だけ 取得する ({ emojis, excluded, total, page, perPage, q })。全件をブラウザに送らないため、表示速度がインデックス件数に依存しない。excludedからSetを作りカードを薄表示。- フィルタはサーバー側 (D1 の
LIKE、name / description 部分一致)。<Form method="get">で?qに載せるため URL を共有できる。フィルタ変更でページは 1 に戻る。 - ページャ:
total/perPageから総ページ数を計算し、前へ / 次へのリンク (?q&page) を表示する。 action:intentで分岐 (exclude/include)。useFetcherで送信し、完了後に RR7 が loader を自動再検証して一覧が更新される。- カードの絵文字部分 (画像 + 名前 + description) は詳細ページ
/emojis/:nameへのリンク。 - 同期の起動・進捗表示は
/syncsに分離した (このページに sync UI は無い)。
Sync (/syncs)
loader: URL クエリ?pageを読み、getSyncStatus()+listSyncHistory({ page, perPage: 20 })で{ syncStatus, history }を返す。履歴は D1sync_jobsから startedAt 降順。action:intentで分岐 (sync/rebuild)。Sync from Slack (diff) と Rebuild (rebuild、window.confirmつき) の 2 ボタン。どちらも Workflow を起動して即返り、ジョブ実行中は両ボタンを disable。実行中に押した場合は「既に実行中」の案内を表示。- 進捗パネル:
syncStatusがあれば phase (対象計算中 / キャプション生成中 / 埋め込み生成中 / 完了 / 失敗)・processed/total・削除数・警告 (caption fallback 等) を表示。実行中はuseRevalidatorで 3 秒間隔で loader を再検証して進捗を反映する。ページを閉じてもジョブは Workflow 側で続行する。 - 履歴テーブル: 開始時刻 / モード / 状態 / 処理数 / 削除数 / 警告数 / 失敗理由。
?pageでページング (20 件/ページ)。
Emoji 詳細 (/emojis/:name)
loader:deps.vectors.get(name)+getSyncStatus()で{ emoji, syncStatus }。未登録なら 404。- 画像・名前・最終更新・モデル情報を表示。手動編集された caption は「caption: 手動編集」と表示する。
- キャプションを修正: textarea で description を編集して保存 →
action(intent=update-caption)→updateEmojiCaption()。手動 caption で embedding を作り直し、captionModel: "manual"を記録する (Rebuild でも再生成されない)。 - AI で再解析: 追加指示 (任意) を入れて実行 →
action(intent=reanalyze)→reanalyzeEmoji()。Gemini 単件呼び出しで caption を再生成し embedding を作り直す。 - どちらも
useFetcherで送信し、完了後は loader の自動再検証で表示が更新される。sync ジョブ実行中は両フォームを disable し (サーバー側でもユースケースが拒否)、エラーは{ error }をインライン表示する。
Search (/search)
<Form method="get">で?q&limit&thresholdを URL に載せ、loaderがsearchEmojis()を実行。結果は共有可能な URL になる。threshold既定 0.7、limit既定 5。busy 表示はuseNavigation()。
Visualization (/visualization)
- 「Generate」で
useFetcher→action(intent=generate)→ 共有コアのvisualizeEmojis({ method, queries, clusterCount })を サーバー側で 実行する。処理は (1)listWithVectors()で全ベクトル取得 → (2) 事前クラスタリング (1024 次元 embedding の k-means) → (3) PCA / t-SNE で 2D 投影 → (4) 事後クラスタリング (投影後 2D 座標の k-means) → (5) 事前/事後それぞれのクラスタに AI でカテゴリ名 + 説明を生成 (AiGateway.generateClusterLabels、並列 2 リクエスト)。ベクトル (1024 次元 × 全件) はブラウザに送らず、返るのは 2D 座標 + クラスタ情報のみ。 - query 点はローカル state の文字列リスト (「Add」はサーバーを呼ばない)。Generate 時にサーバーが
generateQueryEmbedding()で embedding し、絵文字と同じ平面に投影する。query 点には high-D 最近傍 10 件 (Euclidean) を付与。 - クラスタ数は Auto (件数から自動決定、2–12) または固定値 (4–20) を選択。AI ラベル生成に失敗した場合は「クラスタ N」の仮ラベル + 警告表示にフォールバックし、可視化自体は成立させる。
- SVG (
viewBox="0 0 640 480") で散布図。絵文字点は クラスタ別に色分け (事前 / 事後をトグルで切替)、query 点は赤。クラスタ凡例 (AI 生成のカテゴリ名・説明・代表絵文字) のクリックで該当クラスタを強調表示。点クリックで右ペインに詳細 (所属クラスタのラベル + 2D 最近傍)。 - t-SNE は点数 2000 超で PCA にフォールバックする (警告表示)。サーバー側 CPU を使うため UI Worker の
wrangler.tomlでlimits.cpu_ms: 300000を設定している。
データ取得 (loader / action)
- ブラウザは独自の API クライアントを持たない。
<Form>/useFetcher/ リンク遷移で loader / action を呼ぶ。 - loader / action は
app/lib/deps.server.tsからbuildWebDepsとユースケースを import し (中身は共有コア@tied-workspace/noisy-crow-coreの再 export)、context.cloudflare.envを渡して実行する。 deps.server.tsは サーバー専用。.server.ts命名により RR7/Vite がクライアントバンドルから除外する (@slack/web-api等の Node 依存がブラウザに混入しない)。
不変条件
- データ操作は loader / action 経由のみ。ブラウザに配信されるコードから Cloudflare バインディングや Slack を直接叩かない。
- 依存追加は慎重に。可視化系ライブラリ (recharts / Three.js / d3) を入れない。SVG + 自前 PCA/t-SNE で完結させる。
- 認証ロジックをアプリに書かない。認証は共有認証基盤
@tied-workspace/authに集約し、ページ側はrequireSessionを loader / action 冒頭で呼ぶだけ (GET/POST 両方)。 - ルートを追加するときは
app/routes.tsとroot.tsxの nav (NavLink) を両方更新し、loader / action にrequireSessionを入れる (/sign-inと/api/auth/*を除く)。 - 新しいデータ操作を呼びたいときは:
- 必要なら共有コア
packages/noisy-crow-coreにユースケース関数を追加 (domain ← application ← infrastructureを維持) app/lib/deps.server.tsで re-export- 対象ルートの loader / action から
buildWebDeps(env)越しに呼ぶ という順で進める。型は共有コア (@tied-workspace/noisy-crow-core) を import し、ダッシュボードで再定義しない。
- 必要なら共有コア