# 管理画面（F-10 システム管理）設計検討 — 2026-09-10

**位置づけ**: 最終フェーズで作る管理画面の**設計検討メモ**。指示書ではない。実装はしていない。
**前提のコミット**: `develop` / `8381d3c`（作業ツリーはクリーン、41ファイル 778テスト全パス、最終決定番号 **D-101**）
**調査方法**: 5本の調査エージェントの報告を、主要な主張だけ実コードで再検証したうえで統合した。

> ★**出所についての注記**★（2026-09-16 追記）
>
> 本メモは 2026-09-10 のセッション（`8381d3c` で指示書I / D-101 を記録した回）自身の成果物です。
> その後の作業が繰り返し「★他作業の未コミット変更★」として回避してきましたが
> （`docs/decisions.md:8961` / `:9142` ほか）、★別の担当者の作業ではありません★。
> 同じ未追跡集合にあった `README.md` と `docs/code-map.md` は既にコミット済みで、
> ★取り残されていたのは本ファイルだけ★でした。
>
> ★中身は当時のまま★です。前提コミット（`8381d3c`）・テスト数（41ファイル 778テスト）・
> 最終決定番号（D-101）は ★2026-09-16 時点では古い★（実測 96ファイル 2141テスト / D-141）。
> ★設計検討の結論そのものは未着手のため生きています★が、実測値は読み替えてください。

---

## 0. スコープ（2026-09-10 ユーザー確定）

ユーザー選択は **「F-10 の編集可能化 ＋ ユーザー管理系」**。

★これは `docs/spec.md` §2.3 および **D-032** の P1 スコープを超える★。両者は
「ユーザー管理 / 権限管理 / 監査ログ は**スコープ外**（ユーザー管理未実装のため。P2以降）」と
明記しているため、**着手時に決定事項として上書きを記録する必要がある**（§8 の T-0）。

| レイヤー | 中身 | spec.md の扱い | 本メモの章 |
|---|---|---|---|
| **L1 設定** | 表示設定タブ（新規）＋ 運用設定の編集可能化 | P1（実装する） | §3 |
| **L2 認証・認可** | ユーザー管理・権限管理 | ★P2以降 → 今回スコープに繰り上げ★ | §4 |
| **L3 監査** | 監査ログ | ★P2以降 → 今回スコープに繰り上げ★ | §5 |

★依存の向きは L2 → L3 → L1★。「誰が変えたか」を記録できるのは L2 が入ったあとなので、
**L2 を先に作るほうが手戻りが少ない**（§6 の段階分割）。

---

## 1. 現状の実測（推測なし）

### 1.1 画面

| 事実 | 根拠 |
|---|---|
| `app/(dashboard)/system/page.tsx` は `SystemScreen` を返すだけの5行 | 実測 |
| `SystemScreen.tsx` は **253行・運用設定のみ・完全な読み取り専用**。★タブUI自体が無い1画面ベタ構成★ | `components/features/system/SystemScreen.tsx` |
| 画面冒頭に方針が明記されている: ★「動かないトグル・入力欄・保存ボタンは置きません（触れるのに保存できないUIは事故のもと）」★ | `SystemScreen.tsx:5-9` |
| アドバイス文言だけは `GET /api/settings/advice` 経由で取得済み。**閾値3つは `lib/config` を直接 import** という非対称がある | `SystemScreen.tsx:20-22, 29-33` |
| `PlaceholderScreen` のまま残っているのは **`/inspections` の1枚だけ**（facilities / schedules / system は実装済み） | `app/(dashboard)/` 実測 |
| `lib/i18n.ts` に「表示設定」「ユーザー管理」「監査ログ」の訳語は**無い** | 実測（grep 0件） |

### 1.2 設定値の保持

| 事実 | 根拠 |
|---|---|
| 設定の保持場所は3系統のみ: `lib/config/` のコード定数（大多数）/ `data/settings/*.json`（アドバイス文言・D-S備考の2つだけ）/ 環境変数（6個） | `lib/config/`, `lib/settings/` |
| ★`NEXT_PUBLIC_*` は1件も無い★ | 実測 |
| ★`GRADE_THRESHOLDS` 等は `export const ... as const` の**定数**★ | `lib/config/index.ts:11-25` |
| ★`judgeGrade(rate)` は閾値を **import で直接参照**しており、差し込む引数が無い★ | `lib/judge/grade.ts:1, 23-27` |
| ★`"use client"` のコンポーネント **11ファイル**が `@/lib/config` を直接 import している★ | 実測（`components/features/*/`） |
| `data/` は `.gitignore` 済み（コミット事故なし） | `.gitignore` |

### 1.3 永続化基盤 `lib/settings/json-store.ts`（D-075 で共通化済み）

- **原子性: あり**（`.tmp` → `fs.rename`、失敗時は `.tmp` 掃除＋元エラー再スロー）
- **排他: なし。後勝ち**（コード上に明示。「利用者が増えたらロックを入れる」）
- ファイル未存在（ENOENT）は静かに空を返す。**ENOENT 以外は投げる**
- 破損時はタイムスタンプ付き `.bak` へ退避。退避失敗時は例外
- ★**値の型検証は無い**★ — トップレベルがオブジェクトかしか見ない

### 1.4 認証（D-101 / 指示書I で実装済み）

