監視ダッシュボード (dashboard)
ブラウザでチーム状況を一望する read-only サーバーです。Node の標準ライブラリのみで実装され、外部 npm 依存はゼロ、LLM も一切呼びません。
設計の柱:read-only と「許可リスト方式」
サーバーの安全性は「禁止を列挙する」のではなく「許可を列挙する」アプローチで担保されています。server.js の入口で 3 つのゲートを通します。
① メソッドのゲート
既定は GET/HEAD のみ。状態を変える POST は MUTATING_PATHS に列挙した 3 つだけ許可します。
const MUTATING_PATHS = ['/api/archive-session', '/api/restore-session', '/api/pin-session'];
function isMethodAllowed(method, pathname) {
if (method === 'GET' || method === 'HEAD') return true;
return method === 'POST' && MUTATING_PATHS.includes(pathname);
}しかもこの 3 つの変更操作すら「ファイルを移動するだけ・中身は不変・完全に可逆」に限定されており、LLM 非依存です。
② Host ヘッダのゲート
config.json の allowedHosts に含まれない Host からのアクセスは 403 で弾きます(DNS リバインディング等への最小防御)。ポートは無視して比較します。
function isHostAllowed(hostHeader, allowedHosts) {
if (!allowedHosts || allowedHosts.length === 0) return true;
if (!hostHeader) return false;
const hostname = String(hostHeader).split(':')[0].toLowerCase();
return allowedHosts.map((h) => String(h).toLowerCase()).includes(hostname);
}WARNING
別の PC やスマホ(例:Tailscale 経由)から見る場合は、host を 0.0.0.0 にした上で allowedHosts にアクセス元のホスト名/IP を必ず追加する必要があります。未追加だと 403 で弾く安全側の挙動です。
③ 静的ファイルのゲート
配信できる静的ファイルも PUBLIC_FILES で固定列挙(/, /index.html, /README.md, /demo.html)。任意パスは配信しません。ファイル読み取り系のヘルパ resolveSafe() は、解決後の絶対パスが基準ディレクトリの外に出るとエラーを投げ、ディレクトリトラバーサルを防止します。
設定はリクエスト毎に読み直す
config.json はリクエストごとに readConfig() で読み直されます。これにより、サーバーを再起動せずにチーム名や許可ホストの変更を反映できます(ただし listen 中の port/host だけは起動時に固定)。
const server = http.createServer(async (req, res) => {
const config = readConfig(CONFIG_PATH); // 毎リクエストで読み直し、再起動なしで設定変更を反映
...
});クライアントへ返す設定は sanitizePublicConfig() で 公開してよい範囲だけに絞られます。serverOnly(パス・スクリプト・起動コマンド等)は一切含めず、agents も label/color だけに削ぎ落とします。
API 一覧(すべて read-only、一部 POST)
lib/api.js が ROUTES テーブルでルーティングします。
| エンドポイント | 内容 | メソッド |
|---|---|---|
/api/config | 公開用にサニタイズした設定 | GET |
/api/tasks | tasks/*.md を読み取り | GET |
/api/projects | project_*.md の一覧 | GET |
/api/skills | skillsDir 配下の SKILL.md 一覧 | GET |
/api/agents | tmux ペインの稼働状況 | GET |
/api/sessions | resume 可能なセッション一覧(新しい順) | GET |
/api/archive | アーカイブ済みセッション一覧 | GET |
/api/memory-search | 記憶(FTS5)横断検索 | GET |
/api/session-search | 本物のセッションに絞った全文検索 | GET |
/api/log-search | 全 transcript を grep | GET |
/api/docs | 許可リスト一致のドキュメントのみ読む | GET |
/api/archive-session | セッションをアーカイブへ移動 | POST |
/api/restore-session | アーカイブから復元 | POST |
/api/pin-session | ピン留めのトグル | POST |
/api/health | 死活確認 | GET |
各データソースの読み取りは lib/sources.js に集約され、どれも「ファイルを読む / 既存 CLI を呼ぶ / grep を呼ぶ」だけで課金が乗る経路を持ちません。
- 記憶検索:
index_memory.py search --jsonをexecFileで呼ぶ。 - 生ログ検索:
grep -rIiF -m1で全 transcript を検索(exit code 1 = 該当なしを正常扱い)。 - tmux:
tmux list-panesを 3 秒タイムアウトで呼び、無ければ空配列。
セッション発見ロジックの妙:lib/sessions.js
ダッシュボードの目玉は「散らばった transcript から 本物のセッションを見つけて一覧・検索する」ことです。ここには Claude Code の transcript 構造に踏み込んだ実装があります。
大きな transcript を端だけ読む
セッションの表示には「cwd」「最初のユーザー発話」「タイトル」が要りますが、transcript 全文を読むのはセッション数 × ファイルサイズで重い。そこで 先頭 256KB と末尾 64KB だけを読みます。
const HEAD_BYTES = 262144; // 256KB: 先頭ユーザー発話は CLAUDE.md/記憶の自動注入込みで大きいことがある
const TAIL_BYTES = 65536; // 64KB: 末尾のタイトルレコードを拾う範囲- 先頭から:
cwdと最初のユーザー発話を拾う。発話はsystem-reminderブロックや XML 風タグを除去(cleanText)してから採用。 - 末尾から:タイトルレコード(
ai-title/custom-title)を拾う。末尾=最新として優先。
タイトルの優先順位は 手動命名(/rename = custom)> 自動生成(ai)> 最初の発話 です(pickSessionTitle)。なお 256KB〜末尾 64KB の中間帯に挟まった custom-title は拾えないという妥協がコメントで明記されています(全文走査を避けるための割り切り)。
稼働中(live)判定:pid 生存チェック
「いま動いているセッションか」は、ライブレジストリ(~/.claude/sessions/*.json)に書かれた pid が生きているかで判定します。process.kill(pid, 0) はシグナルを送らず生存確認だけを行う Unix の定石です。
if (j.sessionId && j.pid) {
try { process.kill(j.pid, 0); live.add(j.sessionId); }
catch (_e) { /* 死んでる */ }
}稼働中セッションはアーカイブ対象から自動除外され(409 を返す)、誤って動作中の会話を退避してしまう事故を防ぎます。
「本物のセッション」だけに絞る検索
session-search は grep で当たった jsonl のうち、ファイル名が UUID かつ **親フォルダが - 始まり(プロジェクト直下)**のものだけを採用します。これによりサブエージェントやツール結果の jsonl を除外し、resume 可能な本セッションだけを返します。
if (!UUID_RE.test(base) || !parent.startsWith('-')) continue; // 本セッション かつ プロジェクト直下のみactive なら resume コマンド、archive なら復元コマンドを各ヒットに付けて返すのも気の利いた点です。
ピン留めはサイドカー JSON
ピン留めはセッション実体に触れず、別ファイル pins.json に ID を記録するだけ(サイドカー方式)。ピン留めしたセッションは limit から漏れても必ず一覧の先頭に表示されます。
セッションのアーカイブは「移動するだけ」
moveSessionFile() は ~/.claude/projects/<folder>/<id>.jsonl を ~/.claude/projects-archive/<folder>/ へ fs.rename するだけです。中身は不変・プロジェクトフォルダ名を維持・完全に可逆。同じ処理は CLI(scripts/manage_sessions.sh)からも実行でき、ダッシュボードの POST API と CLI が同じファイル移動セマンティクスを共有します(運用参照)。
デモと起動
cd dashboard
cp config.example.json config.json # <...> を自分の環境値に置換
node server.js # 既定 :8080サーバーを立てなくても、dashboard/demo.html をブラウザで開けば サンプルデータ入り・各タブに使い方解説つきの自己完結 HTML で全体像を確認できます。
start.sh は冪等で、既に同ポートでダッシュボードが稼働中なら /api/health で検知して正常終了します。手動起動・tmux 起動・プラグイン monitor が競合しても二重起動・クラッシュになりません。
テスト
ダッシュボードは node:test 組み込みのみ(外部依存ゼロ)で、lib/ の各モジュール(純粋関数・データソース読み取り・セッション操作・API ルーティング)を単体テストし、サーバーを実起動して全 API を叩く統合テスト(アーカイブ往復・method/host ガード含む)まで備えます。
cd dashboard && npm test