Skip to content

Web (Dashboard) 仕様

apps/noisy-crow-appReact 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-coreexports から composition/web-deps.tsbuildWebDeps(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-coredomain/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 には無く、別 Worker noisy-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 ページは廃止)
/emojisroutes/emojis.tsx絵文字一覧 (ページング + サーバー側フィルタ)、除外/含める
/emojis/:nameroutes/emoji-detail.tsx絵文字詳細。キャプション手動修正 / AI 再解析 (追加指示つき)
/syncsroutes/syncs.tsx同期の起動 (Sync / Rebuild)、進捗パネル、ジョブ履歴
/searchroutes/search.tsxクエリでベクトル検索
/visualizationroutes/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 } を返す。履歴は D1 sync_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 } をインライン表示する。
  • <Form method="get">?q&limit&threshold を URL に載せ、loadersearchEmojis() を実行。結果は共有可能な URL になる。
  • threshold 既定 0.7、limit 既定 5。busy 表示は useNavigation()

Visualization (/visualization)

  • 「Generate」で useFetcheraction(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.tomllimits.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.tsroot.tsx の nav (NavLink) を両方更新し、loader / action に requireSession を入れる (/sign-in/api/auth/* を除く)。
  • 新しいデータ操作を呼びたいときは:
    1. 必要なら共有コア packages/noisy-crow-core にユースケース関数を追加 (domain ← application ← infrastructure を維持)
    2. app/lib/deps.server.ts で re-export
    3. 対象ルートの loader / action から buildWebDeps(env) 越しに呼ぶ という順で進める。型は共有コア (@tied-workspace/noisy-crow-core) を import し、ダッシュボードで再定義しない。