| 事実 | 根拠 |
|---|---|
| `proxy.ts`（Next 16 で `middleware.ts` から改名）で 相関ID発行 → IP制限 → cookie 認証 | `proxy.ts` |
| cookie: `httpOnly: true` / `secure: true` / ★`sameSite: "lax"`★ / `path: "/"` / ★**期限なし（セッションcookie）**★ | `lib/auth/cookie.ts:29-34` |
| ★cookie の値は共有トークン `AUTH_TOKEN` そのもの。**誰が使ってもバイト列が同一**★ | `app/api/auth/login/route.ts` |
| 認可（authorization）の層は**存在しない**。`AuthDecision` は `allow` / `deny_ip` / `unauthenticated` の3値 | `lib/proxy/authenticate.ts:27-33` |
| ★`lib/config/allowed-ips.ts` の `ALLOW_ALL_WHEN_EMPTY` が既定 `true` のまま。本番前に `false` 必須★ | D-101 項6-3 / 項8 |
| 依存は `next` / `react` / `@prisma/*` / `pino` の4系統のみ。★bcrypt も SQLite も入っていない★ | `package.json` |

### 1.5 既存の書き込み系 API の作法

- 書き込みは **JSON body の `PATCH`** のみ（`/api/settings/advice`、`/api/ds-devices/{serial}/note`、対応状況の更新）
- ★**成功時のログが1行も出ていない**★。`api.error`（失敗時）しか記録していない。
  `db.write` イベント型は定義済みだが実コードからの呼び出しは0件

---

## 2. モックが定める「正」（`d-watch-mock.jsx`）

### 2.1 タブ構成（`:1156-1164`）

9件並んでいるが id は8種類。★**`ops`（運用設定）が2回入っているバグ**★があり、
そのまま移植すると React の duplicate key になり、かつ `sysTab === "ops"` で
**2つの本文（`:1221` と `:1628`）が同時に描画される**。

| id | ラベル | 本文 | 今回の扱い |
|---|---|---|---|
| `display` | 表示設定 | `:1182-1219` | **L1で作る**（初期タブ） |
| `ops` | 運用設定 | `:1221`（ops#1） / `:1628`（ops#2） | **L1で編集可能化**。★ops#2 を採用（既存実装と同じ）★ |
| `notify` | 通知設定 | `:1303-1340` | **作らない**（D-032。D-Security側画面を流用） |
| `users` | ユーザー管理 | `:1342-1396` | **L2で作る** |
| `roles` | 権限管理 | `:1398-1504` | **L2で作る**（★下記のとおり参照のみで足りる★） |
| `customer` | 顧客管理 | `:1506-1571` | **作らない**（D-032） |
| `group` | グループ管理 | `:1572-1627` | **作らない**（D-032） |
| `auditlog` | 監査ログ | `:1699-1734` | **L3で作る** |

### 2.2 表示設定タブ（`:1187-1191`）

トグルは **4つ、すべて既定 ON**。保存ボタンは無く、トグル即時反映。

| key | ラベル | 説明 |
|---|---|---|
| `secBanner` | D-Securityアラートバナー | セキュリティ無効施設の赤色警告バナー |
| `detailLog` | 清掃詳細ログ（コンパクト版） | △×の清掃詳細ログテーブル |
| `scheduleBar` | スケジュール進捗 | 完了数/予定数のプログレスバー |
| `failBreakdown` | 失敗の内訳 | ロボット異常/未実施/セキュリティ有効/制御盤異常の件数 |

★**数値カード4列がトグルに入っていない**★。`docs/spec.md:35`「各ブロックはON/OFF可能」と食い違う（§8 の Q5）。

### 2.3 権限管理タブ（`:1398-1504`）— ★編集UIが無い★

ロール定義表（4行）とメニュー×ロールのアクセスマトリクス（18行）が**静的に描かれているだけ**で、
追加・編集・保存のボタンは1つも無い。★つまり「権限管理」は**ロール定義の参照画面**であって、
権限をデータとして編集する機能ではない★。これはスコープを大きく縮める。

**ロール定義（モックが正）**

| ロール | 実装フェーズ | 紐づき | データスコープ | 説明 |
|---|---|---|---|---|
| DFAシステム管理者 | **P1** | ー | 全顧客・全施設 | 全権限。権限管理の編集はこのロールのみ |
| DFA運用担当 | **P1** | ー | 全顧客・全施設 | 全メニュー閲覧＋施設ロボットマスタ編集。権限管理は閲覧のみ |
| 顧客管理者 | P2 | 会社 | 自社施設のみ | 自社の閲覧全般 |
| 施設運用者・清掃業者 | P2 | 施設 | 自施設のみ | 設定メニュー以外の自施設データを閲覧 |

→ ★**今回作るのは上2つだけ**★。下2つはロール定義表に「P2」バッジ付きで表示するに留める
（モックがそう描いている）。**P1 は DFA 社内のみ**という `CLAUDE.md` §1 の前提とも一致する。

**アクセスマトリクスの要点（P1の2ロール分）**

| メニュー | DFA管理者 | DFA運用 |
|---|---|---|
| ダッシュボード / 清掃詳細ログ / アラート / レポート / D-Secログ / D-Secアラート / スケジュール | 閲覧 | 閲覧 |
| 施設ロボットマスタ | 閲覧/編集/登録 | 閲覧/編集/登録 |
| 表示設定 | 閲覧/編集 | 閲覧/編集 |
| 運用設定 | 閲覧/編集 | ★閲覧のみ★ |
| ユーザー管理 / 権限管理 | 閲覧/編集/登録 | ★閲覧のみ★ |
| 監査ログ | 閲覧 | 閲覧 |

