ステータス: 合意済み(2026-10-05、Issue #17、工程4: 詳細設計)。CODEXのレビュー(#6893)を反映し、OllamaのモデルをGPUから下ろす操作を加えて、運用者が合意した(#17 #6942)。論点A〜Jは運用者が決めた(#6885、本書§20)
更新するときは、設計文書の更新ルールに従うこと。
本書は、ランタイム管理 設計(合意済み)の§4.3・§4.4・§8・§9のうちダッシュボードが担う部分を、実装できる粒度に具体化する。範囲は実装計画書§5.1の#17である。ダッシュボードは、Brainの状態取得(詳細設計: UIプロセスの観測と状態取得§8、以下「observation」)と、運用者向け管理API(詳細設計: 常駐監視対象の登録と、UIへの終了の要求§3、以下「registration」)を使う。
schema_version 2)| 本書で扱う | 扱わない(担当) |
|---|---|
ダッシュボードのプロセス、待ち受け、systemd --userのunit |
自動起動の有効化(systemctl --user enable)と起動順序(#21) |
| Windowsのデスクトップのアイコン(ダッシュボードの起動・停止) | 音声UI自身の起動・停止のUI(#19) |
| ブラウザの認証(ログイン、セッション)と、Brainへのトークンの受け渡し | トークンの配布・ローテーションの自動化(当面は手動。設計書§9.4) |
| Brainの状態の表示(実行環境とBrainの状態取得の組み合わせ) | Brainの状態取得・管理APIそのもの(#16・#24) |
| Brainの起動・停止(実行環境を通して) | UIの起動・再起動、自動復旧(原則5、NFR-03) |
| 管理APIの中継(登録・解除・復帰警告の確認・UIへの終了の要求) | |
| Ollamaのモデルの表示と、GPUから下ろす操作(§4.3、§5.2) | Ollama自身の起動・停止、keep_aliveの設定の変更 |
| 画面 | ログの閲覧・検索 |
| LANから開ける形(待ち受けのアドレス、Windowsファイアウォールの規則の手順) | LAN上の別のPCからの実機検証(段階2) |
keina_assistant.dashboardとし、python -m keina_assistant.dashboardで起動する。FastAPIとuvicornを使う(Brainと同じ。依存を増やさない)DASHBOARD_HOST(既定0.0.0.0)のDASHBOARD_PORT(既定8812)。Brainは今のまま127.0.0.1:8811で待ち受け、LANに向けて開くのはダッシュボードだけにするsystemd --userに任せる(1つのunitは同時に1つしか動かない)。前景で2つ目を起動した場合は、ポートを開けずに終了コード1で終わるdeploy/systemd/keina-dashboard.service(Brainのunit、brain-lifecycle §3と同じ形)。
[Unit]
Description=Keina Dashboard (AiChatWls)
[Service]
Type=exec
WorkingDirectory=%h/develop/AiChatWls
ExecStart=%h/develop/AiChatWls/.venv/bin/python -m keina_assistant.dashboard
Environment=PYTHONUNBUFFERED=1
KillSignal=SIGTERM
TimeoutStopSec=10
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
enable)は#21で行う。本書ではunitの作成とlink(systemctl --user link)までを行うTimeoutStopSecは10秒とするRestart=on-failure)。常駐を保つためである。起動し直すとセッションは失効し、ログインし直しになる。設定の誤り(終了コード2)でも起動し直しを繰り返すが、systemdの既定の上限(10秒間に5回)で止まり、failedになる。アイコン・systemctl --user stopでの停止は正常終了なので、起動し直さない| 手段 | 内容 | 担当 |
|---|---|---|
| 自動起動 | Windowsへのログイン後に、systemd --userがダッシュボードを起動する |
有効化は#21 |
| デスクトップのアイコン「ダッシュボード」 | ダッシュボードを起動し、ブラウザで開く。既に動いていれば開くだけ(§9) | 本書 |
| デスクトップのアイコン「ダッシュボードを止める」 | ダッシュボードを止める(§9) | 本書 |
keina-dashboardのunitを操作する。どの手段で起動しても、同じ1つのダッシュボードになるsystemctl --user status keina-dashboard、journalctl --user -u keina-dashboard)で確認する(設計書§4.3)%USERPROFILE%\.wslconfigの[general]にinstanceIdleTimeout=-1を書き、自動で止めないようにする(運用者の判断。WSL 2.6.3で使える)。この設定は、このPCのすべてのWSLのディストリビューションに効き、使っていない間もWSLのメモリを確保したままにする。効かせるにはwsl --shutdownで止め直す必要があるsystemd --userは、既定ではログインセッションがある間だけ動く(loginctl show-userのLinger=no)。loginctl enable-lingerで、セッションが無くても動き続けるようにするBRAIN_VIEW_TOKENまたは管理トークンBRAIN_ADMIN_TOKENを入力する(設計書§9.3の(1)。ダッシュボード自身が認可する).env(または環境変数)から、閲覧・管理トークンと、会話用トークンBRAIN_AUTH_TOKENを読む。会話用トークンは、照合には使わず、ほかのトークンと別の値であることを確かめるためだけに読む。設定されているトークン同士が1組でも同じ値なら、設定の誤り(終了コード2)とする(Brainが設定の誤りで止まっていても、ダッシュボードは起動するため、Brain側の検証に任せない。会話用トークンを持つUIが、ダッシュボードにログインできないようにするため)401を返す(総当たりを遅くするため)。待つ間はイベントループを止めない(asyncio.sleep。ほかの利用者の要求を待たせない)secrets.token_urlsafe(32))を作り、Cookiekeina_dashboard_sessionで返す。属性はHttpOnly・SameSite=Strict・Path=/。Secureは付けない(HTTPで使うため。設計書§9.2)。Max-Ageは付けない(ブラウザを閉じると消える)DASHBOARD_SESSION_IDLE_HOURS(暫定値12時間)を過ぎたセッションは失効させる。画面を開いたままなら、5秒ごとの読み直し(§7)で延び続ける401・{"detail": {"code": "not_logged_in", …}}。画面はこれを受けてログイン画面に戻る403・forbiddenPOST・DELETE)は、Originヘッダーがあり、その値が要求のHost(http://<Host>)と一致する場合だけ受け付ける。無い、または一致しなければ403・origin_mismatch。SameSite=StrictのCookieと合わせた二重の防止であるOrigin(403)→要求の形式(422)→中継先の結果、とするBRAIN_VIEW_TOKEN、管理の権限ならBRAIN_ADMIN_TOKEN(設計書§9.3の(2)。Brain自身も認可する)DASHBOARD_BRAIN_URLは、ループバックのhttpだけに限る(§11)。BrainへのHTTPクライアントは、リダイレクトを追わず(follow_redirects=False。リダイレクトの応答はinvalid_responseとして扱う)、環境変数のHTTPプロキシを使わない(trust_env=False)。設定の誤りやリダイレクトで、トークンをLAN・プロキシへ送らないためであるすべての応答(画面・スクリプト・API)に次を付ける。
| ヘッダー | 値 | 目的 |
|---|---|---|
Content-Security-Policy |
default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none' |
埋め込まれたスクリプトの実行を止める。ほかのサイトの枠に入れさせない |
X-Content-Type-Options |
nosniff |
種類の取り違えを防ぐ |
Referrer-Policy |
no-referrer |
URLを外へ漏らさない |
Cache-Control |
no-store(/static/以外) |
状態・ログイン状態をブラウザに残さない |
スクリプトとスタイルは、HTMLに埋め込まず、同じダッシュボードの/static/app.js・/static/app.cssとして返す(script-src 'self'のまま、'unsafe-inline'を許さずに済ませるため)。
systemctl --user show keina-brainで、次の項目を取る(--no-pager、-pで項目を限る。タイムアウト5秒)。
LoadState・ActiveState・SubState・Result・ExecMainCode・ExecMainStatus・ActiveEnterTimestamp・InactiveEnterTimestamp
ActiveState |
サービスの状態(service.state) |
|---|---|
active |
running |
activating・reloading |
starting |
deactivating |
stopping |
inactive |
stopped |
failed |
failed |
取得できない(systemctlの失敗、LoadStateがloadedでない) |
unknown |
failedのときは、Result(exit-code・signal・timeout等)と終了コード(ExecMainStatus)を示す。終了コードの意味(brain-lifecycle §8)を画面で添える: 1 その他の失敗、2 設定の誤り、3 二重起動、4 ロックファイルを開けないjournalctl --user -u keina-brainを表示する(設計書§4.3)runningならActiveEnterTimestamp、stopped・failedならInactiveEnterTimestampを示すサービスの状態を正とし(設計書§4.4)、Brainの状態取得(GET /runtime/status。タイムアウト3秒)で詳細を補う。
| サービス | 状態取得 | 画面の表示(brain.display) |
|---|---|---|
running |
応答あり、brain.state: running |
稼働中(running) |
running |
応答あり、brain.state: stopping |
停止処理中(stopping) |
running |
応答なし | 稼働中(応答なし)(running_unreachable)。起動直後にも一時的に起こる |
starting |
— | 起動中(starting) |
stopping |
— | 停止処理中(stopping) |
stopped |
応答なし | 停止中(stopped) |
failed |
応答なし | 起動失敗・異常終了(failed) |
stopped・failed |
応答あり | サービスの外で動いている(outside_service。下記) |
unknown |
— | 不明(unknown) |
stopped・failedのときも、BrainのGET /health/live相当の到達確認として状態取得を試みる。応答があれば(前景で動かしている等)、次のとおり扱う
brain.displayはoutside_service、brain.outside_serviceはtrue、brain.reachableはfalse、brain.unreachable_reasonはoutside_service、brain.statusとruntimeはnullとする(その内容は表示しない。サービスとして管理されていないBrainを、管理されているBrainと取り違えないため)operationsはどちらもfalse)。サービスとして起動すると、単一起動の拒否(brain-lifecycle §4)で失敗するためであるstarting・stopping・unknownのときは、Brainの状態取得を呼ばないrunning_unreachable、またはrunning以外)は、登録一覧・UIの状態・復帰警告・会話の集計を「取得できない(Brainに到達できない)」と表示する。以前に取得した内容を今の状態として見せない(設計書§4.4)。ダッシュボードは、Brainの状態取得の結果を保持しないBrainが使うOllama(同じ実行ホストのollama.service)がGPUに載せているモデルを、OllamaのGET /api/psで取る(タイムアウト3秒)。Brainを通さない(Brainが止まっていても表示し、下ろせるようにするため)。UC-D08。
name・size_vram・expires_atだけとするDASHBOARD_OLLAMA_URLは、Brainの接続先と同じく、ループバックのhttpに限り、リダイレクトを追わず、環境変数のHTTPプロキシを使わない(§3.4、§11)。Ollamaへはトークンを送らないexpires_atがgenerated_atより前のモデルは、下ろす要求を受けて、処理中の回答が終わるのを待っている(§5.2)。画面には「下ろし中(処理中の回答が終わると下りる)」と出す200でない、形式が違う)ときは、区画を「取得できない(Ollamaに到達できない)」とする。以前に取得した内容を今の状態として見せない。Brainの表示(§4.2)には影響させない(Brainの利用可能性は、Brainの状態取得が示す)systemctl --user --no-block start -- <unit>とsystemctl --user --no-block stop -- <unit>に固定する(<unit>はDASHBOARD_BRAIN_UNIT。既定keina-brain.service。--で、unit名がオプションと解釈されないようにする)。引数のリストで渡し、シェルを使わない。サービス名・コマンドをブラウザからの入力で受け取らない(設計書§4.4)--no-blockにより、要求は起動・停止の完了を待たずに返る(停止には最大約1分かかる。brain-lifecycle §9)。結果は、画面の読み直し(§7)で状態の変化として見せる続けて押した場合・途中で押した場合(設計書§12.2で詳細設計へ送った論点):
| 押したとき | 起動 | 停止 |
|---|---|---|
stopped・failed |
実行する(202) |
何もしない(200、"already": true) |
running |
何もしない(200、"already": true) |
実行する(202) |
starting |
断る(409・brain_busy、今の状態を添える) |
断る(同) |
stopping |
断る(同) | 断る(同) |
unknown |
断る(409・brain_state_unknown) |
断る(同) |
| サービスの外で動いている(§4.2) | 断る(409・brain_outside_service。前景のBrainを止めるよう案内する) |
断る(同) |
systemctl showで行う。判定と実行の間に状態が変わった場合も、systemctl start・stop自体が冪等であるため、Brainの状態は乱れないsystemctlが失敗した場合(終了コードが0でない)は500・systemctl_failedとし、systemctlの標準エラー出力(トークンを含まない)をmessageに添える{"accepted": true, "already": false, "service": {…§4.1の状態…}}。serviceは、操作を決めた時点(コマンドを実行する前)の状態である。実行後の状態は、次の読み直しで分かるPOST /api/ollama/unload、本文{"model": "<モデル名>"}。モデル名は1〜200文字で、[A-Za-z0-9][A-Za-z0-9._:/-]*の形とする。違えば422・invalid_requestGET /api/psで判定する。到達できなければ503・ollama_unreachable
POST /api/generate・{"model": "<モデル名>", "keep_alive": 0}を送り(タイムアウト10秒)、200なら{"accepted": true, "already": false, "model": "<モデル名>"}を返す200・{"accepted": true, "already": true, "model": "<モデル名>"}を返す(既に下りている。画面に「既に下りています」と出す)200以外を返した、または送る途中で失敗したときは502・ollama_failedとし、Ollamaのエラーの文言をmessageに添える/api/psにはモデルが残る(§4.3の「下ろし中」)keep_alive(OLLAMA_KEEP_ALIVE)の期間、載ったままになる。ダッシュボードはkeep_aliveの設定を変えないregistration §3の4本を、ダッシュボードのAPI(§8)から中継する。管理の権限を要し、Brainへは管理トークンを付ける。
| ダッシュボード | Brain |
|---|---|
POST /api/targets(本文: 監視対象キー3要素) |
POST /runtime/targets |
DELETE /api/targets/{ui_kind}/{host_id}/{target_id} |
DELETE /runtime/targets/{ui_kind}/{host_id}/{target_id} |
POST /api/recovery-warnings/{warning_id}/ack |
POST /runtime/recovery-warnings/{warning_id}/ack |
POST /api/processes/{process_instance_id}/exit-request |
POST /runtime/processes/{process_instance_id}/exit-request |
422・invalid_request(Brainへ不正なパスを送らないため)detail.codeをそのまま画面が使う)。ただし、Brainの401(ダッシュボードの持つトークンがBrainと食い違っている)は、502・brain_auth_failed(「ダッシュボードとBrainのトークンが一致しません」)に置き換える(利用者のログイン切れと区別するため)503・brain_unreachable。要求を溜めない(設計書§7.8。ダッシュボードは状態を持たない)GET /でHTML 1枚を、/static/app.js・/static/app.cssでスクリプトとスタイルを返す(§3.5)。ビルドの道具・外部のライブラリ・CDNを使わない(LANの中だけで完結させるため)GET /api/stateを呼び、画面を描き直す。前の呼び出しが終わってから5秒後に次を呼ぶ(呼び出しを重ねない。systemctl・Brainが遅いときに要求が積み上がらないようにするため)。操作の後は直ちに読み直すhost_label・app_version・ui_kind・target_id等)、Brain・systemctlのエラーの文言(detail.message)、その他のAPIの値は、textContent(またはcreateTextNode)で入れ、innerHTML・outerHTML・insertAdjacentHTML・document.writeでは入れない。HTMLとして解釈させないためである(会話用トークンを持つクライアントは任意のhost_labelを申告できる。それが管理のセッションの画面でスクリプトとして動くと、管理の操作を乗っ取られる)。§3.5のCSPは、この規則を破ったときの二重の防止であるgenerated_atとの差で計算する)| 区画 | 内容 | 根拠 |
|---|---|---|
| 見出し | ログインしている権限(閲覧/管理)、最終更新時刻、ログアウト | — |
| Brain | 表示の状態(§4.2)、状態が変わった時刻、起動時刻・commit(短縮7文字。取得できなければ「不明」)・人向けversion、利用可能性(ready/not_ready/unknown)と失敗した依存先、停止処理中、猶予期間の終わり、対応するプロトコル版。failedなら結果・終了コードの意味・確認先。管理なら[起動][停止] |
UC-B04、UC-B05、UC-D02、UC-D03 |
| 常駐監視対象 | 監視対象キー(ホストはhost_label、無ければhost_idの先頭12文字)、表示上の状態(稼働中/非対応版/未稼働/応答なし/不明)、重複起動、登録日時、一致するプロセス(各プロセスの起動時刻・commit・版・最終確認時刻・終了の要求)、最終観測の要約(終わり方・時刻)。管理なら[解除]、プロセスごとに[終了を求める] |
UC-D01、UC-D02、UC-D03、UC-D06、UC-R08 |
| 未登録のUI | プロセスごとに、監視対象キー、状態(稼働中/応答なし)、重複起動、起動時刻・commit・版・最終確認時刻、終了の要求。管理なら[登録][終了を求める] | UC-D01、UC-D06、UC-R07、UC-R08 |
| 復帰警告・バックアップ | 未確認の復帰警告(発生日時を必ず表示、種別、戻した世代の日時または「戻せる世代なし」、退避したファイルまたは「該当なし」/「退避未完了」(registration §4.6))。registry.backupがstale、registry.recovery_pendingがtrueなら警告を出す。管理なら警告ごとに[確認済みにする] |
UC-D07 |
| 会話 | セッション数、処理中件数だけ | UC-D04 |
| Ollama | GPUに載っているモデルごとに、名前、VRAMの使用量(GB、小数1桁)、自動で下りる予定の時刻と残り時間、または「下ろし中」(§4.3)。管理ならモデルごとに[GPUから下ろす] | UC-D08 |
403で断る)exit_request_deliverableがfalseのプロセスでは「このUIには伝わりません(版1等)」と出す。既に求めている(exit_requested)なら、求めた時刻を出すdetail.messageを出す(detail.codeで表現を変えない。Brainの文言をそのまま使う)| メソッド・パス | 用途 | 要る権限 |
|---|---|---|
GET / |
画面(ログイン前はログイン画面を出す) | なし(データを含まない) |
GET /healthz |
ダッシュボード自身の生存確認(アイコンの起動待ちに使う)。{"status": "ok"} |
なし |
POST /api/login |
ログイン(本文{"token": "…"})。{"role": "view"|"admin"} |
なし |
POST /api/logout |
ログアウト | セッション |
GET /api/state |
画面の内容(§8.1) | 閲覧 |
POST /api/brain/start・/api/brain/stop |
Brainの起動・停止(§5) | 管理 |
| §6の4本 | 管理APIの中継 | 管理 |
POST /api/ollama/unload |
Ollamaのモデルを GPU から下ろす(§5.2) | 管理 |
/docs・/redoc・/openapi.jsonは無効にする(Brainと同じ。observation §3.2)POST /api/login・POST /api/logoutもOriginを検証する(§3.3)GET以外で、未知のパスは404GET /api/state{
"generated_at": "2026-09-30T10:00:00.000+09:00",
"role": "admin",
"brain": {
"display": "running",
"service": {
"state": "running",
"active_state": "active",
"sub_state": "running",
"result": "success",
"exit_status": 0,
"since": "2026-09-30T09:00:00+09:00"
},
"reachable": true,
"unreachable_reason": null,
"outside_service": false,
"status": { "…": "Brainの状態取得のbrain(observation §8.1)" }
},
"runtime": {
"schema_version": 2,
"targets": [],
"unregistered_processes": [],
"recovery_warnings": [],
"registry": { "backup": "ok", "backup_stale_since": null, "recovery_pending": false },
"conversations": { "session_count": 0, "in_flight_requests": 0 }
},
"ollama": {
"reachable": true,
"unreachable_reason": null,
"models": [
{ "name": "gemma4:12b", "size_vram": 13000000000, "expires_at": "2026-10-01T10:00:00+09:00", "unloading": false }
]
},
"operations": { "brain_start": false, "brain_stop": true, "ollama_unload": true }
}
runtimeは、Brainの状態取得のbrain・generated_at以外をそのまま入れる。Brainに到達できなければnullbrain.statusは、Brainの状態取得のbrain。到達できなければnullbrain.unreachable_reason: 到達できないときの理由(connection_refused・timeout・auth_failed・invalid_response・not_running・outside_service)brain.displayは§4.2の表の値(running・stopping・running_unreachable・starting・stopped・failed・outside_service・unknown)operations: 管理の権限で、今その操作が受け付けられるか(brain_start・brain_stopは§5.1の表の「実行する」に当たるか、ollama_unloadはOllamaに到達できるか)。閲覧の権限では、すべてfalseollama: §4.3。到達できなければreachableがfalse、modelsがnull、unreachable_reasonがconnection_refused・timeout・invalid_responseのいずれか。unloadingはexpires_atがgenerated_atより前であることschema_versionが2でなければ、runtimeをnullとし、unreachable_reasonをinvalid_responseとする(知らない形式を表示しない)Windows側に置くファイルは、このリポジトリのtools/windows/に置く(AiChatリポジトリは変更しない)。
| ファイル | 内容 |
|---|---|
dashboard-open.ps1 |
引数-Distro・-Port。wsl.exe -d <Distro> -- systemctl --user start keina-dashboardを実行し、http://127.0.0.1:<Port>/healthzが応答するまで最大15秒待ってから、既定のブラウザでhttp://127.0.0.1:<Port>/を開く。既に動いていれば、startは何もしないので、開くだけになる |
dashboard-stop.ps1 |
引数-Distro。wsl.exe -d <Distro> -- systemctl --user stop keina-dashboardを実行する |
install-dashboard-shortcuts.ps1 |
引数-Distro(既定Ubuntu)・-Port(既定8812)。デスクトップに2つのショートカット(「ダッシュボード」「ダッシュボードを止める」)を作り、同じ-Distro・-Portを各スクリプトの引数として埋め込む |
powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File <スクリプト>を呼ぶ。コンソールの窓は出さない(一瞬出ることはある)wsl.exe・systemctlの終了コードが0でない、15秒待っても応答しない)は、メッセージボックスで理由と確認先(systemctl --user status keina-dashboard)を出す\\wsl.localhost\<ディストリビューション>\home\…\tools\windows\から直接実行する(コピーしない。更新が自動で反映されるようにするため)wsl.exeの呼び出しでWSLごと起動するDASHBOARD_PORTを既定(8812)から変えたときは、同じ値を-Portに渡してショートカットを作り直し、ファイアウォールの規則(§10)も同じポートで作り直す(runbookに書く)。ポートの値は、.env・ショートカット・ファイアウォールの規則の3か所にあり、1か所だけ変えると、起動の待ち合わせ・開くURL・LANからの到達が合わなくなるnetworkingMode=mirroredで動いている(%USERPROFILE%\.wslconfig)。この構成では、WSL2で0.0.0.0に待ち受けたポートは、Windowsの127.0.0.1とLAN側のアドレスの両方から届く。Brainの127.0.0.1:8811は、WindowsとWSL2の中からだけ届くDASHBOARD_PORT(既定8812)のTCPの受信だけを、プライベートのネットワークに限って許す(ポートは§9と同じ値にする)127.0.0.1に限りたい場合(LANから開かない)は、DASHBOARD_HOST=127.0.0.1にする.envまたは環境変数で指定する。Brainと同じ.envを読む。
| 名前 | 既定値 | 用途 |
|---|---|---|
BRAIN_VIEW_TOKEN・BRAIN_ADMIN_TOKEN |
(Brainと共通) | ログインの照合(§3.1)と、Brainへの受け渡し(§3.4) |
DASHBOARD_HOST |
0.0.0.0 |
待ち受けのアドレス |
DASHBOARD_PORT |
8812 |
待ち受けのポート |
BRAIN_AUTH_TOKEN |
(Brainと共通) | ほかのトークンと別の値であることの確認だけに使う(§3.1) |
DASHBOARD_BRAIN_URL |
http://127.0.0.1:<BRAIN_PORT> |
Brainの接続先 |
DASHBOARD_OLLAMA_URL |
http://127.0.0.1:11434 |
Ollamaの接続先(§4.3、§5.2) |
DASHBOARD_BRAIN_UNIT |
keina-brain.service |
起動・停止・状態の取得に使うBrainのunit名。起動時に1度だけ読み、ブラウザからは受け取らない |
DASHBOARD_SESSION_IDLE_HOURS |
12 |
セッションの失効までの時間(§3.2) |
次を満たさなければ、設定の誤り(終了コード2)とする。
DASHBOARD_PORTは1〜65535の整数で、BRAIN_PORTと異なるDASHBOARD_HOSTは空でないDASHBOARD_BRAIN_URLは、スキームがhttp、ホストが127.0.0.1・localhost・::1のいずれか、ポートを持ち、利用者情報(user:pass@)・クエリ・フラグメントを持たず、パスが空か/である(§3.4。トークンをループバックの外へ送らないため)DASHBOARD_OLLAMA_URLは、DASHBOARD_BRAIN_URLと同じ条件を満たす(操作をループバックの外へ送らないため)DASHBOARD_BRAIN_UNITは[A-Za-z0-9@._][A-Za-z0-9@._-]*\.serviceの形(先頭の-を許さない)DASHBOARD_SESSION_IDLE_HOURSは0より大きい有限の数BRAIN_AUTH_TOKEN・BRAIN_VIEW_TOKEN・BRAIN_ADMIN_TOKENのうち、設定されているもの同士がすべて異なる値(§3.1)標準エラー出力(systemd経由ならjournal)に、key=value形式の1行で出す(observation §10と同じ形式・エスケープ)。ロガー名はkeina_assistant.dashboard、本文はdashboard_event event=<出来事>で始める。
| 出来事 | 出すとき | 項目 |
|---|---|---|
dashboard_started |
起動 | host、port、view_login・admin_login(その権限でログインできるか) |
login_succeeded・login_failed |
ログイン | role(成功時)、client(接続元のアドレス) |
brain_start_requested・brain_stop_requested |
Brainの起動・停止を受け付けた | already、service_state |
brain_control_failed |
systemctlが失敗した |
command(start/stop)、error |
admin_relayed |
管理APIを中継した | operation、status(Brainの応答の状態コード)、code |
ollama_unload_requested |
モデルを下ろす要求を受け付けた | model、already |
ollama_unload_failed |
Ollamaへの要求が失敗した | model、error |
GET /api/stateはログに出さない(5秒ごとに出てjournalを埋めるため)。同じ理由で、uvicornのアクセスログは出さず、httpx(Brain・Ollamaへの要求)のログはWARNING以上だけを出すsrc/keina_assistant/dashboard/(新規): __main__.py(起動、uvicorn)、settings.py(設定の読み込みと検証。§11)、app.py(API・認証・中継)、service.py(systemctlの呼び出し。差し替えられる形)、ollama.py(Ollamaの/api/psと下ろす要求。差し替えられる形)、static/page.html・static/app.js・static/app.css(画面。§3.5・§7.1)deploy/systemd/keina-dashboard.service(新規)tools/windows/(新規): §9の3つのPowerShellスクリプト_bearer_matches相当)、ログの形式(format_event)、.envの読み込み(config._read_dotenv)は、Brainの既存の関数を使う。BrainのSettingsは使わず、ダッシュボードの設定は別に持つ(ダッシュボードが、Brainの設定の誤りで起動できなくならないようにするため)systemctlの呼び出しと、Brain・OllamaへのHTTPは差し替えて確かめる。
| 対象 | 確かめること |
|---|---|
| ログイン | 管理・閲覧トークンでそれぞれの権限になる。違うトークン・空のトークン・会話用トークンは401。未設定のトークンではその権限でログインできない。Cookieの属性(HttpOnly・SameSite=Strict)。応答・ログにトークンが出ない。失敗の待ち時間の間も、ほかのセッションの/api/stateが待たされない |
| セッション | Cookieが無い・知らないIDは401。失効時間を過ぎると401。ログアウトで失効。読み直しで延びる |
| 権限 | 閲覧の権限で、起動・停止・中継が403。/api/stateのoperationsがすべてfalse |
| Origin | POST・DELETEで、Originが無い・Hostと違うと403 origin_mismatch。判定の順序(401→403→Origin→422) |
| Brainの状態 | §4.1の対応表の各行。§4.2の組み合わせの各行(runningで応答なし、stopping等)。到達できない間はruntimeがnull。サービスの外で動くBrainの注記。schema_versionが2でないときの扱い |
| 起動・停止 | §5の表の各行(実行する・already・409)。コマンドが固定の引数リストである。systemctlの失敗で500 |
| 中継 | 4本がBrainの対応するパスへ管理トークンで送られる。Brainの応答(201・409・404・503等)がそのまま返る。Brainの401が502 brain_auth_failedになる。到達できないと503 brain_unreachable。パスの形式違いは422でBrainへ送らない |
| 設定 | §11の条件を満たさない値で設定の誤り |
| 画面 | GET /がデータ・トークンを含まない。/docs等が無い。§3.5のヘッダーがすべての応答に付く(/static/以外はno-store)。app.jsがinnerHTML等(§7.1の禁止の一覧)を使っていない(ソースの検査) |
| 文字として表示 | host_label・app_version・detail.messageに<img src=x onerror=alert(1)>・<script>を入れた状態をブラウザなしで描画の関数に通し、要素が作られず文字として入る(描画の関数は、DOMに依存しない「文字列→表示用の値」の部分と、textContentで入れる部分に分け、前者を単体テストする)。実機でも§14.2で確かめる |
| サービスの外のBrain | サービスがstopped・failedでBrainが応答するとき、§4.2のとおりdisplayがoutside_service、status・runtimeがnull、operationsがどちらもfalse。起動・停止が409 brain_outside_service |
| Brainの接続先 | ループバック以外・https・利用者情報・クエリ・フラグメント付きのURLは設定の誤り。Brainがリダイレクト(307)を返したとき、リダイレクト先へ要求を送らない(Authorizationを付けた要求が1回だけ)。環境変数のプロキシを使わない |
| トークンの分離 | 3種類のうち設定されているもの同士が同じ値なら設定の誤り(会話用と閲覧、会話用と管理、閲覧と管理) |
| unit名 | 先頭が-のunit名は設定の誤り。systemctlに渡す引数で、unit名の前に--がある |
| Ollama | /api/psの応答がollama.modelsになる(名前・size_vram・expires_atだけ)。期限を過ぎたものがunloading。到達できない・形式違いでreachable: falseになり、Brainの表示に影響しない。下ろす要求: 載っているモデルだけkeep_alive: 0で送る。載っていなければOllamaへ送らずalready: true。名前の形式違いは422。到達できないと503 ollama_unreachable、Ollamaの失敗で502 ollama_failed。閲覧の権限で403。接続先がループバックのhttpでなければ設定の誤り。リダイレクトを追わない |
| ユースケース | 手順 | 期待結果 |
|---|---|---|
| UC-D01・UC-D04 | Windowsのブラウザでアイコンから開き、閲覧トークンでログインする | Brain・常駐監視対象・未登録のUI・復帰警告・会話の集計・Ollamaのモデルが一画面に出る。会話内容・トークンが出ない |
| UC-D05(段階1) | 同じLANのスマートフォンでhttp://<PCのアドレス>:8812/を開く(ファイアウォールの規則を作った後) |
同じ内容が見られる。ログインしなければ見られない |
| UC-B05 | 管理トークンでログインし、Brainを停止・起動する。.envに誤りを入れて起動する(後で戻す) |
停止処理中→停止中→起動中→稼働中と変わる。停止中も画面は使え、登録一覧は「取得できない」と出る。誤りのときは「起動失敗」と終了コード2の意味・確認先が出る。閲覧の権限ではボタンが出ず、APIも403 |
| UC-D02 | 登録済みの音声UIを止める | 一覧から消えず「未稼働」と出る |
| UC-D03 | テストUIを古いcommitで起動したまま、同じキーでもう1つ起動する | 2つが起動時刻・commitで区別され、重複起動と出る |
| UC-D06 | 画面から、未登録のUIを登録し、解除する。閲覧の権限で操作できない | 登録で常駐監視対象に移り、解除で戻る |
| UC-D07 | Brainを止め、登録情報を壊して、画面から起動する | 復帰警告(発生日時・戻した世代・退避したファイル)が出て、確認済みにすると消える |
| UC-R08 | 版2の音声UI・版1のテストUIに、画面から終了を求める | 版2は終了して一覧から消える。版1は「伝わりません」と出て、求めた時刻とともに稼働中のまま残る |
| アイコン | 「ダッシュボードを止める」→「ダッシュボード」。WSLをwsl --shutdownで止めた状態から「ダッシュボード」 |
止まり、また起動してブラウザが開く。WSLごと起動して開く |
| 常駐 | .wslconfigのinstanceIdleTimeout=-1とloginctl enable-lingerを済ませ、wsl --shutdownの後にアイコンで起動する。WSLの端末・VS Code・ブラウザをすべて閉じ、既定のアイドル時間(15秒)を十分に超える時間(5分以上)待ってから、Windowsのブラウザとスマートフォンから新しく/healthzと画面を開く |
ダッシュボードが応答する(WSLもダッシュボードも止まっていない) |
| 文字として表示 | テストUIを--host-label '<img src=x onerror=alert(1)>'で起動し、管理でログインした画面を見る |
文字のまま表示され、スクリプトが動かない |
| サービスの外のBrain | サービスを止め、前景でBrainを起動して画面を見る(runbook §1.3) | 「サービスの外でBrainが動いています」と出て、起動・停止のボタンが使えない |
| UC-D08 | 管理でログインし、Brainに質問してモデルを載せてから、Ollamaの区画で[GPUから下ろす]を押す。続けてBrainに質問する。閲覧でログインした画面も見る | 区画からモデルが消え、nvidia-smiでVRAMの使用量が減る。次の質問は読み込みの分だけ遅れて答え、モデルがまた区画に出る。閲覧の権限ではボタンが出ない |
runtime-management-runbook.md)に加えること%USERPROFILE%\.wslconfigの[general]にinstanceIdleTimeout=-1を書き、wsl --shutdownで効かせる手順(Brain・Ollama・音声UIの会話が止まることを先に知らせる)。loginctl enable-linger。#21もこの手順を前提にするlink、アイコンの作成(install-dashboard-shortcuts.ps1)systemctl --user status keina-dashboard)journalctl --user -u keina-brain)curl http://127.0.0.1:11434/api/generate -d '{"model":"<モデル名>","keep_alive":0}'curlの手順は、ダッシュボードが使えないときの手段として残す.envの値を変え、Brainとダッシュボードの両方を再起動する(ダッシュボードの再起動で、既存のセッションはすべて失効する)。閲覧・管理の権限の人には新しい値を手で渡す.envのDASHBOARD_PORT、ショートカット(-Portで作り直す)、ファイアウォールの規則の3か所を同じ値にする(§9)| 文書 | 反映すること | 時期 |
|---|---|---|
runtime-management-use-cases.md |
UC-D08(OllamaのモデルをGPUから下ろす)を追加し、§1.1・§2・§6・原則5に、その操作を加える | 本書の合意時(2026-10-05に反映済み) |
runtime-management-design.md |
§12.2の「Brainの起動・停止を続けて押した場合の扱い」「ダッシュボードの実現方式」を、本書で決めたと注記する。FR-24を加え、§1.3・NFR-03・§4.4・§9.3・§11に、Ollamaのモデルを下ろす操作を加える。§14に経緯を加える | 本書の合意時(2026-10-05に反映済み) |
runtime-management-implementation-plan.md |
§5.1の#17に、デスクトップのアイコン(ダッシュボード自身の起動・停止)と、常駐の前提(.wslconfigのinstanceIdleTimeout=-1、loginctl enable-linger)の手順と、Ollamaのモデルの表示と下ろす操作を加える。§4にUC-D08を加える。#21に、その前提を使うことを加える。§8に経緯を加える |
本書の合意時(2026-10-05に反映済み) |
runtime-management-runbook.md |
§15 | 実装時 |
| Issue #17 | 題名・本文を、Brainとは別のWebサーバーとしての範囲に書き直す(論点J) | 本書の作成時 |
なし
運用者が決めた論点A〜J(§20)に無く、本書の草案で決めた点である。
Max-Ageを持たない(ブラウザを閉じると消える)Cookieとし、最後に使ってから12時間で失効させる(§3.2)401を502 brain_auth_failedに置き換える(§6)DASHBOARD_BRAIN_UNITを設定で変えられるようにする(テストと、unit名を変えた場合のため。ブラウザからは変えられない)(§11)Settingsを使わず、設定を別に持つ(§13)loginctl enable-lingerを、ダッシュボードの常駐の前提の1つとする(§2.3。CODEXのレビューで、WSL自体を止めない設定(instanceIdleTimeout=-1)も要ると指摘され、運用者がそれを決めた)instanceIdleTimeout=-1とenable-lingerの後、WSLの端末等をすべて閉じても、WSLとダッシュボードが動き続けること(§14.2の「常駐」)wsl.exe -- systemctl --user …が、ほかにWSLのセッションが無い状態でも動くこと0.0.0.0:8812に届くこと| 日付 | 決めたこと | 理由 | 却下・置き換えた案 | 根拠 |
|---|---|---|---|---|
| 2026-09-30 | ダッシュボードは常駐させる(systemd --userのkeina-dashboard.service、ポート8812、既定の待ち受け0.0.0.0)。起動・停止の手段は、自動起動(有効化は#21)と、Windowsのデスクトップのアイコン2つ(起動して開く/止める)とする |
別のPC・スマートフォンから見るときに、先にPCで起動する必要が無いようにするため。アイコンは、運用者が手で起動・停止できる手段として加えた(運用者の判断) | 必要なときだけアイコンで起動する案(別の端末から見る前に、PCでの操作が要る)。ソケット起動(開いたときだけ動く)案 | #17 #6885 |
| 2026-09-30 | 画面はHTML 1枚と5秒ごとの読み直し。ブラウザの認証はトークンでログインし、HttpOnly・SameSite=StrictのCookieのセッションとOriginの検証で守る。Brainへは、ダッシュボードが持つトークンを権限に合わせて付ける。Brainの状態は実行環境を正とし、状態取得で補う。起動・停止はsystemctl --user --no-blockの固定のコマンドで行い、既に目的の状態なら何もせず成功、途中なら今の状態を添えて409。管理APIはそのまま中継する。LANへはWindowsファイアウォールの規則で開く |
運用者の判断(論点B〜J) | — | #17 #6885 |
| 2026-09-30 | 常駐の前提に、WSL自体を止めない設定(Windowsの.wslconfigの[general]にinstanceIdleTimeout=-1)を加えた(§2.3) |
loginctl enable-lingerは、動いているWSLの中でユーザーのサービスを残すだけで、WSLのインスタンスは使う人がいなくなると既定15秒で止まる(CODEXの指摘、Microsoftの説明)。止まると、別の端末から開けない。設定1行で済む公式の方式を選んだ(運用者の判断) |
Windowsのログイン時にwsl.exe -- sleep infinityを見えない形で動かし続ける方式(仕組みが1つ増え、それが止まると気づけない)。enable-lingerだけで常駐するとした草案の記述(本行で置き換えた) |
#17 #6893 |
| 2026-09-30 | CODEXのレビューのほかの指摘5件と任意の提案7件を反映した。ダッシュボード側でも3種類のトークンがすべて異なることを検証する(§3.1)。動的な文字列はtextContentで入れ、CSP等のヘッダーを付け、スクリプトを別ファイルにする(§3.5、§7.1)。サービスの外で動くBrainの状態・表示・操作を決め、起動・停止は409 brain_outside_serviceとする(§4.2、§5)。ポートをショートカットとファイアウォールの規則に同じ値で渡す(§9、§10)。Brainの接続先をループバックのhttpに限り、リダイレクトを追わず、プロキシを使わない(§3.4、§11)。ログインの失敗の待ちはイベントループを止めない、読み直しを重ねない、Restart=on-failure、unit名の前に--、serviceは操作前の状態と明記、トークンのローテーションとポートの変更の手順 |
会話用トークンを持つUIがダッシュボードにログインできる、UIの申告値で管理の画面のスクリプトを乗っ取れる、サービスの外のBrainで状態・操作が決まらない、非既定のポートでアイコンが失敗する、設定の誤りでトークンが外へ出る、の余地を閉じる | 草案の各記述(本行で置き換えた) | #17 #6893 |
| 2026-10-05 | ダッシュボードに、OllamaがGPUに載せているモデルの表示と、管理の権限でモデルをGPUから下ろす操作を加えた(§1.2、§4.3、§5.2、§7、§8、§11〜§16)。ユースケース文書にUC-D08を加えた。Brainの処理中でも断らない | OLLAMA_KEEP_ALIVE=24hのため、gemma4:12bが最後の質問から24時間GPUに残る。WSLを止めずにGPUを空ける手段を、画面から使えるようにする(運用者の判断)。処理中の生成は、下ろす要求の後も最後まで終わることを実機で確かめた |
必要なときに手でcurlを送る案。OLLAMA_KEEP_ALIVEを短くする案(使わなかった後の最初の質問が遅くなる)。別のIssueに分ける案。Brainの処理中の件数が0でなければ409で断る案(実機の確認で不要と分かった) |
#17 #6942 |
| 2026-10-05 | 常駐の前提のinstanceIdleTimeout=-1を運用者が改めて確認し、本書に合意した(ステータスを合意済みにした)。§16の他の文書への反映を行った |
運用者の判断 | — | #17 #6942 |
| 2026-10-05 | 実装(工程5)で、設定の読み込みと検証をsettings.pyに分けた(§13)。uvicornのアクセスログを出さず、httpxのログをWARNING以上に絞った(§12) |
設定を単体テストしやすくするため。実機で、httpxが要求ごとにINFOのログを出し、画面を開いている間5秒ごとに2行ずつjournalにたまることが分かった(§12の「GET /api/stateはログに出さない」の趣旨に反する) |
— | #17 #6945 |