★**矛盾**★: マトリクスは施設ロボットマスタを「編集/登録」可としているが、
`docs/spec.md` §2.3 F-08 は★「登録・更新はD-Security側の画面で行うためD-WatchはGETのみ」★と確定している。
**spec.md を正とし、マトリクスの当該セルは「閲覧」に読み替える**（§8 の Q6 で確認）。

★**実装上の意味**★: P1 の2ロールの差は**運用設定・ユーザー管理・権限管理の3箇所だけ**。
それ以外は同一。つまり必要な認可判定は「**管理者かどうか**」の1ビットで足りる。

### 2.4 監査ログタブ（`:1699-1734`）

4列の読み取り専用テーブル: **日時 / 操作者 / アクション / 対象**。フィルタ・ページングは無い。

★モックのサンプル行は「清掃開始（手動）」「スケジュール変更」「ロボット同期」「施設追加」で、
**いずれも D-Watch には無い操作**（D-Security 側 / Pudu Cloud 側の操作）★。
そのまま作ると空のテーブルになる。**D-Watch が実際に記録できる操作に置き換える必要がある**（§5.1）。

### 2.5 モックと確定仕様の食い違い（実装前に潰すもの）

| # | モック | 正 | 対処 |
|---|---|---|---|
| 1 | 「◎基準％**以上**」 | `CLAUDE.md` §4.1 は「**超**」 | ★CLAUDE.md 優先★（既存 `SystemScreen.tsx:16-18` で対処済み） |
| 2 | × は「上記未満」のみ | `CLAUDE.md` §4.1 は「10%未満、**または未実施**」 | 文言に「または未実施」を足す（既存実装で対処済み） |
| 3 | `ops` タブが2回定義 | — | 1つに直す。本文は `:1628` 側 |
| 4 | 入力欄の min/max（10-120 等） | ★どの資料にも根拠が無い★ | §3.5 のバリデーション設計で決め直す |
| 5 | 顧客管理が**会社単位**で契約モジュールを持つ | `facilities.contract`（★施設単位・varchar(10)★。D-080） | 顧客管理は作らないので今回は影響なし。P2 の地雷として記録 |
| 6 | アクセスマトリクスの施設ロボットマスタ編集可 | spec.md §2.3 F-08 は GET のみ | §2.3 のとおり読み替え |

---

## 3. L1 — 表示設定・運用設定の編集可能化

### 3.1 保存先: ★JSON ファイル継続（`lib/settings/json-store.ts` を包む）を推奨★

| 案 | 評価 |
|---|---|
| ★**A: JSON 継続**★ | ★**推奨**★。`advice.json` / `ds-device-notes.json` の実績あり。原子性・破損退避・テスト差し替えが実装済み。さくらクラウド VM **単一ホスト**前提でスケールアウトしないため、ファイルで十分 |
| B: 点検記録用 SQLite に相乗り | ★M6（点検記録）が未着手で依存が生まれる★。`CLAUDE.md` §6-4「TeamWorks待ち/未完成のものでブロックされない」に反する。★ただし L3 監査ログには SQLite が向く（§5.2）ので、L3 で導入するなら L1 も後から寄せられる★ |
| C: 環境変数 | 「画面から編集」を満たさない。★ただし「閾値は編集させない」判断（§8 Q1-b）を採るならこれが答えになる★ |
| D: メインDB | ★禁止★（`CLAUDE.md` §3・§7。書き込み可は4カラムのみ） |

**保存形式は overrides 方式**（既定値との差分だけを保存）。`advice.ts` の前例を踏襲すると
「既定値へ戻す」がファイル削除／キー削除で表現でき、既定値の変更が自動で追従する。

### 3.2 ★最大の実装論点: 閾値を判定ロジックへ届ける方法★

現状 `judgeGrade()` は `GRADE_THRESHOLDS` をモジュール定数として import している。
JSON で上書きした値を届けるには**関数シグネチャの変更が必要**で、`lib/judge/` 全体と全呼び出し元に波及する。

- ★**採用**: 純粋関数のまま、閾値を**既定引数付きの引数**で受ける★
  （`judgeGrade(rate, thresholds = GRADE_THRESHOLDS)`）。既存テストは無変更で通り、
  `CLAUDE.md` §9.4「判定関数は純粋関数にする」と整合する。
- ★**採ってはいけない**: モジュール変数を実行時に書き換える方式★。
  Next.js のワーカー構成が**不明**で、プロセスごとに違う値を持つ／リクエスト間で不整合になる。
- 反映タイミング: **プロセス再起動なしで効く**（リクエストごとに JSON を読む）。
  100バイト未満のファイルなので★初版はキャッシュしない★。入れるなら mtime 方式
  （PATCH でのキャッシュ破棄は複数プロセスだと壊れる）。

### 3.3 ★クライアントが `lib/config` を直接 import している問題★

`"use client"` の11ファイルが閾値をビルド時定数として読んでいる。サーバ側だけ動的化すると
**判定と画面の説明文が静かに食い違う**。特に深刻な3箇所:

| 箇所 | 症状 |
|---|---|
| ★`SystemScreen.tsx:65-77`★ | ★**管理画面自身が固定値を表示する**★。85 に変えても「90%超」と出続ける |
| `FacilityPerformanceTable.tsx:76-77` vs `lib/judge/kpi.ts:79-80` | 同じ行で「ステータス＝良好／セル＝警告色」の矛盾が復活する（この矛盾を避けるために閾値を統一した経緯がコメントに残っている） |
| `SUCCESS_RATE_TARGET` / `FACILITY_TABLE_THRESHOLDS` | ★クライアント専用なので動的化しても一切反映されない★ |

→ ★**対処**: 動的化する閾値は、画面には必ず API レスポンス経由で渡す★。
`SystemScreen.tsx:20-22` がアドバイス文言についてすでに採っている流儀を、閾値にも広げるだけ。

### 3.4 ★「変えても何も起きない設定」を画面に出してはいけない★

| 設定 | 実態 |
|---|---|
| `LAUNCH_GRACE_MINUTES`（猶予30分） | ★**消費者ゼロ**★。`lib/db/index.ts:275` に「LAUNCH_GRACE_MINUTES はここから読まない」と明記（D-091 項1 / 指示書R 禁止事項3） |
| `PRECHECK_MINUTES_BEFORE`（60/30分前） | 消費者は `SystemScreen.tsx:131`（表示のみ）と `scripts/seed-local-db.ts`（シード生成）。★事前チェックを実際に行うのは TeamWorks のデータ収集処理★で、画面から 60→45 にしても実際のチェック時刻は変わらない |
| ↑ さらに危険 | `CLEAN_TIMETABLE_PRECHECK.rounds = [1, 2]` が `PRECHECK_MINUTES_BEFORE` と**添字で対応**している。要素数を増やすと事前チェック列がずれる／消える |

→ ★モック `:1637-1646` の「事前チェック設定」入力欄を**そのまま実装してはいけない**★。
`SystemScreen.tsx:5-9` が自ら掲げた方針に真正面から反する。
★**保存できるが何も変わらない UI は、保存できない UI より悪い**★。

★**帰結: 実際に機能する編集対象は「完了率閾値3つ ＋ エスカレーション日数2つ ＝ 5項目」と
アドバイス文言（既に編集API あり）と表示トグル4つだけ**★。

### 3.5 バリデーション

- ★**部分更新は「現在値へ重ねた結果の全体」を検証する**★（単体では妥当でも、重ねると破綻する）
- `excellent > good > poor` と `level3Days > level2Days` は**等値も拒否**
  （等値にすると ○ や L2 が例外もテスト失敗も出さずに消える）
- 範囲は 0〜100（整数か小数かは §8 Q7）
- `error.code` は既存の `validation_failed` を使い、新設しない
- ★ストア側でも再検証する★（ファイルを直接壊された場合。`json-store.ts` は型を見ない）

### 3.6 API 契約

★`d-watch-api.yaml` には表示設定・運用設定のエンドポイントが**1本も無い**★。
`CLAUDE.md` §6-1 により**実装より先に YAML を直す**。

- 命名案: `/settings/thresholds` ＋ `/settings/dashboard`
  （`/settings/operations` は既存の `/settings/advice` と守備範囲が重なるため回避）。§8 Q8 で確認
- 一覧ではないので `meta` は不要
- リセットは既存 `/settings/advice` の `reset` フラグの作法を踏襲。**項目単位のリセットは作らない**

### 3.7 表示設定の解決場所: ★サーバ保存・レスポンス同梱を推奨★

- `DashboardScreen.tsx:93` は `!loading && !error && data` で**本体を一括描画**するため、
  設定を `/api/dashboard` のレスポンスに同梱すれば★ちらつき（OFFのブロックが一瞬見えて消える）は構造的に起きない★
- ★ブラウザ保存は不可★: リポジトリ全体で `localStorage` の使用は**0件**、`ThemeProvider.tsx:25-26` も
  `useState` 固定（リロードで dark/ja に戻る）。「テーマと同じ場所」という選択肢は**存在しない**
- ★ユーザーごとの設定にはしない★: L2 でユーザーが入っても、表示設定は
  アクセスマトリクス上 全ロール「閲覧/編集」＝ **全員共通の設定**。個人設定にするとマトリクスと食い違う
- レイアウト崩れの危険2箇所:
  - `DashboardScreen.module.css:34` の `grid-template-columns: repeat(4, 1fr)` **固定**（カード個別OFFなら空き枠）
  - `DashboardScreen.tsx:149-153` の下段グリッドが `failure_breakdown ? "1fr 1fr" : "1fr"` の**三項演算子**。
    子2つを独立ON/OFFすると4通りになり表現できない（両方OFFで空 div が残る）
- ★「失敗の内訳」は D-048 で API がプロパティごと省略しており、**ONにしても出ない**★。
  D-053「動かないUIを出さない」に抵触するため、トグルを出す前に API 側の整理が要る
- 契約フラグとの二重制御は**現状発生しない**: `AppShell.tsx:8-29` は静的配列で `contract` を参照していない
  （`docs/handoff.md:1797-1799`「施設横断のグローバルUIでどの施設の契約か決まらないため P1 は全表示」）。
  将来の原則は ★契約 > 未実装 > 表示設定 の AND 合成★

### 3.8 ★最重要の論点: 閾値変更で過去の判定が遡って変わる★

◎○△× も L1〜L3 も **DB に保存されていない**（`lib/db/index.ts:307-309`。
サマリテーブルにも grade カラムは無い ＝ `prisma/schema.prisma:701-732`）。毎回その場で計算される。

→ ★**閾値を変えた瞬間、過去すべての清掃ログの判定が遡って書き換わる**★。

**具体例: ○の基準を 70 → 75 に上げる**

| 施設 | 日付 | 完了率（生値） | 変更前 | 変更後 |
|---|---|---|---|---|
| A店 | 09-01(月) | 72.4% | ○ | ★△★ |
| A店 | 09-02(火) | 71.0% | ○ | ★△★ |
| A店 | 09-03(水) | 73.8% | ○ | ★△★ |

変更前は3日とも ○ ＝ 問題なし・連続0。変更後は3日とも △ ＝ 連続3日 → ★L3「DFAオンサイト訪問を検討」が発火★。
レポートの「連続問題施設」に A店 が突然出現する。
★**9月1日の朝に「異常なし」と確認して閉じた日が、今日見ると「L3」に見える**★。

**TeamWorks のサマリとは食い違わない**（先方は判定を持たず、件数・面積・率だけ）。
食い違うのは ★DFA 内部の時間軸★ ＝「確定した過去の期間の判定が、後から変わる」。

---

## 4. L2 — ユーザー管理・権限管理

★ここが今回のスコープ拡張の本体で、**D-101 で作ったばかりの認証を作り替える**ことになる★。

### 4.1 いま何を作り替えるのか

| 現状 | ユーザー管理を入れた後 |
|---|---|
| 共有トークン `AUTH_TOKEN` 1本 | ユーザーごとのアカウント（メール＋パスワード） |
| cookie の値 ＝ トークンそのもの（全員同一） | cookie の値 ＝ セッション識別子（ユーザーごとに異なる） |
| 認可の層が無い（`allow` / `deny_ip` / `unauthenticated` の3値） | ロール（管理者 / 運用担当）による認可判定を追加 |
| ログに「誰が」が原理的に載らない | ログに `actor` が載る ＝ ★L3 監査ログが初めて成立する★ |

### 4.2 ユーザーストアの置き場所

★メインDBには置けない★（`CLAUDE.md` §3・§7）。選択肢は3つ。

| 案 | 評価 |
|---|---|
| A: JSON（`data/users.json`） | ★実装最小。既存 `json-store.ts` をそのまま使える★。数名規模なら十分。★弱点: 排他が無い（後勝ち）ので、2人が同時にユーザーを追加すると片方が消える★。監査ログ（追記が多い）には不向き |
| ★**B: SQLite**★ | ★**推奨**★。ユーザー・セッション・監査ログを1ファイルで扱え、★追記と検索に強い★。**F-05 点検記録が使う予定のストアと同じ技術**なので、ここで導入すれば M6 と基盤を共有できる。★弱点: 依存が1つ増える（`package.json` に SQLite は未導入）★。ネイティブビルドを避けるなら `node:sqlite`（Node 22+ 同梱）が候補だが、★本番 VM の Node バージョンが**不明**★（§9） |
| C: メインDB | ★禁止★ |

★**推奨は B。ただし「本番 VM の Node バージョン」を確認してから決める**★。
確認が取れないうちは A で始め、L3（監査ログ）着手時に B へ寄せる逃げ道がある。

### 4.3 パスワードの扱い

- ★**新しい依存を足さずに済む**★: `node:crypto` の `scrypt` が使える（bcrypt/argon2 のネイティブビルド不要）。
  さくらクラウド VM でのビルド事故を避けられる
- 保存するのは `scrypt` ハッシュ＋ソルト＋パラメータ。★平文・可逆暗号は不可★
- 検証は `timingSafeEqual`（既存 `lib/auth/token.ts` が定数時間比較を採っている流儀に揃える）
- ★パスワードは `lib/logger/redact.ts` の秘匿対象に必ず追加する★（`dwatch_session` を足した前例あり）
- 初期管理者は**環境変数から1人だけ作る**（ブートストラップ）。画面から作れると誰でも管理者になれる

### 4.4 セッション

現状は cookie 値＝トークンなのでサーバ側にセッションの実体が無い。ユーザーが入ると
「**ユーザーを削除／無効化したら、その人のセッションを切る**」が要件になるため、
★サーバ側セッション（ストアに `session_id → user_id, created_at`）を持つのが素直★。

★**ここで `CLAUDE.md` §7 の解釈を確認する必要がある**★:

> ❌ トークン期限/残日数の実装

この禁止の出所は **D-030**で、内容は★「**D-Secログ・D-Secアラート**（F-06/F-07）の
D-Sデバイスのトークン有効期限表示・残日数列を実装しない」★という画面要件の話である。
**ログインセッションの寿命の話ではない**。
D-101 はこれを cookie の `maxAge` にも適用して★期限なし（ブラウザを閉じるまで有効）★としたが、
実ユーザーのアカウントを入れると**セッション寿命はセキュリティ統制そのもの**になる
（席を離れた PC から設定を変えられる、が現に穴として残っている）。
→ ★§8 Q3 で確認する論点★。

### 4.5 認可の実装箇所

P1 の2ロールの差は**運用設定・ユーザー管理・権限管理の3箇所だけ**（§2.3）なので、
★必要なのは「管理者かどうか」の1ビット★。過剰な RBAC を作らない。

| 層 | 何をするか |
|---|---|
| `proxy.ts` | セッション → ユーザー解決までを行い、`allow` に `user_id` / `role` を載せる。★画面の出し分けは proxy でやらない★（画面は全部見せてよい。差は編集可否だけ） |
| API ルート | ★**認可の判定はここが正**★。管理者限定の PATCH は 403 を返す。UI だけで隠さない |
| 画面 | 権限が無いロールには入力欄を `disabled` にし、理由を表示する。★「押せるのに 403」は事故のもと★ |

★`d-watch-api.yaml` に 403 のレスポンス定義を追加する必要がある★（現状は 401 系のみ）。

### 4.6 権限管理タブは「参照のみ」で作る

§2.3 のとおりモックに編集UIが無い。★ロール定義とアクセスマトリクスを**静的な表として描くだけ**★。
ただし ★**画面の表と実際の認可コードが乖離すると嘘の表になる**★ので、
表のデータは `lib/config/roles.ts` のような**1箇所の定義から生成し、認可判定も同じ定義を読む**こと。

---

## 5. L3 — 監査ログ

### 5.1 何を記録するか（★D-Watch が実際にできる操作だけ★）

モックのサンプル行（清掃開始・スケジュール変更・ロボット同期・施設追加）は
**すべて D-Watch のスコープ外**なので採用しない。実際に記録できるのは次のとおり。

| アクション | 対象 | 現状の実装 |
|---|---|---|
| ログイン成功 / 失敗 / ログアウト | ユーザー | ★ログイベントは D-101 で実装済み（`actor` だけが無い）★ |
| 事前チェックの対応状況を更新 | `robot_error_reports` | API 実装済み。★成功ログが無い★ |
| D-Secアラートの対応状況を更新 | `control_error_histories` | API 実装済み。★成功ログが無い★ |
| D-Sデバイスの備考を編集 | `data/settings/ds-device-notes.json` | API 実装済み（D-076）。★成功ログが無い★ |
| アドバイス文言を編集 / リセット | `data/settings/advice.json` | API 実装済み。★成功ログが無い★ |
| ★閾値・表示設定を変更★ | `data/settings/*.json` | **L1 で新設** |
| ユーザーの追加 / 編集 / 削除 | ユーザーストア | **L2 で新設** |
| 点検記録の登録 / 削除 | 独自ストア | F-05 未実装（M6） |

★**先に直すべき前提**★: 現状 **成功時のログが1行も出ていない**。
監査ログは「成功した操作の記録」なので、**まず書き込み系 API に成功ログを入れる**のが土台になる。
`docs/logging-design.md:68` が★「P2スコープ外の監査ログ（説明責任トレイル）」と
「デバッグ用技術ログ」を別物と裁定済み★なので、両者の関係は整理してから作ること。

### 5.2 保存先と保持

- ★アプリログ（pino / ファイル）だけでは監査ログにならない★: logrotate で消える（★保持期間は現状**不明**★）、
  検索・ページングができない、画面に出せない
- → ★**追記専用のストアを別に持つ**★。JSON 配列は追記のたびに全書き換えになり肥大化に弱いので、
  **SQLite（§4.2 案B）か JSONL（1行1レコード）**が向く。★ユーザーストアと同じ選択に揃えるのが自然★
- 保持期間・上限件数・ローテーションを**決めてから**作る（決めないと際限なく増える）
- 画面はモックどおり4列（日時 / 操作者 / アクション / 対象）。★モックにはフィルタもページングも無いが、
  件数が増えるので `meta: { total, page, per_page }` 付きの一覧 API にする★（`CLAUDE.md` §9.3）

### 5.3 既に半分用意されているもの

`lib/logger/events.ts:96-100`（`judge.grade`）と `:148-154`（`judge.advice`）は
★`thresholds_used` のスナップショットを持っており、コメントで F-10 の閾値編集を名指しで想定している★。

> F-10（M5）でしきい値が編集可能になると、同じ入力でも判定日によって発火するルールが変わるため、結果だけでは後から再現できない

★ただし一覧側の `judge.grade` ロギングは未実装★。閾値変更後に「なぜこの行が◎なのか」を
後から検証できるのは、1件詳細と一覧の境界近傍の行だけ。
★一覧側 `judge.grade` を `settings.change` とセットで入れると追跡が閉じる★。

---

## 6. 段階分割（指示書の割り方の案）

★依存の向きは L2 → L3 → L1★。「誰が変えたか」を後から足すと、L1 のログ設計をやり直すことになる。

| 段階 | 内容 | 依存・外部待ち |
|---|---|---|
| **T-0** | ★スコープ変更を `docs/decisions.md` に記録★（spec.md §2.3 / D-032 の上書き）＋ `docs/spec.md` の F-10 表と M5 を改訂 | なし。★最初にやる★ |
| **T-1** | `d-watch-api.yaml` を先に直す（設定・ユーザー・監査ログ・403） | T-0 |
| **T-2** | ユーザーストア＋パスワード（scrypt）＋セッション。既存 `AUTH_TOKEN` 方式からの移行 | T-1。★§8 Q2/Q3 の回答が要る★ |
| **T-3** | 認可（管理者ビット）を API ルートに入れる。権限管理タブ（参照のみ） | T-2 |
| **T-4** | 監査ストア＋既存の書き込み系APIに成功ログを追加＋監査ログタブ | T-3 |
| **T-5** | 判定関数の閾値引数化（`lib/judge/` 全体） | ★期待値表を先に確定★（`CLAUDE.md` §0-2。分担してはいけない） |
| **T-6** | 閾値ストア＋ `/settings/thresholds` ＋ 運用設定タブの編集UI（確認モーダル付き） | T-4, T-5。★§8 Q1 の回答が要る★ |
| **T-7** | 表示設定タブ＋ `/settings/dashboard` ＋ ダッシュボードのレイアウト対応 | T-4。★D-048（失敗の内訳）と D-091 項3（`security_off`）の未決が先★ |
| **T-8** | タブUI化（現在の1画面ベタ構成をタブに再構成）＋ `lib/i18n.ts` の訳語追加 | T-6, T-7 |
| **T-9** | デプロイ関連: `ALLOW_ALL_WHEN_EMPTY=false`、`data/` の権限・バックアップ、`docs/deploy.md` 追記 | 本番デプロイ方式の確認（★現状不明★） |

★**T-1 〜 T-9 を1本の指示書にしないこと**★。`docs/decisions.md` の決定番号を取るため衝突する
（2026-09-07 に D-068 が二重採番された事故がある）。

★**実データ接続を待たずに全部進められる**★。管理画面は主に DFA 側ストアの話で、
最大のブロッカーである「接続先ホスト名・ポートの未受領」の影響を受けない。

---

## 7. 実装前に潰しておくべき既存の不整合

| # | 内容 | 根拠 |
|---|---|---|
| 1 | ★`lib/config/advice-rules.ts:101` の B1 の `condition: "完了率が10〜69%（△判定）"` が実際の判定（70.0% も △）と食い違う★。これは F-10 画面に**実際に表示される文字列** | `docs/handoff/analysis/code-analysis-pure.md:3311` |
| 2 | ★`/api/settings/advice` の PATCH にバリデーション穴★: 長さ制限なし・空文字/空配列を弾かない・非文字列要素が静かに落ちる。GET には try/catch が無い。D-S備考APIが `DEVICE_NOTE_MAX_LENGTH` を持つのと非対称 | `app/api/settings/advice/route.ts:76-89` |
| 3 | ★`lib/config/__tests__/` に設定値の妥当性（`excellent > good > poor`）を検証するテストが無い★（`env-check.test.ts` 1本のみ） | 実測 |
| 4 | ★`lib/judge/__tests__/grade.test.ts:14-30` は境界値をリテラルで持ち `GRADE_THRESHOLDS` を import していない★。閾値を可変にすると「期待値表が何の値に対する表なのか」が曖昧になる | 実測 |
| 5 | ★`ALLOW_ALL_WHEN_EMPTY=true` を `npm run check:env` が検査していない★（TS定数なので原理的に載らない）。書き込み解禁後は被害が「見られる」から「書き換えられる」に変わる | `lib/config/env-check.ts` |
| 6 | ★`docs/deploy.md` に `data/` の記述が1件も無い★。保存先が `process.cwd()` 起点なので、再 clone 型デプロイだと設定が静かに既定値へ戻る（`json-store.ts` が ENOENT を正常扱いするため気づけない） | grep 0件 |
| 7 | ★CSP が未設定★（`next.config.ts` は6行）。XSS が1つ入ると CSRF 対策の有無に関係なく設定変更が通る。★「見られるだけ」から「書き換えられる」に変わると重みが変わる★ | 実測 |
| 8 | ★モック `:1163` の `ops` タブ重複★ | 実測 |

### CSRF について（結論: ★現状は追加対策不要。ただし条件つき★）

cookie は `SameSite=Lax`・`Domain` 指定なし（host-only）・`HttpOnly`・`Secure`。
書き込みは JSON body の `PATCH` のみで、HTML form は PATCH を送れず、
cross-origin fetch は CORS プリフライトで止まる（`next.config.ts` に CORS ヘッダなし）。

★**この結論が崩れる唯一の条件は「設定変更を GET で作ること」**★（Lax はトップレベル GET に cookie を送る）。
→ ★`CLAUDE.md` §9.3 に「設定変更は必ず PATCH/POST + JSON body。GET に副作用を持たせない」を1行足すのが最も効く★。

`SameSite=Strict` は外部リンク遷移で毎回ログイン画面に飛ぶため非推奨。
`Origin` 検証は任意だが、★比較基準の `Host` を nginx が素通しするかが**不明**★（確認せず入れると全員 403。fail-closed なので即座に気づける）。

---

## 8. ★ユーザー判断が必要な論点★（帰結つき）

| # | 論点 | 選択肢と帰結 |
|---|---|---|
| **Q1** | ★閾値変更で**過去の判定が遡って変わる**ことを許容するか★ | **(a) 許容する** → 実装が単純。ただし §3.8 の例（9/1に「異常なし」と閉じた日が、今日見ると L3「オンサイト訪問検討」）が起きる。／ **(b) 許容しない** → ★閾値は画面から編集させず `.env` ＋再起動に留める★（運用設定タブは参照のみのまま）。実装が大幅に減る。／ **(c) 許容するが、変更時に「全画面の判定が変わります」と警告し、監査ログに残す**（推奨） |
| **Q2** | ★ユーザー管理の認証方式★ | **(a) メール＋パスワード（scrypt）** → 依存追加ゼロ。パスワード再設定の運用が必要。／ **(b) 共有トークンを人数分に分ける** → 実装は3箇所・半日規模だが、★得られるのは「認証された身元」ではなく共有可能なラベル★。監査ログの信頼度が下がる。／ ★スコープが「ユーザー管理」である以上 (a) が筋★ |
| **Q3** | ★ログインセッションに有効期限を付けるか★ | `CLAUDE.md` §7 の禁止は **D-030 ＝ D-Sデバイスのトークン期限表示（F-06/F-07）**の話で、ログインセッションの寿命ではない（§4.4）。**(a) 付けない**（現状維持） → 席を離れた PC から設定を変えられる穴が残る。／ **(b) 付ける** → §7 の文言を「D-Sデバイスのトークン期限に限る」と明確化する決定が要る |
| **Q4** | ★ユーザー/監査の保存先★ | **(a) SQLite**（推奨） → F-05 点検記録と基盤を共有できる。★本番 VM の Node バージョン確認が先★。／ **(b) JSON/JSONL** → 依存ゼロだが排他が無く（後勝ち）、同時操作で消える。監査ログの検索が弱い |
| **Q5** | ★表示設定に「数値カード4列」を含めるか★ | モックのトグルは4つで**数値カードが入っていない**が、`docs/spec.md:35` は「各ブロックON/OFF可能」。**含めない**（モック準拠）→ 実装が単純。／ **含める** → `grid-template-columns: repeat(4, 1fr)` 固定の改修が要る |
| **Q6** | ★アクセスマトリクスの「施設ロボットマスタ 編集/登録」★ | spec.md §2.3 F-08 は GET のみと確定。**spec.md 準拠で「閲覧」に読み替える**（推奨）／ モック準拠で編集機能を作る → ★F-08 の「登録更新はD-Security側」という責任分界と衝突する★ |
| **Q7** | 閾値は整数のみか小数を許すか | 本案は整数（モックの `<input type="number">` に合わせる）。`clean_ratio` は `double(4,1)` なので小数第1位まで来る。★決めておかないと後で揉める★ |
| **Q8** | エンドポイント名 | `/settings/thresholds` + `/settings/dashboard`（推奨。既存 `/settings/advice` と守備範囲が重ならない）／ `/settings/operations` + `/settings/display` |
| **Q9** | ★編集のキルスイッチ（`SETTINGS_WRITABLE`）を用意するか★ | **用意する**（推奨） → 画面を先に作って解禁は後、が可能。障害時の凍結レバーにもなる。★向きは `=== "true"` ＝ 未設定なら編集不可★（D-101 項2-2 の規則）。本番で有効化を忘れると 403 になるので理由をメッセージに明示する。／ **用意しない** → デプロイした瞬間に全員が編集可能 |
| **Q10** | fixtures モードで設定変更を反映するか | 反映しないと `CLAUDE.md` §6-3（判定ロジックは切替の外側）の例外になる |

★**着手前に最低限 Q1・Q2・Q4 の回答が要る**★。Q1 が (b) なら L1 の大半が消え、Q2/Q4 は L2 の土台を決める。

---

## 9. ★不明な点（推測で埋めていない）★

| # | 不明 | 影響 |
|---|---|---|
| 1 | ★本番 VM の Node バージョン★ | `node:sqlite` が使えるか（§4.2 案B）が決まらない |
| 2 | ★本番のデプロイ方式（git pull / rsync / 再 clone / コンテナ）★ | `data/` が再デプロイで生き残るか判断できない |
| 3 | ★VM のプロセス管理方式（systemd / PM2 / Docker）と logrotate の保持期間★ | 監査ログをアプリログで代替できるかの判断に効く。`docs/logging-design.md` §9 でも未解決 |
| 4 | ★nginx が `Host` / `X-Forwarded-Host` を素通しするか★ | `Origin` 検証の実装可否 |
| 5 | ★アプリのポートに nginx 以外が到達できないか★ | D-101 申し送り1 のまま未確認 |
| 6 | ★D-091 項3（`security_off` を D-Secアラートから外す件）の内部調整結果★ | 表示設定の `secBanner` 項目の要否そのものが決まらない |
| 7 | ★D-048（`failure_breakdown` を API が省略している件）の整理★ | `failBreakdown` トグルが「ONにしても出ない」ままになる |
| 8 | UI仕様書 v6.0 の F-10 部分 | `docs/` 配下に転記が存在しない。F-10 の「正」は実質 モック ＋ `docs/spec.md:103-109` ＋ D-032 / D-036 / D-083 / D-084 の4決定に閉じている |

---

## 10. 参照

- `docs/spec.md` §2.3 F-10 / §3 M5 — 機能一覧と P1 スコープ
- `docs/decisions.md` D-030（トークン期限）/ D-032（F-10 限定スコープ）/ D-048 / D-053 / D-075（JSONストア）/ D-076 / D-079 / D-080（contract）/ D-084 / D-091 / D-101（認証）
- `d-watch-mock.jsx:1152-1734` — システム管理画面の全タブ
- `d-watch-api.yaml:614-` — 既存 `/settings/advice`（新設エンドポイントの雛形）
- `docs/logging-design.md` §3 / §5 / §6 — イベント一覧、未実装の `judge.grade`、「誰が」を設けない裁定
- `docs/handoff/analysis/prismaSource実装前チェックリスト.md` — 実データ接続時に静かに壊れる箇所
