# ソースコード地図 — 何がどこで決まっているか

**非エンジニア向け。** D-Watch のソースコードのどのファイル・どの関数・どの数値が何を司っているかを、
専門用語をできるだけ使わずに書いたものです。

- **第4版。記録した時点: 2026-10-05**（決定番号 **D-157** 時点・`develop` の `a1d7e43`）
  初版 2026-09-10 / 第2版 2026-09-14 / 第3版 2026-10-01 / 第3版改訂 2026-10-05（D-156）
- ★**第4版改訂: 2026-10-06**（**D-158** / 指示書AU の実装 `f5dce7b` まで）★ — 指示書AU で古くなった記述だけを直しました
  （当期 KPI の式・`task_area = 0` は 0%・予定は2表から読む・見出し語「屋号」→「顧客」・新しいファイル2本）。
  ★それ以外の行番号・行数は第4版（`a1d7e43`）のまま★です。PDF は作り直していません
- ★**第4版改訂2: 2026-10-06**（**D-159** / 指示書AV の実装 `137a94b` まで）★ — 指示書AV で古くなった記述だけを直しました
  （ダッシュボードの数値カードと表を「各清掃エリアの直近の清掃（7日以内）」に・ダッシュボードが `/api/clean-logs` を呼ばなくなった・
  清掃詳細ログの既定を全期間に・新しいファイル2本と `cleanLogRunMeta()`）。★直した箇所の行番号は `137a94b` で確かめました★。それ以外は上のとおりです
- ★**第4版改訂3: 2026-10-06**（**D-160** / 指示書AW の実装 `f02eb1d` まで）★ — 指示書AW で増えたもの・古くなった記述だけを直しました
  （新しい画面 ★エラー一覧（`/errors`）★・API 2本（`GET /api/errors`・`PATCH /api/errors/{error_id}/status`）・`DataSource` の3メソッド・
  `lib/error-reports/`・`lib/db/error-report-mapping.ts`・fixtures 2本・`lib/prechecks/status-cycle.ts`（★改訂4 で削除。D-161★）・メインDBへの書き込みが2本の道になったこと）。
  ★新しく書いた箇所の行番号は `f02eb1d` で確かめました★。それ以外は上のとおりです（★`lib/db/index.ts` の既存メソッドの行番号は AV・AW で後ろにずれています。例: Prisma 版 `prechecks()` は `:1216` → `:1399`（`f02eb1d`）★）
- ★**第4版改訂4: 2026-10-07**（**D-161〜D-174** / `04f974a` まで。コードは `8a657a2` から変わっていない）★ — 10/06〜10/07 の手直しで古くなった記述だけを直しました
  （★アラート・エラー一覧の対応状況は「編集モード」でだけ変えられ、「保存（N件）」でまとめて送る★（D-161 のプルダウン → D-165）・`lib/prechecks/status-cycle.ts` の削除・
  ★レポートの CSV が動くようになった★（`GET /api/reports/facility-performance/export`。D-163）・CSV の共通部品 `lib/csv.ts` と値が無い率の空欄（D-163・D-164）・
  `components/ui/date-picker.ts`・分類プルダウンの固定幅（D-162）・表の列幅の配り方と見出しの寄せ（D-166〜D-168。★新設 [3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)★）・
  ★事前チェックは成功・失敗の判定に使わない★（D-170。[5.9](#59-ダッシュボードの集計--libdashboardaggregatets123行)）・表示する事前チェックは 60/30 のまま（D-169）・
  ★事前チェックの予定が無い清掃の 1h前ST / 30分前ST は `null`（画面「-」・CSV 空）、本清掃が成功した事前チェック異常はアラートで控えめに★（D-171。`lib/format/precheck-status.ts`・`main_run_succeeded`）・
  ★**レポートがまだ無い清掃は、予定の終了から2時間（`RESULT_WAIT_MINUTES`）まで「結果待ち」とし、失敗に数えない**★（D-173。`isResultPending()`・`CleanLogItem.result_pending`・ダッシュボードの「結果待ち」カード・`settleMinutes` の廃止。★新設 [5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)★））。
  ★直した箇所・新しく書いた箇所の行番号は `a961829`〜`04f974a`（HEAD。`a961829` 以降はテスト1本と docs だけの変更）で `git show HEAD:<path>` から確かめました★（D-171 までの節で `f2b91fc` と書いてある行番号は、D-173 で動かなかったファイルのもの）。それ以外は上のとおりです（★`lib/config/index.ts` は AU 以降で後ろにずれています。例: `MAX_CSV_ROWS` は `:469` → `:494`（`a961829`）★。D-161〜D-173 で触っていない行は直していません）。
- ★**第4版改訂5: 2026-10-19**（**D-180** / 指示書AX の実装 `d282e07` まで）★ — 指示書AX で古くなった D-Sec アラートの記述だけを直しました（取得元を `robot_error_reports` に・編集モード・`PATCH /ds-alerts/{id}/status` の削除・fixtures の `ds-alerts.ts` の削除・削除済みの施設・清掃エリアの行を出さない）。★直した箇所の行番号は `d282e07` で確かめました★。それ以外は上のとおりです
- ★**第4版改訂6: 2026-10-19**（**D-181** / 指示書BA の実装 `b09e352` まで）★ — 編集モードのある3画面の見た目と D-Sec の文言を揃えた分だけ直しました（解消日時の判定 `showsResolvedTime()`・対応状況 120px・D-Sec の備考の入力欄の位置・変えた行の目印・アラートの「編集」の位置・D-Sec の状態ボタン「未解消のみ / 全件 / 解消済み」）。★直した箇所の行番号は `b09e352` で確かめました★。それ以外は上のとおりです
- ★**第4版改訂7: 2026-10-19**（**D-182** / 指示書BB）★ — データ層・API の止まる経路と削除済みの扱いを揃えた分だけ直しました（`lib/db/visibility.ts` を足した・ロボット0台／状態が 0/1/2 以外／心拍0件／直近モードのメタ欠けで例外にしない・`dsAlerts({ open_only })`・`ConsecutiveDbRange.facility_ids`・`parseDate()` の存在しない日付・`resolveTargetCustomer()` の AND・`weekStart()` / `cleanErrorLabel()` / `CLEAN_ERROR_LABELS` の削除・`/ds-devices` の `last_reported_at` が nullable）。★関数・型・設定の行番号と、本書で変わったファイルの行数は `86fea53`（コード）で引き直しました★（JSON ストアの書き込みの直列化 `c5fe519` を含む。各所の「行番号は `xxxx`」という注記よりこちらが新しい）。★関数名を伴わない位置の参照（`:N` だけのもの）は、その文を最後に書いた版から差分で写しただけ★で、それより前からずれていたものはずれたままです（関数名で探すこと）。D-182 は 2026-11-05 に `docs/decisions.md` へ追記済み
  期待値表（D-171 の N1〜N5・M1〜M6、D-173 の表1・表2）の中身は `docs/decisions.md` とテストを見てください。★D-172（第12回の候補への回答）のうち、レポートの書き込みの遅れ（30分を超えうる）は D-173 で解決済み★。★D-174（アドバイス B2 の根拠をエラーコードが決まったら本清掃のエラー内容に切り替える）は 5.8 に書きました★
- ★**第4版改訂8: 2026-10-19**（**D-183** / 指示書BC の実装 `50a3082` まで）★ — 画面の古い応答・失敗の表示・表示設定の保存・D-Sec ログの備考の編集モードの分だけ直しました。★直した箇所の行番号は `50a3082` で確かめました★（画面のファイルの `:N` は `77f2f1b` → `50a3082` の差分で写し、変わった行を指すものは関数名・文言で引き直した）。それ以外は上のとおりです。D-183 は 2026-11-05 に `docs/decisions.md` へ追記済み
- ★**第4版改訂9: 2026-10-27**（**D-184** / 指示書BD の実装。★未コミットの作業ツリー★（`bb79de9` の上））★ — エラー一覧の「エラー内容」（`error_lists` の id）を API で選べるようにした分だけ直しました
  （新しい API ★`GET /api/error-lists`★（選択肢）・`PATCH /api/errors/{error_id}/status` が ★5列目の `error_list_id`★ も書く（★この経路だけ。事前チェックと D-Sec は4列のまま★）・
  `ErrorReportItem` の3項目（`error_list_id` / `error_list` / `error_list_selectable`）・新しいファイル（`lib/db/error-list-source.ts`・`lib/errors/status-update.ts`・`lib/fixtures/error-lists.ts`）・
  `DataSource.errorLists()`・設定2つ（`ERROR_LIST_NOTE_REQUIRED_IDS` / `ERROR_LIST_UNSELECTABLE_ERROR_TYPES`）・★`prisma/schema.prisma` の `ErrorList` と `error_list_id` は db pull できないための手書き（D-056 の例外）★）。
  ★直した箇所の行番号は 2026-10-27 の作業ツリーで `grep -n` して確かめました★。それ以外は上のとおりです。PDF は作り直していません。D-184 は 2026-11-05 に `docs/decisions.md` へ追記済み
- ★**第4版改訂10: 2026-10-27**（指示書BE。エラー一覧の画面でエラー内容を選ぶ）★ — 「種類」の2行目と選択窓（`ErrorContentPicker`）・エラー一覧専用の下書き（`lib/errors/edit-drafts.ts`）・その他の備考必須。D-185 は 2026-11-05 に `docs/decisions.md` へ追記済み（案は指示書BE §11）
- **この文書は「正」ではありません。** ルールの正は [CLAUDE.md](../CLAUDE.md)、構成と依存の向きの正は
  [docs/architecture.md](architecture.md)、決定の理由の正は [docs/decisions.md](decisions.md) です。
  食い違ったら向こうが正しいと考え、この文書を直してください
- 対象: 人が書いたコード **177ファイル / 29,722行**（TypeScript のみ。テストと自動生成物を除く）。
  テストは **104ファイル / 2,469件**（全件成功）。★改訂時点（`f5dce7b`）は **106ファイル / 2,540件**（全件成功。2026-10-06 `npx vitest run` で実測）★。★改訂2 時点（`137a94b`）は **111ファイル / 2,601件**（全件成功。監督が確認）★。★改訂3 時点（`f02eb1d`）は **119ファイル / 2,790件**（全件成功。監督が確認）★。★改訂4 時点（`04f974a`）は **125ファイル / 2,946件**（全件成功。監督が確認。`f02eb1d` から +7本・−1本。[11.2](#112-いまの規模実測)）★
- ★**行番号はあてになりません。**★ 1か月で4回改訂するほど動いています。**ズレていたら定数名・関数名で検索してください。**

## ★第4版で新しくなったところ★

★**[章5-B「画面に出る数字の出どころ」](#5-b-画面に出る数字の出どころ--テーブル列式根拠)を新設しました。**★
画面に出る数字を1つずつ、**どのテーブルのどの列から、どういう式で、なぜその根拠なのか**を表にしています。
「この数字は何を数えているのか」「なぜこの計算なのか」と思ったら、まずそこを見てください。

あわせて章8（データの出どころ）と章6（設定値）にも、取得元の列と根拠の決定番号を添えました。

## ★前提の変遷 — 古い版を読んだ人は、ここだけ先に読んでください★

この1か月で、**判定の基準・完了率の出し方・接続先の呼び名**が何度か変わりました。
いま正しいのは右端の列です。

| 項目 | 9/14 版 | 第3版（10/01） | ★現在（第4版）★ | 根拠 |
|---|---|---|---|---|
| ◎○の境界 | 90%**超**（90.0 は ○） | 90%**以上**（90.0 は ◎） | ★90%**以上**（**90.0 は ◎**）★ | D-139 |
| ○△の境界 | 70%**超**（70.0 は △） | 70%**以上**（70.0 は ○） | ★70%**以上**（**70.0 は ○**）★ | D-139 |
| ×の境界 | 10%未満 | 10%未満 | 10%未満（**10.0 は △**） | D-139 |
| ★清掃完了率の出し方★ | 期間の面積の合計どうしの比 | `clean_ratio` 列の単純平均 | ★**1回ごとの面積比**（上限100・小数第1位切り捨て）の単純平均。★`clean_ratio` 列は使わない★（★**確定**。第11回 P-2=A★）★ | D-151 → D-155 → **D-158** |
| ★`task_area = 0` の清掃★ | — | — | ★**1回ごとの完了率を 0%**（判定 ×）★。旧（10/02〜10/05）: `null`（分子・分母の外） | D-155 → **D-158**（第11回 P-5=B） |
| ★当期の稼働率・出発成功率・エラー発生率★ | — | — | ★**正常終了（status 4）÷ 予定 / 開始できた回数 ÷ 予定 / エラー ÷ 予定**（サマリと同じ式。エラー発生率は上限なし）★。旧（指示書AU の前）: レポート行がある回数 ÷ 予定 / 同じ / エラー ÷ 実施回数 | **D-158**（第11回 P-6） |
| ★清掃の予定の取得元★ | — | `clean_timetables` | ★`clean_timetables` と `clean_timetable_histories` の**両方**（同じ id は本表を採る）★ | **D-158** 項4（第11回 P-7） |
| ★`customers` の見出し語★ | — | 屋号 | ★**顧客**（EN "Customer" / "All Customers"）★ | **D-158**（第11回 P-15=B） |
| ★ダッシュボードの数値カード・表の母集団★ | 当日 | 当日 | ★**各清掃エリアの直近の清掃1件**（7日以内・★予定の終了 ≤ 現在★の予定のみ・出発失敗と結果待ちを含む）★。★旧（10/06）: 終了＋30分（`settleMinutes`）たった予定のみ。D-173 で廃止★。`?date=` 指定時は従来どおりその日。★スケジュール進捗・要レスキューは当日のまま★ | **D-159**（指示書AV）→ **D-173** |
| ★清掃詳細ログの既定の期間★ | 今日1日 | 今日1日 | ★**期間を指定しない（全期間）**★ | **D-159** 追記（ユーザー指示「C」） |
| 予定外のタスク（`clean_timetable_id` が NULL か 0） | — | （扱いなし） | ★**1件として数える**（予定 +1・開始 +1・`status = 4` なら正常終了 +1・1回ごとの完了率を平均に入れる。★今の期間だけ。確定期間は TeamWorks のサマリのまま★）。総清掃面積にも入れる★。旧（10/02〜10/29）: 完了率・実施回数には入れない | D-155 項3 → **D-187**（指示書BG・A案） |
| 清掃実績の取得元 | `clean_status_histories` | `clean_task_reports` | `clean_task_reports` | D-151 |
| 清掃結果（`clean_task_reports.status`） | — | 数字のまま | ★表示名（0 清掃開始前〜5 清掃異常終了）★。**判定には使わない** | D-155 項2 |
| ★接続先DBの呼び名★ | （外部DBは未接続。ローカルの MySQL のみ） | 「本番DBそのもの」 | ★**ステージング**（本番DBは別にある）★ | D-148 → **D-152** |
| 接続先DBの中身 | （ローカルに当方のシード） | 実データ（予定0行でほぼ空） | 実データ（ドン・キホーテの実店舗10件＋本社） | 2026-10-01 |
| ★DBへの書き込み★ | しない | しない | ★`/alerts` から4カラムを更新できる★（ステージングに限り可）。★10/06 から `/errors`（エラー一覧）からも、`robot_error_reports` の全行（削除済みの施設・清掃エリアの行を除く。U8。D-180 項11）に書ける★。★10/27 から `/errors` の PATCH だけは5列目の `error_list_id`（エラー内容）も書く。画面は「詳細」の列（備考の左）から選ぶ（指示書BE §12）★ | D-152 / D-156 / ★D-160★ / D-180 / ★D-184★ |
| ★エラーの一覧★ | — | — | ★`/errors`（エラー一覧）が `robot_error_reports` の全行（削除済みの施設・清掃エリアの行を除く。U8。D-180 項11。`meta.total` にも数えない）を出す（既定は未解消・対応中。種類は生値のまま）★。アラート画面は事前チェック＋出発失敗のまま | **D-160**（指示書AW）/ D-180 |
| ★対応状況・備考の変え方（`/alerts`・`/errors`）★ | — | —（10/02〜10/06 は押すたびに巡回するボタン＋備考はフォーカスが外れたら保存。指示書AT） | ★**編集モード**★（通常は表示だけ。「編集」で入り、プルダウンと入力欄で下書き → 「保存（N件）」で★変えた行だけを1行ずつ順に★ PATCH。全部成功で通常に戻る・失敗行は下書きを残す・編集中は絞り込みとページ送り不可）★ | D-161（プルダウンで即保存。10/06）→ **D-165** |
| ★レポートの「CSVエクスポート」★ | 押しても何も起きない | 押しても何も起きない | ★**施設別パフォーマンスの表を CSV で出す**（表と同じ条件・`limit` は無視して全施設）★ | **D-163** |
| ★CSV の値が無い率★ | `-` | `-`（Excel では `'-` と見えた） | ★**空のセル**★（清掃詳細ログ・レポートの両方。数式対策 D-125 は維持） | **D-164** |
| ★事前チェックの結果・有無と成功／失敗★ | — | — | ★**判定に使わない**★（本清掃のレポート行の有無と完了率だけで決める。すでにそうなっていたことを確かめ、歯止めのテストを足した） | **D-170**（D-048 と同じ考え方） |
| ★事前チェックの予定が無い清掃の 1h前ST / 30分前ST★ | `Standby.` | `Standby.`（異常なしと区別できない） | ★`null`★（画面「-」・CSV 空のセル・アドバイス B2/B3 に当たらない）。予定があって異常なしは `Standby.` のまま | **D-171** |
| ★本清掃が成功した事前チェック異常（`/alerts`）★ | — | — | ★カードを控えめに（不透明度 0.6）＋「本清掃は成功」★。API に `main_run_succeeded`。★対応状況・件数は変えない★ | **D-171** |
| ★レポートがまだ無い清掃★ | — | 予定の開始を過ぎたら出発失敗 | ★**結果待ち**★（予定の開始 ≤ 現在 ＜ 予定の終了＋`RESULT_WAIT_MINUTES`（120分）。清掃中も含む）。★失敗・未実施に数えない★（詳細ログは出発「…」・清掃結果「結果待ち」・判定なし／ダッシュボードは「結果待ち」カード／アラートの出発失敗・連続日数・要レスキュー・当期の率から外す）。過ぎたら今までどおり出発失敗。★旧（10/06〜10/07 朝）: ダッシュボードだけ終了＋30分待ち、他は開始を過ぎた瞬間から出発失敗★ | **D-173**（D-091 項1 をこの件に限り上書き） |
| DBの時刻 | — | 一部の列がUTC | ★全列 JST の壁時計の値★（先方が統一。当方の再実測は未了） | D-155 項4 |
| 年次の集計 | 読まない | 解禁 | 解禁 | D-151 |
| 画面 | 12 | 13 | 13（★改訂3 時点 14★） | — |
| APIルート | 19本 | 22本 | **23本**（★改訂3 時点 25本・改訂4 時点 26本★） | — |
| `DataSource` のメソッド | 15本 | 20本 | **22本**（★改訂2 時点 23本・改訂3 時点 26本★。改訂4 で増減なし） | — |
| テスト | 1,668件 | 2,297件 | **2,469件**（★改訂時点 2,540件・改訂2 時点 2,601件・改訂3 時点 2,790件★。改訂4 時点 125ファイル・2,946件） | — |

★**清掃完了率の式は確定しました**★（第11回 P-2=A「今の式のまま」。P-1: 訂正後の `clean_ratio` 列もこの式と同じ値になる。D-158）。
★まだ「暫定」の数字もあります★（施設ステータスの閾値など）。どれが暫定かは[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)の一覧にまとめています。

## 目次

| 章 | 内容 | こんなときに |
|---|---|---|
| [0](#0-このドキュメントの読み方) | このドキュメントの読み方 | 最初に |
| [1](#1-d-watch-は何をするアプリか) | D-Watch は何をするアプリか | 全体像をつかむ |
| [2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで) | **まず1本だけ追う** | ★これだけ読めば他の画面も分かる |
| [3](#3-画面カタログ) | 画面カタログ（13画面。★改訂3 から14画面★） | 画面から探す |
| [4](#4-apiの入口--appapi-の23本) | APIの入口（23本。★改訂4 時点 26本★） | データの窓口を探す |
| [5](#5-アプリの頭脳--判定と計算) | **判定と計算**（◎○△×・L1〜L3・KPI） | ★数字の出し方を知る |
| [5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠) | ★**画面に出る数字の出どころ**（テーブル・列・式・根拠）★ | ★**この数字は何？なぜこの計算？**★ |
| [6](#6-変えると挙動が変わる数値--libconfig) | **変えると挙動が変わる数値** | ★数値を変えたい |
| [6-B](#6-b-画面ごとのデータの取り出し方--クエリ層) | クエリ層・型・設定の保存 | 絞り込みや並べ替えを変えたい |
| [7](#7-見せ方--整形部品色言葉) | 見せ方（整形・部品・色・言葉） | 表示や文言を変えたい |
| [8](#8-データの出どころ) | データの出どころ | どのテーブルから来るのか知りたい |
| [9](#9-裏方--入口の関門ログ起動) | 裏方（認証・IP制限・ログ） | アクセスできない・ログを見たい |
| [10](#10-人が書いていないファイル開かなくてよいフォルダ) | 人が書いていないファイル | ファイル数に圧倒されたとき |
| [11](#11-テストは何を守っているか) | テストは何を守っているか | 変更後の確認 |
| [12](#12-よくある変更と触る場所) | **よくある変更と、触る場所** | ★やりたいことが決まっている |
| [13](#13-用語対応表) | 用語対応表 | 画面の言葉からコードを探す |
| [14](#14-設定ファイルと保存ファイル--アプリの外側の決めごと) | 設定ファイルと保存ファイル | ビルド設定・環境変数・保存先を知る |
| [付録A](#付録a-全ファイル一覧) | 全ファイル一覧 | ファイル名から探す |
| [付録B](#付録b-画面から引く逆引き索引) | 画面から引く逆引き索引 | 画面名から探す |
| [付録C](#付録c-設定値定数の早見表) | 設定値・定数の早見表 | 数値から探す |

> **急いでいる人へ:** 章2 →章5-B →章12 →付録B の順に読めば、たいていのことはできます。


---

## 0. このドキュメントの読み方

### 誰のための文書か

**コードを書き換えることもあるが、プログラミングの専門家ではない人**のための、D-Watch のソースコードの地図です。
「この数字はどこで決まっているのか」「ここを直したら何が変わるのか」を引けるようにしてあります。

### 何が書いてあって、何が書いていないか

| | この文書 | 他の文書 |
|---|---|---|
| ファイル・関数・定数が何を司っているか | ✅ ここ | — |
| どこを直すと何が変わるか | ✅ ここ | — |
| **機能の仕様**（この画面は何を表示すべきか） | ❌ | [docs/spec.md](spec.md) |
| **なぜこの仕様なのか**（決定の経緯） | ❌ | [docs/decisions.md](decisions.md) |
| **守るべきルール・禁止事項**（これが「正」） | ❌ | [CLAUDE.md](../CLAUDE.md) |
| **依存の向き・触ってはいけないもの**（これが「正」） | ❌ | [docs/architecture.md](architecture.md) |
| いま何が未実装か・進行中の作業 | ❌ | [docs/handoff.md](handoff.md) |
| 開発環境のセットアップ | ❌ | [docs/getting-started.md](getting-started.md) |

**この文書は「正」ではありません。** ルールの正は `CLAUDE.md`、構成と依存の向きの正は `docs/architecture.md` です。
この文書はその**言い換えと索引**であり、食い違ったら向こうが正しいと考えてください（そしてこの文書を直してください）。

### 使い方の目安

- **とにかく1本流れを掴みたい** → 章2だけ読む。他の10画面も同じ形です
- **画面から探したい** → 付録B（逆引き索引）
- ★**画面の数字が何を数えているか・なぜその計算かを知りたい** → [章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)★
- **数値を変えたい** → 章6 と 付録C
- **やりたいことが決まっている** → 章12
- **ファイル名から探したい** → 付録A

---

## 1. D-Watch は何をするアプリか

**夜のあいだに自動で走った清掃ロボットの結果を、翌朝 DFA の運用担当が確認して、問題のある店舗を見つける** — これが D-Watch のすべてです。業務の流れは1本しかありません。

```mermaid
flowchart LR
  A["清掃ロボット<br/>（店舗に設置）"] --> B["Pudu Cloud<br/>（メーカーのクラウド）"]
  B --> C["メインDB<br/>（MariaDB）"]
  C --> D["D-Watch<br/>（このアプリ）"]
  D --> E["運用担当<br/>（翌朝ブラウザで確認）"]

  style C fill:#3d2a06,stroke:#f59e0b,color:#e2e8f0
  style D fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
```

**このリポジトリが担当しているのは、青枠（D-Watch）だけです。**
ロボットからデータを集めてデータベースに書き込むところまでは外部の会社（TeamWorks）の担当で、
D-Watch は**できあがったデータベースを読んで画面に出す**のが仕事です。

★**サーバーの用意・構築・保守も TeamWorks の担当です**★（D-143。2026-09-16 確認）。
DFA はサーバーの費用を払いますが、作るのは**画面（このリポジトリ）だけ**です。
以前の文書に「DFA がアプリ用のサーバーを構築する」と書かれていた箇所がありますが、それは誤りです。

そのため D-Watch は原則としてデータベースに**書き込みません**。★**例外は「対応状況を記録する」4項目と、エラー一覧の「エラー内容」1項目だけ**★です —
アラート画面（`/alerts`）で事前チェック異常の行の対応状況（未解消 / 対応中 / 解消）と備考を変えると、
`robot_error_reports` の `status` / `comment` / `responded_at` / `resolved_at` の4列を更新します（指示書AT。2026-10-02）。
★10/06 からはエラー一覧（`/errors`）からも、`robot_error_reports` の全行（削除済みの施設・清掃エリアの行を除く。U8。D-180 項11）の同じ4列を書けます（D-160）★。
★10/27 からはエラー一覧の窓口（`PATCH /api/errors/{error_id}/status`）だけが、5列目の `error_list_id`（エラー内容。`error_lists` の id）も書けます（D-184 / 指示書BD）。画面はエラー一覧の「詳細」の列（備考の左）から選びます（指示書BE §12）★。
★アラート（`/prechecks`）と D-Sec アラートからは `error_list_id` を書けません★（4列のまま）。
★どちらの画面も「編集」で編集モードに入り、「保存（N件）」を押したときだけ送ります（D-165）★。
★メインDBへ書き込むコードは `lib/db/robot-error-write.ts` の1ファイルだけ★で、★経路ごとに許す列が決まっている★作りです（事前チェックは4列・エラー一覧は5列。それ以外は例外。章8・章9）。
以前は「本番DBへは一切書き込まない」（D-148）方針でしたが、接続先がステージングだと分かり（D-152）、
ステージングへの4列の書き込みはユーザーの指示（2026-10-01「書き込んでよい」）で解禁されました。

### アプリの中身の流れ

```mermaid
flowchart LR
  U["利用者の<br/>ブラウザ"] -->|"① 画面を開く"| P["画面部品<br/>components/features/"]
  P -->|"② データをください"| API["APIの窓口<br/>app/api/"]
  API -->|"③ 取り出して"| DB["データ層<br/>lib/db/"]
  DB -->|"④a 練習用"| F["サンプルデータ<br/>lib/fixtures/"]
  DB -->|"④b 本番用"| M[("メインDB<br/>MariaDB")]
  API -->|"⑤ 判定・計算"| J["判定ロジック<br/>lib/judge/"]

  style J fill:#0a3d1f,stroke:#22c55e,color:#e2e8f0
  style F fill:#3d2a06,stroke:#f59e0b,color:#e2e8f0
```

④a と ④b の切り替えは **`USE_FIXTURES` という1つのスイッチ**だけで決まります（章8）。
画面の見た目はどちらでも1ピクセルも変わりません。**何も指定しなければ ④a（練習用の作り物データ）です。**

★**清掃の実績は `clean_task_reports` というテーブル1つから読みます**★（D-151。2026-09-30）。
以前は `clean_status_histories` から読んでいましたが、一本化されました。

### 最初に知っておくと迷わない3つのこと

**① 接続先のDB（ステージング）には実データが入りましたが、まだ「ほぼ空」です**

出どころは2通りあります。

| スイッチ | どこから読むか | 中身 |
|---|---|---|
| `USE_FIXTURES` が `false` 以外（既定） | `lib/fixtures/` | 練習用の作り物データ |
| `USE_FIXTURES=false`（現在の設定） | ★**ステージングDB**（TeamWorks のサーバー。★本番DBは別にある★。D-152）★ | ★**2026-09-30 夜〜10-01 未明に実データが入った**★ |

ステージングの中身は 9/30 の夜に入れ替わり、当方のダミーは消えました。いま入っているのは
★ドン・キホーテの実店舗10件＋本社★です（[CLAUDE.md](../CLAUDE.md) §3。2026-10-01 実測・先方投入）。
★データは今後も入れ替わることがあります★（第10回 N-6。時期未定）。

★**ただし清掃の予定が0行なので、清掃詳細ログ・ダッシュボード・アラートは空です。**★（2026-10-01 時点）
★2026-10-06 には予定が入り始め、ダッシュボードは直近モード（D-159）で11エリアを出しました★（先方投入。追記61。★予定と実績はまだつながっていない★。D-158 末尾）
清掃面積の画面だけは実績を直接数えるので、★面積の合計には `clean_task_reports` の2行ぶんが出ます★。
ただしその2行はどちらも `clean_timetable_id` が NULL（★予定外のタスク★）なので、
★実施回数と完了率には入りません★（D-155 項3。実施回数0回・完了率は「-」の見込み。★画面での確認は未了★）。
**画面どうしで数字が食い違って見えるのはこのためで、不具合ではありません。**
清掃詳細ログを「予定」起点にするか「実績」起点にするかは、データが揃ってから検討することになっています（D-151 項7）。

★**DBに書き込むのは、アラート画面・エラー一覧の対応状況・備考の4列と、エラー一覧の窓口のエラー内容（`error_list_id`）だけです**★（上記。D-152 / 指示書AT / D-160 / D-184）。それ以外はすべて読むだけです。

サンプルデータには**わざと異常なケース**（完了率0%、未実施、エラー多発）が仕込んであります。
不具合ではなく、判定が正しく働くか確かめるための仕込みです。

**② 1つの画面は、役割の違う4つのファイルでできている**

画面のファイルを開いたら4行しかなかった、というのは正常です。

| 役割 | 例 | 何をするか |
|---|---|---|
| 住所札 | `app/(dashboard)/detail-log/page.tsx` | URL `/detail-log` を作るだけ。数行しかない |
| 画面の中身 | `components/features/detail-log/DetailLogScreen.tsx` | **あなたのブラウザの中で**動く |
| データの窓口 | `app/api/clean-logs/route.ts` | **サーバーの中だけで**動く |
| 取り出し方 | `lib/clean-logs/query.ts` | 絞り込み・並べ替えの規則 |

なぜ分けるのか。**ブラウザの中で動くコードは利用者に中身が見えてしまう**からです。
データベースの場所やパスワードをそこに書くわけにいかないので、画面はデータベースを直接読まず、
必ずサーバー側の窓口に「これをください」と頼みます。詳しくは章2。

**③ ファイルの大半は、人が書いていない**

`lib/generated/prisma/` は自動生成物で、行数だけならアプリ全体の8割を占めます。
**人が書いたコードは 27,977 行**（TypeScript のみ）です。ファイル数に驚く必要はありません（章10）。


---

## 2. まず1本だけ追う — 清掃詳細ログの数字が画面に出るまで

この章だけは飛ばさずに読んでください。D-Watch のソースコードは170を超えるファイルがありますが、**どの画面も同じ型紙でできています**。その型紙を1本だけ、ブラウザからデータベース（DB）の列まで通して見ます。例に使うのは「清掃詳細ログ」（画面ID F-02、URL は `/detail-log`）です。

> **この章で出てくる言葉**
> - **API**（エーピーアイ）… 画面がサーバーへ「データをください」と頼むための窓口。D-Watch では `app/api/` の下のファイルがそれです
> - **テーブル / 列** … DB の中の表と、その縦の項目。`clean_task_reports.clean_area` は「`clean_task_reports` という表の `clean_area` という列」
> - **Prisma**（プリズマ）… TypeScript から DB を読むための部品。SQL を手で書かずに済みます
> - **ステージング** … TeamWorks が用意した検証用の DB（`133.125.238.144` / `clean_watch`）。★本番DBは別にあり、まだ接続していません★（D-152）

### 2.1 登場するファイル

ブラウザで `/detail-log` を開くと、下の5つが順番に動きます。6番目と7番目は5番目が中で呼ぶ部品です。

| # | ファイル | 行 | 何をする |
|---|---|---|---|
| 1 | `app/(dashboard)/detail-log/page.tsx` | 全5行 | URL `/detail-log` と中身を結びつけるだけの札 |
| 2 | `components/features/detail-log/DetailLogScreen.tsx` | 532行 | 画面そのもの。入力欄・表・ページ送り。表の部品は `CleanLogTable.tsx`（292行） |
| 3 | `lib/api/client.ts` の `apiFetch()` | `:102` | 画面からサーバーへの唯一の通り道 |
| 4 | `app/api/clean-logs/route.ts` の `GET()` | `:47` | サーバー側の入口（112行） |
| 5 | `lib/db/index.ts` の `db.cleanLogs()` | 選ぶのは `:3070`、中身は `:585`（サンプル）か `:990`（実DB） | データの取り出し口 |
| 6 | `lib/db/clean-log-mapping.ts` | 489行 | DB の行を画面の1行に組み立てる変換（出発したか・完了率・最新の1行の選び方） |
| 7 | `lib/db/consecutive-source.ts` の `loadConsecutiveInputs()` | `:388` | 連続日数を数えるための材料を読む（他の画面と共有） |

### 2.2 絵にすると

```mermaid
sequenceDiagram
    participant B as ブラウザ
    participant S as DetailLogScreen.tsx
    participant C as apiFetch()<br/>lib/api/client.ts
    participant R as GET()<br/>app/api/clean-logs/route.ts
    participant D as db.cleanLogs()<br/>lib/db/index.ts
    participant DB as ステージングDB<br/>(MariaDB 10.11)

    B->>S: /detail-log を開く
    S->>S: 入力値を URL の文字列にまとめる（:137 queryString）
    S->>C: 取得の effect（:191）が呼ぶ（:205）
    C->>R: GET /api/clean-logs?date_from=…（:114 fetch）
    R->>R: 1. パラメータ検査（:50）／2. 施設名→施設ID（:77-81）
    R->>D: 3. 取得（:82）
    D->>DB: ① clean_timetables ＋ clean_timetable_histories（本清掃 precheck=0・期間・施設。同じ id は本表）:765
    D->>DB: ② その子の事前チェック行（parent_id）:831
    D->>DB: ③ clean_task_reports（clean_timetable_id ＝ 本清掃の id）:850
    D->>DB: ④ robot_error_reports（clean_timetable_id ＝ 事前チェック行の id）:857
    D->>DB: ⑤ 連続日数の材料（consecutive-source.ts:356 / :383）
    D-->>R: CleanLogItem の配列（判定まで済み）
    R->>R: 4. 絞り込み（:98）／5. ページ切り出し（:99-104）／6. 整形（:106-110）
    R-->>C: { items, meta } の JSON
    C-->>S: 型のついたオブジェクト
    S-->>B: 表として描画（CleanLogTable.tsx）
```

右端の「ステージングDB」は TeamWorks の持ち物で、D-Watch は**読むだけ**です（[CLAUDE.md](../CLAUDE.md) §2。例外はアラート画面・エラー一覧の4列の更新と、エラー一覧の窓口の `error_list_id`（D-184）だけ。[章8](#8-データの出どころ)）。

### 2.3 1枚ずつ見る

**① `app/(dashboard)/detail-log/page.tsx`（5行）**

```tsx
import { DetailLogScreen } from "@/components/features/detail-log/DetailLogScreen";

export default function DetailLogPage() {
  return <DetailLogScreen />;
}
```

本当にこれだけです。Next.js は「フォルダの形がそのまま URL になる」という決まりなので、`detail-log/page.tsx` が置いてあること自体が「`/detail-log` という URL を作る」という意味になります。

ここで**丸括弧**が出てきます。`(dashboard)` の括弧は **URL に出ません**。`/dashboard/detail-log` ではなく `/detail-log` です。括弧は「URL のためではなく、**同じ見た目の枠（サイドバー）を共有する仲間**をまとめるための印」です。共有部分の実体は `app/(dashboard)/layout.tsx`（9行）にあり、`app/(dashboard)/` の下にある11画面すべてがこれを着ます。

**角括弧**も同じ仲間です。`app/api/clean-logs/[id]/advice/route.ts` の `[id]` は「ここには数字が入る」という穴です。`/api/clean-logs/37/advice` と呼ぶと `37` が `id` として渡ってきます。

**変えると何が起きるか**: フォルダ名 `detail-log` を変えると URL が変わり、画面内のリンク（モックから移植した遷移）が全部切れます。括弧を外して `dashboard/` にすると、11画面すべての URL に `/dashboard` が割り込みます。

**② `components/features/detail-log/DetailLogScreen.tsx`（587行。`50a3082`）**

1行目が `"use client"` です（`:1`）。これは「**このファイルはブラウザの中で動く**」という宣言です。入力欄に文字を打つ・ページ送りを押す・行を開く、といった**人の操作に反応する**画面にはこの宣言が要ります。宣言が無いファイルはサーバー側だけで動き、ボタンを押しても何も起きません。

この画面がやっていることは3つです。

- 入力値（日付・施設名・機種・シリアル・判定・分類）を持つ（`:45-95` あたり）。★日付の初期値は空＝期間を指定しない（全期間）です★（`:74-75`。D-159 追記。旧: 今日1日）。空の側は URL に付けません（`lib/clean-logs/detail-log-date-range.ts`）
- それを URL の文字列に組み立てる（`:137` の `queryString`）
- その文字列で `apiFetch()` を呼び、返ってきた配列を表にする（`:191` の取得の effect、`:205-210`）。★取得中に条件を変えたら、遅れて届いた古い応答は捨てます★（片付けで `cancelled` にする。D-183）

文字入力（施設名・機種・シリアル）は1文字打つたびに通信すると重いので、0.3秒待ってからまとめて投げます（debounce。`:114-135`）。期間が `MAX_DATE_RANGE_DAYS`（731日）を超えると、通信する前に画面で止めます（`:165`・`:192-201`）。

**変えると何が起きるか**: `:137` の `queryString` の依存リストから `page` を落とすと、ページ送りボタンを押しても取り直しません（コメント `:151-152` が警告しています）。例外もエラーも出ず、ただ「ボタンが効かない」だけになります。

**③ `lib/api/client.ts` の `apiFetch()`（`:102`）**

画面からサーバーを呼ぶときは、必ずこの関数を通します。`fetch`（通信命令）を画面に直接書いてはいけません。認証のやり方が変わったときに手を入れる場所を1箇所にしておくためです（ファイル冒頭 `:1-16`）。

この関数は3つのことを保証します。

- 外部サイトの URL は受け付けない（`:106-110`）。D-Watch は自分のサーバーしか呼びません
- 失敗したときも本文を読み、サーバーが返した日本語の文言を画面に渡す（`:125-142`）
- 通信そのものが届かなかった場合は `status: 0` にする（`:115-122`）

**変えると何が起きるか**: ここを通さずに画面に `fetch` を直書きすると、エラーのときに画面へ `<!DOCTYPE html>...` のような生の文字列が出ます（`:22-27` がその事故を説明しています）。

**④ `app/api/clean-logs/route.ts` の `GET()`（`:47`）**

サーバー側の入口です。ファイル冒頭のコメント（`:9-16`）が、この中の処理を**6段階**と宣言しています。

1. クエリパラメータの検査（日付範囲の上限は731日＝2年）
2. 施設名・分類（ホールディングス／顧客）→ 施設ID の解決
3. データソース（サンプル or 実DB）からの取得
4. 絞り込み（日付 / 施設名 / 機種 / シリアル / 判定）
5. ページング
6. `{ items, meta }` の形で返す

実装でも `:50`（1）→ `:77-81`（2）→ `:82-89`（3）→ `:98`（4）→ `:99-104`（5）→ `:106-110`（6）と、宣言どおり並んでいます。

**3でSQLに絞らせたのに4でもう一度絞っている**のは意図的です（`:18-22`）。絞り込みの「正しい規則」を `filterCleanLogs()`（`lib/clean-logs/query.ts:155`）という1つの関数に置き、SQL 側はその写しにしておく設計です。ただしコメントは同時に警告しています — **SQL が絞りすぎた場合、4では取り戻せません**。

**変えると何が起きるか**: `:77-81` の施設ID解決は、`null`（絞らない）と `[]`（0件ヒット）を厳密に区別しています（`:69-76`）。`??` で書くと、施設名と顧客を同時に指定したときに片方の条件が静かに消えます。**例外もテスト失敗も出ません。**

**⑤ `lib/db/index.ts` の `db.cleanLogs()`**

`db` という名前の実体は、ファイル最終行 `:2591` の1行で決まります。

```ts
export const db: DataSource = USE_FIXTURES ? fixtureSource : prismaSource;
```

環境変数 `USE_FIXTURES` が `"false"` でなければサンプルデータ（`:459` の `fixtureSource`）、`"false"` なら実DB（`:706` の `prismaSource`）です。**どちらを選んでも、④のルートから見える形はまったく同じ**です（[章8](#8-データの出どころ)）。

実DB側の `cleanLogs()`（`:754`）は、DB に4回問い合わせてから、メモリの上で行を組み立てます（次の 2.4 で列ごとに追います）。テーブル同士をつなぐ印（外部キー）が `clean_timetable_id` に張られていないため、Prisma の `include`（関連する表を一緒に読む機能）が使えず、**別々に読んでから手で突き合わせて**います（`:710-718`。TeamWorks に追加を依頼中。D-094）。

**変えると何が起きるか**: `:765` の問い合わせから `precheck: CLEAN_TIMETABLE_PRECHECK.main`（本清掃だけ）を外すと、事前チェックの行まで一覧に並びます。例外にはなりません（`:912-913` のコメント）。

### 2.4 ★1行の値は、どのテーブルのどの列から来るか★

清掃詳細ログの1行は「**清掃の予定1件**」です。**予定**は `clean_timetables`（★と `clean_timetable_histories`。下の注★）、**結果（実績）**は `clean_task_reports`、**事前チェックで見つかった異常**は `robot_error_reports` にあり、それを `clean_timetable_id` という番号で突き合わせて1行にします。

#### 突き合わせ方（4ステップ）

| 段 | 読むテーブル | 条件 | 場所 | 根拠 |
|---|---|---|---|---|
| ① 骨格 | `clean_timetables` | `precheck = 0`（本清掃）・`start_at` が期間内・施設ID | `lib/db/index.ts:905-942` | 1回の清掃は `clean_timetables` の3行（本清掃＋60分前＋30分前の事前チェック）で表される（D-092 項1 / D-112）。`precheck` は「何分前か」の数字そのもので、0 だけが本清掃（TeamWorks 第7回 A5） |
| ② 事前チェック行 | `clean_timetables` | `parent_id` ＝ ①の id | `:831-837` | 事前チェック行は `parent_id` に本清掃の id を持つ（D-092 項1） |
| ③ 結果 | `clean_task_reports` | `clean_timetable_id` ＝ ①の id | `:850-854` | 清掃実績の取得元は `clean_task_reports` に一本化（D-151。[CLAUDE.md](../CLAUDE.md) §4.7） |
| ④ 事前チェックの異常 | `robot_error_reports` | `clean_timetable_id` ＝ ②の id | `:857-861` | 事前チェック行に紐づくエラーが「1h前ST / 30分前ST」になる（D-065 / D-098） |

★**注（2026-10-05 改訂。指示書AU 段階3b / D-158 項4）**★: ①②の予定は ★`clean_timetables` と `clean_timetable_histories`（古い予定が移る履歴の表）の両方★から読みます。
読み口は ★`lib/db/timetable-source.ts` の `findTimetables()` / `findTimetablesByChunks()` / `findTimetableById()` の1か所だけ★で、
★同じ `where` を2表に渡し、同じ id は `clean_timetables` 側を採ります★（履歴の id は本表の id のコピー。ユーザー回答 2026-10-05）。
同じ id で `facility_id` / `section_id` / `start_at` が2表で違えば ★warn ログ `db.timetable_conflict`★ を出します（採るのは本表のまま）。
★③の実績は `clean_task_reports` だけのまま★です（D-151）。④の `robot_error_reports` は ★`lib/db/robot-error-select.ts` の `select` で読む列を絞り、`robot_id` / `robot` を読みません★（先方の DDL で `robot_id` が NULL 可になったため。第11回 P-8 b）。

③で1つの予定に結果の行が2行以上あった場合は、**`ended_at`（終了時刻。空なら `started_at`）が最も新しい1行**を採ります。この規則を知っているのは `latestReportByTimetableId()`（`lib/db/clean-log-mapping.ts:241`）だけで、アラート画面とも共有しています（D-107 / 指示書AQ §1-4）。

ロボット（機種・シリアル）は、③の `clean_task_reports.robot_id` からではなく、**①の予定の清掃エリア → そのエリアのロボット**で引いています（`:793-799`。`section.robots[0]`）。P1 の前提が「1清掃エリア＝1ロボット」だからです（[CLAUDE.md](../CLAUDE.md) §8）。ロボットが0台なら静かに空欄にせず例外を投げます（`:923-928`）。

#### 画面の列 → API の項目 → テーブル.列

| 画面の列（`CleanLogTable.tsx`） | API の項目（`CleanLogItem`） | どのテーブルのどの列か | どう決まるか | 根拠 |
|---|---|---|---|---|
| 施設名 | `facility_name` | `facilities.facility_name` | ①の `facility_id` で `facilities` を一緒に読む（`include: { facility: true }`。`:791-792`） | — |
| 機種 | `model_name` | `devices.maker_id` ＋ `devices.model_name` | ①の `section_id` → `sections` → `robots`（`deleted_at` が空のもの）の1台目 → `devices`。整形は `formatModelName()`（`clean-log-mapping.ts:27`）。メーカー番号→名前は `MAKER_NAMES` | `MAKER_NAMES` の番号割り当ては DFA の暫定決定（D-085。makers テーブルは提供されない＝D-057 A-1） |
| シリアル | `serial_number` | `robots.serial_number` | 同上 | 1清掃エリア＝1ロボット（[CLAUDE.md](../CLAUDE.md) §8） |
| 日付・予定 | `scheduled_at` | `clean_timetables.start_at` | `jstIsoFromDb()` で ISO 形式にする（★9時間足さない★） | DB の時刻は列の型によらず JST の壁時計の値（[CLAUDE.md](../CLAUDE.md) §3 / D-155 項4） |
| 1h前ST / 30分前ST | `status_1h` / `status_30m` | `robot_error_reports.error_type` | ★②の事前チェック行（`precheck` の生値）を分数の大きい順に並べ、1つ目が `status_1h`（1回目）・2つ目が `status_30m`（2回目）★（D-178。2026-10-16。分数は `status_1h_minutes` / `status_30m_minutes` で返す。1つだけなら1回目に入り2回目は `null`。3つ以上なら開始に近い2つを残し、捨てた分数は `data.anomaly`）。★旧: `precheck = 60` が「1h前」・`= 30` が「30分前」（`PRECHECK_MINUTES_BEFORE` と一致する行だけ）★。複数あれば `errored_at` が最新の1件。予定行があってエラーが無ければ `Standby.`。★**予定行そのものが無ければ `null`**（画面は「-」、CSV は空のセル）★（`statusColumnsFromPrecheckRows()` `lib/db/precheck-mapping.ts:187`。`f2b91fc`）。★D-171（2026-10-07）から★。旧: 予定行が無くても `Standby.` で、異常なしと見分けられなかった | 値は変換せずそのまま出す（D-077 / [CLAUDE.md](../CLAUDE.md) §4.8）。`Standby.` は TeamWorks の `robots.status` コード表の原文（D-098）。★`robots.status` を実際に読んでいるのではなく、エラーが無いことの読み替え★。★判定には使わない★（D-170） |
| 出発 | `departed` | `clean_task_reports` に③の行が**あるか** | 行があれば出発、無ければ「出発失敗」（`notExecutedFromReport()` `clean-log-mapping.ts:165`） | ★未実施 ＝ 本清掃に対応する `clean_task_reports` の行が無いこと★。「出発失敗」と「未実施」は同じ扱い（[CLAUDE.md](../CLAUDE.md) §4.7。ユーザー確認 2026-09-30。D-107 設計3-C） |
| 清掃結果 | `task_status` | `clean_task_reports.status` | 0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了 の**表示名**にする（`formatCleanTaskStatus()` `lib/format/clean-result.ts:61`。`a961829`）。表に無い値（6 など）は数字のまま。行が無ければ「出発失敗」。★結果待ち（`result_pending`）の行は「結果待ち」（D-173）★ | TeamWorks 第10回 N-2 の回答（D-155 項2）。★判定には使わない★ |
| 完了率 | `completion_rate` | `clean_task_reports.clean_area` と `clean_task_reports.task_area` | ★`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て★（`runCompletionRate()` `lib/judge/kpi.ts:166`）。★`task_area = 0` は 0%（判定 ×。第11回 P-5=B。旧: 「-」）★。画面では小数第1位を四捨五入して整数で出す（`displayRate()` `lib/judge/grade.ts:43`） | TeamWorks のサマリと同じ式に合わせた（第10回 N-3 / D-155 項1。★第11回 P-2=A で確定。D-158★）。★`clean_task_reports.clean_ratio` 列は使わない★（先方が「計算式が間違っている」と回答） |
| 判定 | `grade` | （列ではない。上の完了率から計算） | `judgeGrade()`（`lib/judge/grade.ts`）。★丸める前の値で判定★ | ◎90%以上 / ○70%以上 / △10%以上 / ×10%未満または未実施（[CLAUDE.md](../CLAUDE.md) §4.1 / D-139） |
| （行を開いたときのアドバイス欄）連続日数・L1〜L3 | `consecutive_problem_days` / `escalation_level` | `clean_timetables`（本清掃）＋ `clean_task_reports` | `loadConsecutiveInputs()`（`lib/db/consecutive-source.ts:388`）が**表示期間より前まで遡って**読み、`computeConsecutiveProblemDays()` が数える。表示上限（既定30日）は API の出口でかける（`capCleanLogs()`） | 連続は「スケジュール存在日」で数える（[CLAUDE.md](../CLAUDE.md) §4.5 / D-026）。4経路で同じ材料を使う（D-109） |

計算の式とその根拠の詳しい説明は[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)にあります。

#### この画面に**出ない**もの

| 出ないもの | 理由 | 根拠 |
|---|---|---|
| ~~★予定外のタスク★~~ ★（2026-10-29 から出る。D-187）★ | 旧: ①の予定から引くので、予定を持たない清掃は③の `in` の条件に一致せず出なかった。★今は `loadUnscheduledCleanLogRows()`（`lib/db/unscheduled-clean-log-source.ts`）で `clean_timetable_id` が 0 か NULL の行を別に読み、1件1行で足す（期間は `started_at`）★ | D-152 3-3・D-151 項7 → **D-187**（指示書BG） |
| まだ予定時刻が来ていない清掃 | `isFutureSchedule()`（`clean-log-mapping.ts:308`）で外します。外さないと、まだレポートが無い未来の清掃が全部「出発失敗」になります | D-107 設計3-C（`lib/db/index.ts:1110-1127` のコメント） |

#### いまステージングで画面を開くとどう見えるか

★**空です。**★ 2026-10-01 の時点で `clean_timetables`（予定）が**0行**、`clean_task_reports`（実績）が2行で、その2行はどちらも `clean_timetable_id` が NULL（予定外）でした（2026-10-01 実測・先方投入。[CLAUDE.md](../CLAUDE.md) §3）。予定が0行なので①が何も返さず、画面は「該当なし」になります。★不具合ではありません★。
それ以降は再実測できていません（2026-10-02 は認証エラー、2026-10-05 時点もログインできない件を別途問い合わせ中。D-155 / D-157）。

**変えると何が起きるか**: 完了率を `clean_task_reports.clean_ratio` 列に戻すときは、`runCompletionRate()` の中身だけを直します。呼び出し側は行をそのまま渡しているので、1関数で全画面が切り替わります（指示書AS D6）。呼び出し側で割り算を書くと、画面ごとに式が分かれます。

### 2.5 なぜ画面が直接DBを読まないのか

「画面から直接データベースを読めば、③と④を省けるのでは」と思えます。省けません。理由は4つです。

1. **ブラウザからDBは見えない。** 画面のコードは利用者のパソコンの中で動きます。そこからDBに接続するということは、DBのパスワードを全利用者に配ることです
2. **判定を1箇所に置けなくなる。** ◎○△× や L1〜L3 は `lib/judge/` の関数が決めます（[CLAUDE.md](../CLAUDE.md) §9.4）。画面がDBを直接読むと、画面ごとに計算が生まれ、**同じ施設の同じ日なのに画面によって判定が違う**という事故が起きます。これは実際に起きています（D-109 / D-103）
3. **サンプルデータへの切り替えができなくなる。** `USE_FIXTURES` の分岐が成立するのは、データの取り出し口が `lib/db/` の1箇所だからです
4. **責任分界。** DFA が作るのはビューアーだけで、DBは TeamWorks の持ち物です（[CLAUDE.md](../CLAUDE.md) §2 / D-143）。読み取り口を1箇所に閉じておかないと、「どのコードがどの列を読んでいるか」を誰も答えられなくなります

### 2.6 1画面＝4つのファイル（役割分担表）

D-Watch の画面はすべてこの形です。新しい画面を読むときも、この4枚を探せば足ります。

| 役割 | 置き場所 | 例（清掃詳細ログ） | 書いてよいこと | 書いてはいけないこと |
|---|---|---|---|---|
| **札**（URL を作る） | `app/(dashboard)/<画面>/page.tsx` | `detail-log/page.tsx` | 画面部品を1つ呼ぶだけ | ロジック・通信 |
| **画面** | `components/features/<画面>/` | `DetailLogScreen.tsx` | 入力の保持、表示の整形、`apiFetch()` 呼び出し | 判定の再計算（◎○△×、連続日数、並び順はAPIから受け取る） |
| **入口** | `app/api/<資源>/route.ts` | `clean-logs/route.ts` | 検査・絞り込み・ページング・エラー整形 | SQL、Prisma の直接呼び出し |
| **取り出し口** | `lib/db/index.ts` ＋ `lib/<資源>/query.ts` | `db.cleanLogs()` / `lib/clean-logs/query.ts` | DBの読み取り、絞り込み規則の関数 | 画面の都合（表示上限など）の混入 |

**変えると何が起きるか**: 画面（2段目）で ◎○△× を計算し直すと、API が返した判定と画面に出る判定が食い違います。[CLAUDE.md](../CLAUDE.md) §9.6 が明示的に禁止している項目です。逆に表示上限（連続日数の頭打ち）を `lib/db/` の中に入れると、`USE_FIXTURES` の true と false で画面の見え方が変わります（`lib/clean-logs/query.ts:225-226` のコメント）。


---

## 3. 画面カタログ

D-Watch には**13画面**あります。`app/(dashboard)/` の下に11、それに `/login` と `/`（入口）を足した数です。
★改訂3（2026-10-06）でエラー一覧（`/errors`。3.5b）が増え、**14画面**（`(dashboard)` の下に12）になりました★。

ここでは「どのファイルを開けばその画面に届くか」と「**その画面の数字がどのテーブルから来るか**」を書きます。**各画面が何を表示するかの機能仕様は [docs/spec.md](spec.md) が正です。** 数字の式と根拠の詳細は[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)にあります。

各画面は同じ6項目で書いています — **1** 業務の役割 / **2** URL と札のファイル / **3** 画面のファイル / **4** 呼ぶAPI / **5** この画面の数字はどこから / **6** 固有の注意点。

### 3.0 全画面が共有する器 — `app/(dashboard)/layout.tsx`

11画面に共通のサイドバー・ロゴ・テーマ切替・JP/EN切替を着せているのが、この9行のファイルです。

```tsx
import { AppShell } from "@/components/layout/AppShell";

export default function DashboardGroupLayout({ children }: { children: React.ReactNode }) {
  return <AppShell>{children}</AppShell>;
}
```
（`app/(dashboard)/layout.tsx:1-9`）

やっていることは1つだけ — **各画面の中身を `AppShell` で包む**ことです。`AppShell`（`components/layout/AppShell.tsx`、165行、本体は `:55`）が実際のサイドバーを描きます。

サイドバーのメニューは3つの定数に直接書かれています。

| 定数 | 行 | 中身 |
|---|---|---|
| `MENU_MONITORING` | `:8-26`（`f02eb1d`） | 清掃詳細ログ / アラート / ★エラー一覧（10/06）★ / レポート / 清掃面積（★点検記録は `:25` でコメントアウト★） |
| `MENU_DS` | `:25-28` | D-Secログ / D-Secアラート |
| `MENU_SETTINGS` | `:30-34` | 施設ロボットマスタ / スケジュール / システム管理 |

ダッシュボードだけは区分に属さず、単独で先頭に置かれています（`:127-131`）。

★**契約フラグ（`facilities.contract`）によるメニューの出し分けは、現在も未実装です**★（2026-10-05 に `AppShell.tsx` を確認）。[CLAUDE.md](../CLAUDE.md) §4.6 は「契約のないセクションはメニューごと非表示」と定めていますが、メニューは `as const` の固定配列で、`AppShell.tsx` は `contract` も `monitoring_enabled` も `d_security_enabled` も参照していません。契約フラグを使っているのは**施設ロボットマスタ画面の表示列だけ**です（`FacilitiesScreen.tsx:179` `:186`）。つまり**現在は D-Security 未契約の施設しか無い顧客でも D-Sec メニューが見えます**。
★**P2（顧客に使わせる前）で作ると決めました**★（ユーザー判断 2026-10-06。D-160 追記4）。★エラー一覧のメニューも常に出しています★（指示書AW D15）。
なお P1 の利用者は DFA 社内だけで（[CLAUDE.md](../CLAUDE.md) §1）、ログインは顧客ごとに分かれていません。誰の契約でメニューを出し分けるのかも未決です（未確認）。

**変えると何が起きるか**: `(dashboard)` の括弧を外すと11画面すべての URL が変わります。`layout.tsx` を消すとサイドバーが全画面から消えます。契約フラグの分岐を足すときは、桁位置のハードコードを避け `lib/format/contract.ts`（桁位置は `CONTRACT_DIGITS`）を通してください（[CLAUDE.md](../CLAUDE.md) §4.6 / D-080）。★`contract` を数値として比較すると、例外もテスト失敗も出ないまま全施設が「契約なし」になりメニューが全部消えます★。

---

### 3.1 入口（`/`）

1. ブラウザでドメインだけを叩いた人を、ダッシュボードへ送り出すだけの画面です。表示物はありません
2. `/` ／ `app/page.tsx`（全5行）
3. なし（`redirect("/dashboard")` を呼ぶだけ。`:4`）
4. なし
5. 数字なし
6. この画面にも認証はかかりますが、リダイレクト先で改めて判定されます

**変えると何が起きるか**: 行き先を変えると、ブックマークやメール中のリンクの着地点が変わります。

---

### 3.2 ログイン（`/login`）

1. 共有トークンを入力して中に入るための画面です。P1 は社内利用のみの簡易トークン認証です（[CLAUDE.md](../CLAUDE.md) §3）
2. `/login` ／ `app/login/page.tsx`（142行）
3. 同ファイル内の `LoginForm`（`:21`）。★`AppShell` は使いません★ — 未認証の画面にメニューを出さないためです（`:11`）
4. `POST /api/auth/login`（`:34`）。★この画面だけ `apiFetch()` ではなく素の `fetch` を使っています★
5. 数字なし
6. 固有の注意点:
   - ★認証はかからないが、IP制限はかかる★（`:4-5`）。許可されないIPからは 403 になります（ただし許可リストは空のままで、空なら全部通す設定です。リリース直前の対象として [CLAUDE.md](../CLAUDE.md) §3 に列挙済み。D-154）
   - ★入力値をログに出さない★（`:7`。`console.log` も `no-console` で lint が落ちます）
   - 失敗時のメッセージは★状態コードで分けます★（401 トークンが違う / 422 空 / 403 IP / 429 回数 / それ以外はサーバー側の失敗。`loginErrorMessage()` `lib/auth/login-error-message.ts`・`:47`。D-183）。★トークンの中身に関わることは言いません★。画面の文言は `t()` を通します（ログイン画面も `ThemeProvider` の中）
   - 成功したら入力欄を即座に空にします（`:41`）
   - 戻り先は `safeNextPath()` を通します（`:42`）。★絶対URLを自前で組まない★（`:8-9`。LB が HTTPS を終端するので `http://` になります）

**変えると何が起きるか**: エラー文言を「トークンが短すぎます」のように具体化すると、総当たりの手がかりを与えます。

---

### 3.3 ダッシュボード（`/dashboard`）

1. 朝イチで開く画面です。★各清掃エリアの直近の清掃（7日以内）の結果のサマリ★、今日のスケジュール進捗、要レスキュー店舗、セキュリティの状況が1画面に並びます（★見出しは「直近の清掃結果サマリー（各清掃エリアの最後の清掃・7日以内）」。D-159 でユーザーが変えた。モックの「昨夜の清掃結果サマリー」とは違う★）
2. `/dashboard` ／ `app/(dashboard)/dashboard/page.tsx`
3. `components/features/dashboard/DashboardScreen.tsx`（★369行。`50a3082`★）。子部品3つ: `RescueRequiredCard.tsx`（165行）/ `ScheduleProgressCard.tsx`（32行）/ `SecurityBanner.tsx`（27行）。★清掃ログの表とエリアモーダルは清掃詳細ログから借りています★（`AreaModal` / `CleanLogTable` を import。`:6-7`）
4. ★`GET /api/dashboard`（`:87`）の1本で数値カードとコンパクト版の表の両方★、行を開いたとき `GET /api/clean-logs/{id}/advice`（`:129`）、`GET /api/clean-logs/{id}/areas`（`:173`）。★初期表示で `GET /api/clean-logs` は呼びません★（D-159 / 指示書AV D7。旧: 表のために別に呼んでいた）
5. **この画面の数字はどこから**（詳細は章5-B）:
   - 清掃結果サマリ（成功・失敗・未実施・★結果待ち★）… ★清掃エリアごとに、予定の終了 ≤ 現在の本清掃のうち一番新しい1件（7日以内）★を選び（`pickLatestRunPerSection()` `lib/dashboard/latest-run.ts:69`。`a961829`）、判定で数えたもの（`computeSummary()` `lib/dashboard/aggregate.ts:41`）。★出発失敗も選ぶ（未実施に数える）★。★結果待ち（`result_pending`）も選び、「結果待ち」カードに数える★（D-173）。`?date=` を指定したときは従来どおりその日の清掃（D-159）
     - ★「結果待ち」カード★（D-173。`DashboardScreen.tsx:263-271`。色は中立の `tone="mt"`（`StatCard` に D-173 で足した。新しい色は足さない）。副題「レポートの到着待ち」。★モックに無いカード★）。★成功率の分母に入れません★（`computeSummary()` の `pending`）。★「成功 / N件」の N には入れる★（カード5枚の合計が N。`:194-207`）ので、成功 ÷ N と成功率は一致しないことがあります
     - ★旧（10/06〜10/07 朝）: 「予定の終了＋30分（`DASHBOARD_LATEST_RUN.settleMinutes`）≤ 現在」の予定だけを候補にしていた★。D-173 で `settleMinutes` を削除し、終わってからレポートが届くまでは行の旗 `result_pending` で「結果待ち」と出すようにしました（[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)）
   - コンパクト版の表 … 同じ母集団の △× の行（レスポンスの `problem_logs`）の★先頭5件★（`COMPACT_LOG_ROWS` `DashboardScreen.tsx:50`）。★API は件数で切りません★。★結果待ちの行は出しません★（`grade` は `×` のままなので旗で外す。`app/api/dashboard/route.ts:222`。D-173）
   - スケジュール進捗の**予定** … `clean_schedules.weeks`（7桁）のうち今日の曜日が `1` で、`inactive = 0` の件数。**実施** … 当日の清掃のうち `clean_task_reports` の行があるもの（`computeScheduleProgress()` `:101`。★結果待ちは「実施」に数えない（出発していない）★）。★予定と実施が別のテーブルから来る★ので、件数が合わないときはログに `data.anomaly` を出します（`app/api/dashboard/route.ts:138-150`。D-049）
   - 要レスキュー … `clean_timetables` ＋ `clean_task_reports` を遡って読んだもの（`loadConsecutiveInputs()`）と施設名（`facilities`）
   - セキュリティバナー … 取得元は `dsAlerts()`（`robot_error_reports` の `D-S_CONTROL_FAILURE`）。★`security_off` は DB から出ないので実データでは常に空（D-180 項7。コードは残す — U7）★（`lib/db/index.ts:2065-2085` / `lib/dashboard/aggregate.ts:131-141`）
   - 成功率カードの「目標: 95%」… `SUCCESS_RATE_TARGET`（`lib/config/index.ts:577`。表示だけ）
6. 固有の注意点:
   - ★ページ送りはありません★
   - 要レスキューは初期表示 **5件**（`RESCUE_INITIAL_VISIBLE`）。「もっと表示」で全件開きます。★API は件数で切りません★（D-137）
   - ★エリアモーダルの `logId` は「エリアを特定するキー」ではなく、施設と日付を特定するための入口★です（`:31-33`）
   - ★いまステージングで開くと、予定（`clean_timetables` / `clean_schedules`）が0行なので、ほぼ全部が0か空です★（2026-10-01 実測・先方投入。[CLAUDE.md](../CLAUDE.md) §3）。
     ★2026-10-06 15:0x JST は直近モードで11エリア・成功9・失敗1・未実施1（先方投入。追記61）★。`?date=当日` は0件（実データの清掃は前日の日付のため）
   - ★数値カード（直近モード）とスケジュール進捗・要レスキュー（当日）は母集団が違います★（D-159。指示書AV 4-1）
   - ★成功・失敗・未実施に事前チェックは関係しません★（D-170）。数えるのは `departed` と `grade` だけです（[5.9](#59-ダッシュボードの集計--libdashboardaggregatets123行)）
   - ★縮小表示の表（`CleanLogTable` の `compact`）は施設名 240px を持ち、余りの幅を5列に幅の比で配ります★（最小幅 1000px。D-167 追記。旧: 施設名だけが余りを受け持ち 1920px で ≒833px）。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)

**変えると何が起きるか**: `CleanLogTable.tsx` を直すと、清掃詳細ログとダッシュボードの**両方**の表が変わります。★列幅は `CleanLogTable.module.css` の `.table`（通常）と `.compact`（縮小）で別々に持っています★。

---

### 3.4 清掃詳細ログ（`/detail-log`）

1. 1回ごとの清掃を一覧で確認する画面です。D-Watch の中心で、[章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで)で1本通して追っています
2. `/detail-log` ／ `app/(dashboard)/detail-log/page.tsx`
3. `components/features/detail-log/DetailLogScreen.tsx`（★587行。`50a3082`★）。子部品: `CleanLogTable.tsx`（★394行。`50a3082`★。表）/ `AreaModal.tsx`（185行。エリア別完了率のモーダル）
4. `GET /api/clean-logs`（`:205`）、`GET /api/clean-logs/export`（CSV。`:516` のリンク）、`GET /api/clean-logs/{id}/advice`（`:253`）、`GET /api/clean-logs/{id}/areas`（`:297`）（★行番号は `f2b91fc`★）
5. **この画面の数字はどこから**: 1行＝`clean_timetables`（★と `clean_timetable_histories`。2026-10-05 から★）の本清掃1件、★または予定外のタスク（`clean_task_reports` の `clean_timetable_id` が NULL か 0）1件（2026-10-29 から。D-187）★。結果は `clean_task_reports`（`clean_timetable_id` で突き合わせ）、事前チェックの状態は `robot_error_reports.error_type`。★完了率は `clean_task_reports.clean_area ÷ task_area`（上限100・小数第1位切り捨て）★で、`clean_ratio` 列は使いません（D-155。★第11回 P-2=A で確定。D-158★）。列ごとの対応表は[章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで)の 2.4。エリア別完了率のモーダルも同じ `clean_task_reports` の面積と `sections.section_name` から作ります（`lib/db/index.ts:1358`）
6. 固有の注意点:
   - ★ページ送りあり★
   - ★日付範囲の上限は **731日（2年）**★（`MAX_DATE_RANGE_DAYS`）。超えると画面側で止めます（`dateRangeError()` `:165`。★両方入っているときだけ★）。★初期表示は期間を指定しない（全期間）★（D-159 追記。旧: 今日1日）。★片方だけ入れたら入っている側だけを送ります★（5b 以前と同じ挙動。`lib/clean-logs/detail-log-date-range.ts`）。★期間なしの取得は Prisma 版で保持期間の予定を全部読むので、リリース前に見直す★
   - ★CSV は **20,000行**を超えると出せません★（`MAX_CSV_ROWS`）。判定は一覧で取った `meta.total` を流用し、CSV のために問い合わせを増やしません（`:167-181`）。★画面の判定とサーバーの判定は同じ向き（`>` であって `>=` ではない）に揃えること★ — 片方だけ `>=` にすると、押せるのに 422 が返ります
   - CSV はリンク（`<a href>`）で、`apiFetch()` を通しません（`:516`）。ファイルのダウンロードだからです
   - ★CSV の完了率は、値が無い（未実施）ときは空のセルです★（`csvRate()` `lib/csv.ts:69`。D-164。旧: `-` を出し、数式対策で Excel に `'-` と見えた）。★画面の表示は「-」のまま★
   - ★事前チェックの予定が無い清掃の「1h前ST / 30分前ST」は「-」（Pill を出さない・色なし）、CSV では空のセルです★（D-171。2026-10-07）。分岐は `precheckStatusCell()`（`lib/format/precheck-status.ts:22`）の1か所で、画面は `PrecheckStatus`（`CleanLogTable.tsx:74`）がそれを使います。★`statusColor()` に `null` を渡さないため★。予定があって異常なしは今までどおり `Standby.` の Pill。★表示だけで、判定には使いません★（D-170）
   - この画面に「検索」ボタンはありません（`:55-60`）。文字入力だけ 0.3 秒待ってからまとめて投げます
   - ★期間の入力欄は枠のどこを押してもカレンダーが開きます★（`openDatePicker()`。D-162）。★ホールディングス・顧客のプルダウンは 180px 固定★（`.masterSelect`。D-162）。★表は全列に px の幅を付け、余りを全列に幅の比で配ります★（施設名 110px・最小幅 1134px。D-167 / D-180 項10）。3つとも [3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)
   - ★「清掃結果」列は `clean_task_reports.status` の**表示名**です★（D-155 項2）。0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了。★表に無い値（6 など）は数字のまま★、レポート行が無ければ「出発失敗」（★ただし結果待ちの行は「結果待ち」。D-173★）。組み立ては `formatCleanTaskStatus()`（`lib/format/clean-result.ts:61`）を `cleanLogRowView()`（`:98`）経由で使い、画面では `t()` に通して英語にも切り替わります（`CleanLogTable.tsx:287`。`a961829`）。★判定には使いません★（中断・異常終了でも完了率で ◎○△× を出す）
   - ★**結果待ち**（D-173）★: 予定の開始を過ぎてレポートがまだ無い清掃のうち、★予定の終了から `RESULT_WAIT_MINUTES`（120分）以内のもの★（清掃中も含む）は、出発「…」・清掃結果「結果待ち」・完了率「-」・判定なしで出し、★行を開けません★（`result_pending`。見え方は `cleanLogRowView()` `lib/format/clean-result.ts:98` の1か所。`CleanLogTable.tsx:212-293`）。★「問題あり（△×）」「×のみ」の絞り込みからは外れ、「全件」にだけ出ます★（`lib/clean-logs/query.ts:186`）。CSV も出発・清掃結果が「結果待ち」、完了率・判定は空のセルです。120分を過ぎたら今までどおり「出発失敗」。詳しくは [5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)
   - ★`task_area` が 0 の清掃は完了率 0%（判定 ×）です★（`runCompletionRate()`。第11回 P-5=B。D-158）。旧（10/02〜10/05）: 出発していても「-」
   - ★予定外のタスク（`clean_timetable_id` が NULL か 0）も1行として出ます★（2026-10-29 から。指示書BG・D-187。旧: 予定が起点のため出なかった）。行の `id` は null・`row_key` は `rp-<report_id>`（`report_id` は19桁の文字列）。予定の列は「予定なし」（D-188）。△× の行の ▼ は `GET /clean-logs/unscheduled/{report_id}/advice`（D-189）。エリア別詳細のボタンは予定のある行だけ
   - ★取得中に絞り込みを変えたら、遅れて届いた古い応答は捨てます★（取得の effect の片付けで `cancelled` にする。エラー一覧と同じ。D-183）。★アドバイスの取得に失敗したら「アドバイスの取得に失敗しました」★（「該当なし」と区別。`adviceFailedIds`）、★予定ありの行では、「エリア別詳細を見る →」はアドバイスの有無・成否によらず出します。予定なしの行には出しません★（指示書BI / D-183）。★CSV は期間の誤り・読み込み中は押せません★（`csvUnavailable`。D-183）。エリア別モーダルの★結果待ちの行は完了率「-」・札の代わりに「結果待ち」・赤字にしない★（`result_pending`。D-182 / D-183）

---

### 3.5 アラート（`/alerts`）

1. 清掃前の事前チェックで見つかった異常と、出発できなかった清掃を一覧し、★事前チェック異常の対応状況と備考を記録する★画面です
2. `/alerts` ／ `app/(dashboard)/alerts/page.tsx`
3. `components/features/alerts/AlertsScreen.tsx`（★700行。`50a3082`★）。共通部品 `FilterButton` / `Pill` / `StatCard` / `Pagination`。★下書きの扱いは `lib/prechecks/edit-drafts.ts`（エラー一覧と共有。D-165）★
4. `GET /api/prechecks`（`:232`）、★`PATCH /api/prechecks/{id}/status`★（`sendPrecheckPatch()` `:123`。2026-10-02 指示書AT 段階6。★10/06 からは「保存」を押したときだけ、変えた行の数だけ呼ぶ★）
5. **この画面の数字はどこから**:
   - 事前チェック異常の行 … `robot_error_reports` のうち、`clean_timetable_id` が事前チェック行（`clean_timetables.precheck` が 0 以外）を指すもの。種別は `error_type` をそのまま（D-077）。対応状況は `robot_error_reports.status`（0 未解消 / 1 対応中 / 2 解消。`lib/db/precheck-mapping.ts:38`。`f2b91fc`）、備考は `comment`、日時は `responded_at` / `resolved_at`、検知日時は `errored_at`（空なら `created_at`。D-105）
   - 出発失敗の行 … `clean_timetables` の本清掃のうち、`clean_task_reports` に対応する行が無いもの（`lib/db/index.ts:1663-1741` あたり。`a961829`。★予定の終了から120分以内でレポートが無い清掃は結果待ちとして出さない（`:1689`。D-173）★。清掃詳細ログの「出発失敗」と同じ判定 `notExecutedFromReport()`）
   - バッテリー残量 … ★いつも空★（`battery_level: null`。`robots.battery` は読んでいない）
   - ★「本清掃は成功」★（D-171） … 親の本清掃に `clean_task_reports` の行があり、完了率の判定が ◎○ なら `main_run_succeeded: true`（`mainRunSucceededFromReport()` `lib/db/precheck-mapping.ts:339`。成功の等級は `SUCCESS_GRADES` `lib/dashboard/aggregate.ts:26` をダッシュボードと共有）。出発失敗の行は常に `false`
6. 固有の注意点:
   - ★ページ送りあり★
   - ★`launch_failed`（出発失敗）だけ赤、事前チェック異常は警告色★（`:50-52`）
   - ★`main_run_succeeded` が true のカードは控えめに出します★（バッジ・施設名・予定・検知・状態の表示を不透明度 0.6 にし、「本清掃は成功」の Pill を足す。`itemCardClass()` `:107-112` / `:592`、CSS は `.itemCardSubdued`）。★対応状況（未解消など）も上の件数も変えません★（本清掃が成功しても未解消は未解消。D-171）。★編集モードのプルダウン・備考は薄くしません★（カード全体に `opacity` をかけない）
   - ★「出発失敗」と「未実施」は同じものです★ — `clean_task_reports` に該当行が無いことが根拠で、区別する列がありません（[CLAUDE.md](../CLAUDE.md) §4.7）
   - この画面にも「検索」ボタンはありません（モック全体に「検索」ボタンが無いため）
   - ★**画面からメインDBに書き込む画面です**★（★10/06 からはエラー一覧（3.5b）も同じ4列を書く。以前はこの画面だけ★）（対応状況と備考。保存先は `robot_error_reports` の4列。[章8](#8-データの出どころ)）。モックにこの操作は無いので、D-Secログの備考・モックの D-Sec アラートの作法を借りています（D-156 項4）
     - ★書けるのは事前チェック異常の行だけ★（`isEditable()` `:115-117`）。出発失敗の行はバッジのままで変えられません（記録先が無い。D-105）。サーバー側も出発失敗の id には 404 を返します
     - ★**編集モード**（10/06 D-165。画面全体で切り替える）★ — 状態は `:182-188` のコメントが正です
       - ★通常モードは表示だけ★: 対応状況は色付きのバッジ、備考は文字だけで、押しても何も起きません
       - 見出しの右の「編集」（`:403`。編集できる行が0件なら押せない。3画面とも同じ位置・同じボタン。D-181）で入ると、対応状況が ★プルダウン（未解消 / 対応中 / 解消。D-161）★、備考が入力欄（上限 `PRECHECK_NOTE_MAX_LENGTH` ＝ 400字。`:610`）になります
       - ★変えた内容はすぐには送らず下書きに貯めます★。同じ値に戻すと下書きから消えます（`setDraftStatus()` / `setDraftNote()`）。変えた行は背景の色と上・右・下の枠（`.itemCardChanged`。D-Sec と同じ。D-181）で分かります
       - ★「保存（N件）」で、変えた行だけを1行ずつ順に PATCH します★（`saveEdits()` `:310` → `saveDrafts()`。並列にしない）。送る本文は ★対応状況は必ず・備考は変えたときだけ★（`patchRequestFromDraft()`）
       - ★全部成功したら通常モードに戻ります。1件でも失敗したら編集モードのまま★、失敗した行は下書きを残して行の下に理由を出し、一覧の上に「{n}件を保存できませんでした」と出ます
       - ★成功した行は返ってきた1行で差し替え、上の件数は `applyResolutionChange()` で差分だけ直します★（一覧を取り直さない）。★「未解消のみ」で絞っていても、次に取り直すまで行は消えません★（押し間違いをその場で戻せるように）
       - ★編集モード中は絞り込みとページ送りを使えません★（`<fieldset disabled>` `:426` / `:690`。取り直すと下書きの行が一覧から消えるため）。★下書きがあるあいだはページを離れる前にブラウザが確認します★（`beforeunload` `:271-278`）。「キャンセル」は下書きがあれば確認してから捨てます（`cancelEditMode()` `:295`）
       - ★保存中は「保存」「キャンセル」とすべての入力欄が押せなくなります★（`saving`。重ねて送らないため）
       - ★D-161 の「プルダウンで選んだら即保存」と指示書AT §3-8 の「備考はフォーカスが外れたら保存」は廃止しました★（`lib/prechecks/status-cycle.ts`（巡回順）は D-161 で削除済み）
     - ★備考は1行で表示し、はみ出す分は「…」で切ります。全文はマウスを乗せると出ます（`title`）★（`.noteText` `:624`。D-162）。★枠は文字の長さによらず行の残りの幅いっぱいの固定の大きさ★（D-164）、編集欄も `resize: none`（D-162）
     - ★解消日時（プルダウン・バッジの下）は 12px★（`.statusTime`。旧 11px。D-162）。★出すのは保存済みが解消で、画面の対応状況（下書き優先）も解消のときだけ★（`showsResolvedTime()` `lib/prechecks/edit-drafts.ts`。3画面で1本。D-181）。★出発失敗の行もバッジ（120px）の下に出す★
     - 対応開始日時・解消日時は画面では入力しません。サーバーが状態に合わせて埋めたり消したりします（`lib/prechecks/status-update.ts:50`）
   - ★期間の入力欄は枠のどこを押してもカレンダーが開き、ホールディングス・顧客のプルダウンは 180px 固定★（D-162。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)）
     - ★書き込んでよいのはステージングだけ★（ユーザー指示 2026-10-01。D-152）。★実データ（ドン・キホーテのエラー行）の対応状況が書き換わることを承知のうえ★です。本番DBにはまだ接続していません
   - ★取得中に絞り込みを変えたら、遅れて届いた古い応答は捨てます★（D-183）。★期間はエラー一覧と同じ検証★（開始 > 終了なら取得せず一覧の場所に誤りを出す・両方空なら「指定なし（全期間）」。`dateRangeError()`。D-183）

---

### 3.5b エラー一覧（`/errors`）— ★改訂3で追加★

1. ★`robot_error_reports` の全行★（ロボット・制御盤・セキュリティ・清掃開始失敗。事前チェックかどうかを問わない。★削除済み（`deleted_at` が NULL でない）の施設・清掃エリアの行は除く — `meta.total` にも数えない（U8。D-180 項11。`isVisibleErrorRow()`）★）のうち、★未解消・対応中★のものを一覧し、★その場で対応状況と備考を記録する★画面です（D-160）。「解消済みも表示」で解消も出ます。★アラート画面（3.5）は事前チェック＋出発失敗だけのまま★です
2. `/errors` ／ `app/(dashboard)/errors/page.tsx`（★モックに無い画面★。メニューは「🔔 アラート」の直後の「🚨 エラー一覧」。`AppShell.tsx:13`）
3. `components/features/errors/ErrorsScreen.tsx` ＋ `ErrorsScreen.module.css`。子部品 `ErrorContentPicker.tsx`（この画面専用の選択窓。共通部品にはしない。指示書BE）。共通部品 `Pagination`。★対応状況・備考の下書きの規則は `lib/prechecks/edit-drafts.ts` と揃えるが、エラー一覧は `lib/errors/edit-drafts.ts` を使う★（`error_list_id` を落とさないため。アラートと D-Sec は共通側のまま。D-165 / 指示書BE）
4. `GET /api/errors`（`apiFetch` の `/api/errors`）、★`GET /api/error-lists`★（編集に入ったとき1回）、★`PATCH /api/errors/{error_id}/status`★（`sendErrorUpdate()`。★「保存」を押したときだけ、変えた行の数だけ呼ぶ★。エラー内容を変えた行は `error_list_id` も送る）
5. **この画面の数字はどこから**（詳細は章5-B の 5B-6b）:
   - 1行 … `robot_error_reports` の1行。★種類（`error_type`）・レベル（`error_level`）・シリアル（`serial_number`）は生値のまま★（`t()` にも通さない。D-077）。シリアル「0」もそのまま
   - 検知時刻 … `errored_at`、空なら `created_at`（`/alerts` と同じ `detectedAtFromError()`）
   - 紐づく予定 … `clean_timetable_id` を2表（本表・履歴）から引いて ★本清掃 / 事前チェック（N分前）/ 予定なし / 予定不明★（`errorScheduleLink()` `lib/db/error-report-mapping.ts:134`、文言は `errorScheduleLabel()` `lib/error-reports/label.ts:27`）
   - 件数「全{n}件」 … 絞り込み後の件数（`meta.total`）
6. 固有の注意点:
   - ★ページ送りあり★。並びは ★検知時刻の降順、同時刻は `error_id` の昇順（文字列の辞書順）★
   - ★2段階で読みます★（キーだけを SQL で絞って全件読み → アプリで絞り込み・並べ替え・ページ分け → そのページの行だけ読み直す。8.7 の24）。★`errored_at ?? created_at` の順を Prisma のクエリビルダで書けないため★（`nulls last` で近似すると `errored_at` が NULL の行が末尾に回る。D-160 追記2）。★「解消済みも表示」は保持期間ぶんのキーを読むので、リリース前に性能を見直す★
   - 絞り込みは期間（★検知時刻の日付。初期値は空＝全期間★）・分類・施設名・種類（★部分一致・大小文字を区別しない。アプリ側★）・「解消済みも表示」。「検索」ボタンは無く、どれを変えても1ページ目に戻ります
   - ★対応状況・備考・エラー内容は**編集モード**でだけ変えられます★（D-165 / 指示書BE。★対応状況と備考の動きは `/alerts`（3.5）と同じ★。エラー内容はエラー一覧だけ）。通常は表示だけ → 見出しの右（件数の右）の「編集」（読み込み中・0件・エラー表示中は押せない。`canStartEdit`）で入る → 対応状況はプルダウン（D-161。解消のときは下に解消日時（下書きで未解消・対応中に戻すと隠れる。D-181））、備考は入力欄、詳細の列はエラー内容（10/27 に種類の2行目から独立した列へ。長い文字は折り返す。指示書BE §12。選べる行はボタン → `ErrorContentPicker`）→ ★「保存（N件）」で変えた行だけを1行ずつ順に PATCH★（`saveEdit()` → `saveErrorDrafts()`）→ ★全部成功で通常モードに戻る・失敗した行は下書きを残して行の下に理由★。★その他（`note_required`）で備考が空の行は送らず、同じ件数に数える★。★編集中は絞り込みとページ送りを使えません★（`filtersDisabled`）。★下書きがあればページを離れる前に確認★（`beforeunload`）、「キャンセル」も確認してから捨てます（`cancelEdit()`）。★日時の規則は `/alerts` と同じ★。書けるのは4列に `error_list_id` を足した5列（[章8](#8-データの出どころ) 8.12。D-184）。★保存に成功した行は返ってきた1行で差し替え、解消にした行も取り直すまで消えません★
   - ★備考は1行で表示し、はみ出す分は「…」。全文は `title`（マウスを乗せる）★（`:635`。D-162）。★枠は列幅いっぱいの固定の大きさ★（D-164。旧: 入力した文字の長さの幅しかなかった）。編集欄も `resize: none`。★解消日時は 12px★（`.statusTime`。D-162）
   - ★書けるのは `robot_error_reports` に行があるものすべて★（事前チェック以外・`robot_id` が NULL の行も）。サーバー側は行が無い id・大小文字だけ違う id・★削除済みの施設・清掃エリアの行（U8。D-180 項11）★に404
   - ★表は `table-layout: fixed`＋`<colgroup>`（`:611`）で JP/EN の切り替えで列幅が動きません★。★列幅は D-166 → 2026-10-27 に「詳細」の列を足して改めた（指示書BE §12）★: 検知時刻 120 / シリアル 140 / レベル 90 / 紐づく予定 110 / 対応状況 150（中のバッジ・プルダウンは 120px 固定。D-181） / 備考 223px を固定し、★施設名は表の幅の 9%・種類は 12%、詳細は残りの幅すべて（最小 60px）★。★表の最小幅 ≒1130px★（(833 + 60) ÷ 0.79。旧 1132・1129・1156px。狭い画面では表だけ横スクロール。★割合を変えたら 0.79 も直す★。2026-10-27 の4回の手直しの経緯は CSS のコメント）。値は `ErrorsScreen.module.css` の `.table`。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)。ただし★ツールバーは JP で「解消済みも表示」が2行目に回ります★（EN は1行）
   - ★期間の入力欄は枠のどこを押してもカレンダーが開き、ホールディングス・顧客のプルダウンは 180px 固定★（D-162）
   - ★i18n に「備考」→ "Note" を足したので、D-Secログの備考列の見出しも EN で "NOTE" になりました★（辞書は全画面で共有。7-8。★`f2b91fc` 時点でもそのまま★）
   - ★契約によるメニューの出し分けはありません★（全メニューと同じ。3.0）

---

### 3.6 レポート（`/reports`）

1. 週・月・年の単位で KPI（稼働率・出発成功率・清掃完了率・エラー発生率）と前期比、施設別の成績、連続して問題が出ている施設、エラー種別の内訳を見る画面です
2. `/reports` ／ `app/(dashboard)/reports/page.tsx`
3. `components/features/reports/ReportsScreen.tsx`（**861行。全画面で最大**。`50a3082`）。子部品: `KpiCard.tsx`（89行）/ `FacilityPerformanceTable.tsx`（★270行★。「変化」とステータスの表示名は `lib/reports/facility-performance-labels.ts` へ移した。D-163）。共通部品 `ProgressBar`
4. `GET /api/reports/kpi`、`/facility-performance`、`/continuous-problems`、`/error-breakdown` の4本（`:230-245`）。★右上の「CSVエクスポート」は `GET /api/reports/facility-performance/export`（`:391-397` のリンク。D-163）★
5. **この画面の数字はどこから**:
   - ★**確定した期間**（先週・先月・昨年など）★ … TeamWorks のサマリテーブル。KPI は `customer_summaries`、施設別は `facility_summaries` の★率4列（`operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio`）をそのまま★、エラー内訳は `facility_error_summaries.error_type` ごとの `error_count` の合計（[CLAUDE.md](../CLAUDE.md) §4.7。指示書AQ）
   - ★**進行中の期間**（今週・今月・今年）★ … サマリにまだ行が無いので、`clean_timetables`（本清掃）＋ `clean_task_reports` ＋ `robot_error_reports` からその場で集計します（[CLAUDE.md](../CLAUDE.md) §4.7）。完了率は1回ごとの完了率の単純平均（D-155）
   - 連続問題施設 … `clean_timetables` ＋ `clean_task_reports`（`loadConsecutiveInputs()`）
   - ★サマリには実施回数の列が無いので、稼働率は検算できません★（[章8](#8-データの出どころ) 8.3）
6. 固有の注意点:
   - ★ページ送りはありません★。代わりに表ごとに「もっと表示」で `limit` を 10（`DEFAULT_LIMIT` `:46`）から 200（`SHOW_ALL_LIMIT` `:55`）に広げます
   - ★「CSVエクスポート」は施設別パフォーマンスの表を CSV で出します★（D-163。★2026-09-07〜10-06 は押しても何も起きない仮の実装だった★。D-069 項8）
     - ★条件は表と同じ（期間・分類・施設名・シリアル・ステータス）で、`limit` を付けません★ — 表示中の10件ではなく、★絞り込み後の全施設★を出します（画面の「全施設表示」の200件上限にも縛られない）。並びも表と同じ稼働率ワースト順
     - ★表と CSV のクエリ文字列は同じ1か所から作ります★（`facilityFilterQuery` → 表は `limit` を足す、CSV の `exportQuery` はそのまま。`:189-211`）。★サーバー側も表と CSV で同じ1本 `loadFacilityPerformance()` を呼びます★（[章4](#4-apiの入口--appapi-の23本) の 26）
     - リンク（`<a download>`）で、`apiFetch()` を通しません。★0件でも押せます★（見出し行だけの CSV）。列と形式は[章13](#13-用語対応表) 13.2
   - ★4本を `Promise.all` ではなく `Promise.allSettled` で呼びます★（`:225-245`）。1本が失敗しても残り3本は表示するためです。★特に KPI は「対象顧客が1社に定まらない」と 422 になります★（サンプルデータは顧客が4件あるため、開発中は毎回これが起きます）
   - ★`/error-breakdown` だけ `meta` を返しません★（集計結果で全件返すため）
   - ★分類（ホールディングス／顧客）の指定には「確定」の操作があります★ — `confirmedClassification`（`:149`）という別の状態で保持し、選んだ瞬間ではなく確定時に反映されます。清掃詳細ログ（1操作＝1確定）とは違います
   - ★週の数え方は `weekOfMonth()`（`lib/format/period.ts`）だけが知っています★。月をまたぐ週は月境界で分割され、週の長さは1〜7日で変わります（[CLAUDE.md](../CLAUDE.md) §4.7 / D-071）
   - ★年次は解禁済み★です（D-151。D-072 項4 は破棄）
   - ★施設別パフォーマンスの表は施設名 200px を持ち、余りの幅を全列に幅の比で配ります★（最小幅 990 → 1040px。D-167 追記。旧: 施設名だけが余りを受け持ち 1920px で ≒760px）。★ホールディングス・顧客のプルダウンは 180px 固定★（D-162）。どちらも [3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)
   - ★4本の結果にクエリを持たせます★（`ReportsResult`。D-183）。★期間・分類が変わったときだけ画面全体を「読み込み中…」に★し、★「全N施設を表示 →」「他N件を表示 →」・施設の絞り込みだけなら前の内容を薄く残します★（`.cardRefreshing`・`aria-busy`。スクロールが飛ばない）。★全件表示でも上限（200）で切れたら注記を出します★（D-183）。KPI カードの前期比は★丸めて0なら「→」・色なし★（`kpiDeltaView()`。D-183）。連続問題施設の説明の日数は `ESCALATION_THRESHOLDS.level2Days` から作ります

---

### 3.7 清掃面積（`/clean-area`）

2026-09-16 のユーザー指示（指示書AN。D-141）で新設された画面です。

1. **指定した期間に、どれだけの床面積を清掃したかを見る画面です。** ホールディングス / 顧客 / 店舗の3つの粒度で内訳を出せます
2. `/clean-area` ／ `app/(dashboard)/clean-area/page.tsx`
3. `components/features/clean-area/CleanAreaScreen.tsx`（★500行。`50a3082`★）。使う関数は `lib/clean-area/` の4本 — `aggregate.ts`（集計）/ `query.ts`（パラメータ検証）/ `visibility.ts`（何件見せるか）/ `anomaly.ts`（異常値の見張り）
4. `GET /api/clean-area`（`:141`）
5. **この画面の数字はどこから**（★面積と完了率で、数える行が違います★。D-155 項3）:
   - ★面積★ … `clean_task_reports.clean_area` と `task_area` の**合計**（`groupBy`。`lib/db/clean-area-source.ts:383-387`）。★予定外のタスク（`clean_timetable_id` が NULL）も含めます★ — 実際に清掃した広さだからです（ユーザー判断 2026-10-02）
   - ★完了率・実施回数★ … `clean_task_reports` のうち `clean_timetable_id` が空でなく、その予定が本清掃（`clean_timetables.precheck = 0`）の行だけ（`:374-406`）。完了率は1回ごとの完了率（`runCompletionRate()`）の単純平均。TeamWorks のサマリと同じ母集団です（第10回 N-3）
   - 期間の基準は `clean_task_reports.started_at`（`lib/db/index.ts:2337`）。★予定のある清掃では、この列に予定の `clean_timetables.start_at` と同じ値が入る★（ユーザー回答 2026-10-05「A」）ので、★清掃詳細ログ・レポートと同じ日に数えられます★。違うのは予定外のタスクだけで、その場合は実際に清掃を始めた時刻です
   - 顧客・ホールディングスへのまとめ … `facilities` ＋ `customers` ＋ `groups`（`deleted_at` で絞らない。`clean-area-source.ts:472`）
   - ★サマリテーブルの面積（`*_summaries.task_area` / `clean_area`）は読みません★ — 実データで `task_area` が全行 1000 固定で、期間内の合計になっていないため（`clean-area-source.ts:35-38`。第9回 Q2-1 で照会中）
   - 単位「㎡」は `AREA_UNIT`。★DFA の判断であって TeamWorks の確認ではありません★（第9回5章で照会中）
6. 固有の注意点:
   - ★**モックにこの画面はありません**★（`:25-40`）。[CLAUDE.md](../CLAUDE.md) §5 は「モックが画面レイアウト・文言・遷移の正」と定めていますが、★本画面はその例外★です。★この注記を消さないこと★ — 次にモックと突き合わせた人が「モックに無い＝余計なもの」と判断して消してしまいます
   - ★**期間の絞り込みに上限がありません**★（`lib/clean-area/query.ts:14-18`）。清掃詳細ログの 731日上限は持ち込みません。面積の合計は1本の集計で、返る行数は粒度ごとの件数（最大でも施設数）だからです
     - ★ただし 2026-10-02 から、完了率・実施回数のために `clean_task_reports` の行（予定に紐づくもの）を取り寄せています★（1回ごとの完了率は集計クエリで書けず、生SQLも使わないため。指示書AS 段階4）。★期間を指定しないと全期間を運びます★。重さを見るため、取り寄せた行数と所要時間を `judge.clean_area` ログに出します（`app/api/clean-area/route.ts:100-108`。D-156 項2）
   - ★「面積はあるのに実施回数0」の施設は不具合ではありません★。画面にもその旨の注記が1行出ます（`CleanAreaScreen.tsx:377-382`）
   - 期間は `date_from` / `date_to` とも**省略可**（`null` ＝ 上限・下限なし）。`?date_from=` のような空文字も `null` に正規化します（`query.ts:43-47`）
   - ★単位は `AREA_UNIT` から出し、`"㎡"` を直書きしません★（`CleanAreaScreen.tsx:47` `:339-340`）。API は数値だけを返します
   - 内訳の初期表示は **10件**（`CLEAN_AREA_INITIAL_VISIBLE`）。「もっと表示」で全件。★ページ送りはありません★
   - 粒度の既定は ★店舗（`facility`）★（`query.ts:39`）
   - ★表は名前の列（店舗・顧客など）に 240px を持たせ、余りの幅を全列に幅の比で配ります★（最小幅 988 → 1048px。D-167 追記）。★数値の列の見出しは右寄せ、名前の列の見出しは左寄せ★で中身と揃えます（`.table .thNum` / `.table .thName` `CleanAreaScreen.module.css:261` / `:265`。D-168。旧: `.thNum` が `.table th` の `text-align: left` に負け、見出しだけ左に寄っていた）
   - ★期間の入力欄は枠のどこを押してもカレンダーが開き、ホールディングス・顧客のプルダウンは 180px 固定★（D-162）。いずれも [3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)
   - ★画面名を「総清掃面積」「累計清掃面積」にしないこと★（`AppShell.tsx:12-15`）。期間で絞れる画面なので、名前に「総」「累計」を入れると定義を固定してしまいます
   - ★粒度タブを押したときは、数値カード・注記を残して表だけ薄くします★（結果に条件を持たせる `CleanAreaResult`。検索の条件が変わったときだけ画面全体を読み込み中にする。D-183）

**変えると何が起きるか**: `AREA_UNIT` を使わず `"㎡"` を直書きすると、単位を変えたいときに全画面を探すことになります。`query.ts` に日付上限を足すと、「2年ぶんの累計面積」という用途が使えなくなります。面積の `groupBy` に `clean_timetable_id: { not: null }` を足すと、予定外のタスクの面積が黙って消えます。

---

### 3.8 点検記録（`/inspections`）— 出すと決まったが、画面はまだ「準備中」

1. ロボットのブラシ・ダストボックス等7項目を Before/After の写真つきで記録する機能です。**現在は「準備中」の札しか出ません**
2. `/inspections` ／ `app/(dashboard)/inspections/page.tsx`（全5行）
3. ★`PlaceholderScreen`（`components/layout/PlaceholderScreen.tsx`、全15行）★。見出しと「準備中」の2行を出すだけです。`components/features/inspections/` は**存在しません**
4. ★なし★。`app/api/inspections/` のルートもありません（`d-watch-api.yaml:1693` `:1782`（`f2b91fc`）には `GET` / `POST` / `GET {id}` / `DELETE {id}` が定義済み）
5. 数字なし。★点検記録のデータはメインDBに保存しません★（DFA 独自ストアに分離。[CLAUDE.md](../CLAUDE.md) §7 / D-004）
6. ★この「決まったがまだ無い」状態を正確に押さえてください★:

| 項目 | 状態 |
|---|---|
| サービスとして出すか | ★**確定**★（D-150、2026-09-17） |
| サイドバーのメニュー | ★コメントアウトされたまま★（`AppShell.tsx:22`）。理由は `:17-21`（写真ストアが未着手で画面が成立しない。D-079 項3 / D-072 項7） |
| 画面 | `PlaceholderScreen`（準備中） |
| データ層 | ★Prisma版は未実装★（`lib/db/index.ts:2228` `:2231` が例外を投げる）。サンプル版は実装済み（`:733` `:736`） |
| 型・サンプルデータ | ★作成済み★（`lib/types/index.ts` / `lib/fixtures/inspections.ts` 154行） |
| API契約 | ★`d-watch-api.yaml` にかなり書き込み済み★（写真は Before/After × 7項目 ＝ 最大14枚、1枚10MB、jpeg・png、署名付き一時URLで配信） |

★着手をブロックしている未決が4件あります★（D-150 項2）。オブジェクトストレージの実体（B2）、誰が用意し費用を払い保守するか（B3）、★点検記録に「点検結果」を持たせるか（B4）★、誰がどこから入力するか（B5）。**B4 が設計上いちばん重い** — 「点検記録」という名前なのに点検の合否がどこにも残らない構造で、後から足すと既存レコードが全部「結果なし」になります。

**変えると何が起きるか**: `AppShell.tsx:22` のコメントを外すと、メニューは出るが中身は「準備中」のままになります。

---

### 3.9 D-Secログ（`/ds-logs`）

1. D-S制御盤（セキュリティ会社のシステムと制御サーバをつなぐ機器）が正常に動いているかを一覧する画面です
2. `/ds-logs` ／ `app/(dashboard)/ds-logs/page.tsx`
3. `components/features/ds-logs/DsLogScreen.tsx`（★552行。`50a3082`★）
4. `GET /api/ds-devices`（`:185`）、`PATCH /api/ds-devices/{serial_number}/note`（`sendDeviceNote()` `:69-79`。★「保存」を押したときだけ、変えた行の数だけ呼ぶ★）
5. **この画面の数字はどこから**:
   - 通信状態 … `controls.status`（0 正常 / 1 異常。`lib/db/ds-status.ts`。D-065）
   - 最終報告日時 … `control_reports.reported_at` の制御盤ごとの最大値（`groupBy`。`lib/db/index.ts:1969-1973`）。★`control_reports` は制御盤1台につき1行（最新の報告だけ）★と TeamWorks が回答しています（第10回 Q8-3。D-155 項5）
   - 備考 … ★メインDBではなくファイル★ `data/settings/ds-device-notes.json`（[章8](#8-データの出どころ) 8.9）
6. 固有の注意点:
   - ★ページ送りあり★
   - ★14画面のうち、画面から書き込みができるのはこの画面・システム管理・アラート・エラー一覧の4つだけです★（★メインDBに書くのはアラートとエラー一覧だけ★。この画面とシステム管理の保存先はファイル）
   - ★備考は編集モード（エラー一覧と同じ操作。`編集` → `保存（N件）`・`キャンセル`。D-183）★。変えた行だけを1行ずつ順に送り（`saveNoteDrafts()` `lib/ds-devices/note-drafts.ts`。`saveEdit()` `:242-268`）、★返ってきた値で行を書き換えます★（`:253-259`。サーバーが前後の空白を落とし「空文字 → 削除」を行うため）。★失敗した行は下書きと文言を残し、編集モードのままです★（`:260-264`）
   - ★通常モードの備考は文字だけ★（1行・はみ出しは「…」・全文は `title`。★空なら何も出さない★。旧: 「-」のボタン）。★編集中は絞り込みとページ送りを無効にし（`<fieldset disabled>`）、下書きがあればページを離れる前に確認します★（`beforeunload`）。★旧: セルを押して入力・フォーカスが外れたら1件ずつ保存（D-171 まで）は廃止★
   - 通信状態は★正常／異常の2つだけ★（`:30-34`。D-065。「通信断」は異常そのものであり3つ目ではありません）
   - 備考の文字数上限は `DEVICE_NOTE_MAX_LENGTH`（200字。`maxLength={DEVICE_NOTE_MAX_LENGTH}` `:507`）
   - ★表は施設名 200px を持ち、余りの幅を全列に幅の比で配ります★（最小幅 800px は据え置き。D-167 追記）。★見出し（`.th` `DsLogScreen.module.css:134`）は `text-align: left`★で中身と揃えます（`:145`。D-168。旧: 指定が無く th の既定の中央寄せだった）。★備考の文字の頭も見出しと揃えています★（通常モードの文字・編集欄とも左右に 8px はみ出させる。`.noteText` `:334-335` / `.noteInput` `:349-350`。D-171 であわせて直した）。★EN では備考の見出しが "NOTE"★（D-160 の副作用。3.5b）
   - ★ホールディングス・顧客のプルダウンは 180px 固定★（D-162。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)）。期間の入力欄はこの画面にはありません
   - 時刻: `control_reports.reported_at` は以前 UTC で書かれていました（9時間早く見えた）。TeamWorks は 2026-10-02 に DB の既定タイムゾーンを JST にしてデータを再登録したと回答しています（第10回 N-14）。★当方の再実測は未了★（D-155 項4）

---

### 3.10 D-Secアラート（`/ds-alerts`）

1. D-Security 側で起きた異常を一覧する画面です
2. `/ds-alerts` ／ `app/(dashboard)/ds-alerts/page.tsx`
3. `components/features/ds-alerts/DsAlertsScreen.tsx`（★613行。`b09e352`★）
4. `GET /api/ds-alerts`（`:201`）。★書き込みは `PATCH /api/errors/{error_id}/status`（`:96`。`sendDsAlertUpdate()`）★
5. **この画面の数字はどこから**: ★`robot_error_reports` のうち `error_type` が `D-S_CONTROL_FAILURE`（`D_SEC_ALERT_ERROR_TYPES`）・`status` 0/1/2 の行。検知時刻は `errored_at ?? created_at`・対応状況は 0 未解消・1 対応中・2 解消・備考は `comment`・シリアルは行の `serial_number`（生値）・施設と清掃エリアは行の `facility_id` / `section_id`（U3）。削除済みの施設・清掃エリアの行は出ない（U8）★。関数は `lib/db/ds-alert-mapping.ts` の `dsAlertsFromErrorRows()`
6. 固有の注意点:
   - ★ページ送りあり★
   - ★編集モードあり★（エラー一覧と同じ操作。`編集` → `保存（N件）`・`キャンセル`。D-161 / D-165）。★書き込みはエラー一覧と同じ `PATCH /errors/{error_id}/status`（`robot_error_reports` の4列）★。`PATCH /ds-alerts/{id}/status` は契約から削除（D-180 項4）。★備考の入力欄はカードの左の本文（通常モードの備考の行の位置）・対応状況の列は両モードで 120px・解消日時は左寄せ（D-181）★。★状態ボタンは「未解消のみ（対応中も含む）/ 全件 / 解消済み」、数値カードは「未解消 / 対応中 / 解消済み」（アラート画面に揃えた。D-181）★
   - ★集約済み★（第11回 P-10 = A。D-180）
   - ★ホールディングス・顧客のプルダウンは 180px 固定★（`.masterSelect` `DsAlertsScreen.module.css:119`。D-162）。★この画面の表はカードの一覧で、D-167 の列幅の手直しの対象外★

---

### 3.11 施設ロボットマスタ（`/facilities`）

1. どの施設にどのロボットが何台あるか、連絡先は誰かを見る台帳です
2. `/facilities` ／ `app/(dashboard)/facilities/page.tsx`
3. `components/features/facilities/FacilitiesScreen.tsx`（234行）
4. `GET /api/facilities`（`:81`）
5. **この画面の数字はどこから**: `facilities`（`facility_name` / `prefecture_name` / `addr1`・`addr2` / `phone` / `manager_name` / `charger_name` / `email` / `contract`）＋ `robots`（`serial_number`）＋ `devices`（`maker_id` / `model_name`）＋ `customers` / `groups`（所属）。★論理削除（`deleted_at`）された施設・ロボットは出しません★（`lib/db/index.ts:2141-2153`）。★連絡先は `facility_contacts` ではなく `facilities` の列から読んでいます★
6. 固有の注意点:
   - ★ページ送りあり★
   - ★完全な読み取り専用★。★モック右上の登録・更新ボタンは置きません★（`:16-21`）。登録・更新は D-Security 側の画面で行います（D-053）。★グレーアウトして置くのも同じ理由で不可★
   - ★契約フラグを画面に出している唯一の場所★です（`:180` `:187` の `monitoring_enabled` / `d_security_enabled`。`facilities.contract` の1桁目＝D-Security、2桁目＝D-Watch。D-080）。ただし**表示列としてだけ**で、メニューの出し分けには使っていません（3.0）
   - 任意項目（電話・責任者名・担当者名・メール）は空のことがあり、`"-"` を出します（`orDash()` `:28-31`）
   - ★表は住所 170px を持ち、余りの幅を全列に幅の比で配ります★（最小幅 1300px は据え置き。`FacilitiesScreen.module.css` の `.colAddress`。D-167 追記。旧: 住所だけが余りを受け持っていた）。★列が10あるので、画面幅 1440px では従来どおり横スクロール★。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)

---

### 3.12 スケジュール（`/schedules`）

1. どの施設で、どの曜日の何時に清掃が走る予定かを見る画面です
2. `/schedules` ／ `app/(dashboard)/schedules/page.tsx`
3. `components/features/schedules/SchedulesScreen.tsx`（201行。**全画面で最小**）
4. `GET /api/schedules`（`:83`）
5. **この画面の数字はどこから**: `clean_schedules`（曜日 `weeks` / 開始 `start_time` / 終了 `end_time`（HHMM。時が24〜29を取りうる＝翌日。TeamWorks 回答 B-5）/ 有効 `inactive = 0`）＋ 施設名 ＋ 清掃エリアのロボットのシリアル（`lib/db/index.ts:2188-2225`）。★`clean_schedules.contract` 列は読みません★（[章8](#8-データの出どころ) 8.3）。★ステージングでは0行です★（2026-10-01 実測・先方投入）
6. 固有の注意点:
   - ★ページ送りあり★ — ただし★モックに無いUIです★（`:19-23`）。ユーザーの明示の承認（2026-09-11「ページ送りは作成してください」）に基づいて入れました
   - ★**完全な参照専用**。行クリック・編集・削除を足さないこと★（`:13-18`）
   - ★見出しは「スケジュール管理」、サイドバーは「📅 スケジュール」で一致しません★（`:25-26`）。モックがそうなっているので揃えません
   - ★`weekdays`（7桁の数字）を画面で解釈しないこと★（`:28-31`）。曜日列にはサーバーが作った `weekdays_label`（「毎日」「月水金」等）をそのまま出します
   - ★表は施設名 190px を持ち、余りの幅を全列に幅の比で配ります★（最小幅 700px は据え置き。`SchedulesScreen.module.css` の `.colFacility`。D-167 追記。旧: 施設名だけが余りを受け持っていた）。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)

---

### 3.13 システム管理（`/system`）

1. 運用設定を見る・一部を編集する画面です
2. `/system` ／ `app/(dashboard)/system/page.tsx`
3. `components/features/system/SystemScreen.tsx`（480行）
4. `GET /api/settings/advice`（`:116`）、`GET /api/settings/display-limits`（`:154`）、`PATCH /api/settings/display-limits`（`:193-195`）。★`PATCH /api/settings/advice` はこの画面から呼んでいません★（API はあるが画面に編集欄が無い）
5. **この画面の数字はどこから**: 閾値の表示は `lib/config/index.ts`（`GRADE_THRESHOLDS` / `ESCALATION_THRESHOLDS` / `PRECHECK_MINUTES_BEFORE` 等。[章6](#6-変えると挙動が変わる数値--libconfig)）、表示上限とアドバイス文言は `data/settings/*.json`（保存が無ければコードの既定値）。★DB は読みません★
6. 固有の注意点:
   - ★ページ送りはありません★
   - ★**編集できるのは「連続日数の表示上限」1つだけです**★（`:4`）。他の設定（事前チェック設定 / 完了率の判定閾値 / アドバイス エスカレーション閾値）は★表示だけ★で、見出しに `編集不可` バッジが付きます
   - ★なぜ表示上限だけ先に編集できるのか★（`:11-15`）: 表示上限は★判定に影響しません★（L1〜L3 と R1〜R4 は上限をかける前の生値で判定する）。他は判定に使う閾値なので、変えると過去の判定結果と食い違います（★昨日 ◎ だった清掃が今日は ○ になる★）。D-130
   - 保存先はメインDBではなくファイル（`data/settings/display-limits.json`）
   - ★返ってきた値で画面を書き換えます★（`:200-201`）。サーバーが弾いて既定値に倒れた場合も「実際に有効になった値」が返ります
   - ★保存後にアドバイス文言の一覧を取り直しません★（`:182-183`）
   - 入力欄は文字列で保持し、★`Number()` は送る直前に1回だけ★（`:143`）

---

### 3.14 ページ送りの有無（まとめ）

| ページ送りあり（★7画面★） | ページ送りなし（7画面） |
|---|---|
| 清掃詳細ログ / アラート / ★エラー一覧★ / D-Secログ / D-Secアラート / 施設ロボットマスタ / スケジュール | 入口 / ログイン / ダッシュボード / レポート / 清掃面積 / 点検記録 / システム管理 |

ページ送りが無い画面のうち、ダッシュボード・レポート・清掃面積は「もっと表示」で件数を広げる方式です（`RESCUE_INITIAL_VISIBLE` ＝ 5、`CLEAN_AREA_INITIAL_VISIBLE` ＝ 10、レポートは `limit` を 10 → 200）。

**変えると何が起きるか**: ページ送りを外すと、上限（`PAGINATION.maxPerPage` ＝ 200件）を超えた行が画面から見えなくなります。スケジュール画面にページ送りを入れたのは、まさにその問題（201件目以降が見えない）を直すためでした（D-120）。

---

### 3.15 一覧画面に共通の見た目 — 分類の幅・期間の入力・表の列幅

★**改訂4で追加**★（D-162・D-166〜D-168。2026-10-06）。どれもユーザーが画面を見て指摘したもので、★判定や数字には関係しません★。行番号は `f2b91fc`。

#### ① ホールディングス・顧客のプルダウンは 180px 固定（D-162）

選んだ名前の長さで幅が変わり、隣の部品が動いていたのを止めました。長い名前は「…」で切り、全文はマウスを乗せると出ます（`<select>` の `title` に選んだ名前を入れる）。
★7画面とも、各画面の `.module.css` の `.masterSelect` に同じ指定（`width: 180px` ＋ `text-overflow: ellipsis`）を書いています★（共通部品にはしていない）。

| 画面 | 場所 |
|---|---|
| 清掃詳細ログ | `components/features/detail-log/DetailLogScreen.module.css:94` |
| アラート | `components/features/alerts/AlertsScreen.module.css:181` |
| エラー一覧 | `components/features/errors/ErrorsScreen.module.css:152` |
| レポート | `components/features/reports/ReportsScreen.module.css:81` |
| 清掃面積 | `components/features/clean-area/CleanAreaScreen.module.css:61` |
| D-Secログ | `components/features/ds-logs/DsLogScreen.module.css:116` |
| D-Secアラート | `components/features/ds-alerts/DsAlertsScreen.module.css:119` |

**変えると何が起きるか**: 幅を変えるときは7か所を揃えます。1か所だけ変えると、その画面だけ並びがずれます（エラーもテスト失敗も出ません）。

#### ② 期間の入力欄は枠のどこを押してもカレンダーが開く（D-162）

`components/ui/date-picker.ts` の `openDatePicker()`（`:11`）を `<input type="date">` の `onClick` に渡しています。中身は `showPicker()` を呼ぶだけで、★`showPicker()` が無いブラウザでは何もせず、例外（iframe の中など）は握りつぶします★（開けなくても、カレンダーのマークを押せば開く・数字を打てば入る）。
対象は期間の入力がある4画面 — 清掃詳細ログ（`DetailLogScreen.tsx:336` `:349`）・アラート（`AlertsScreen.tsx:435` `:449`）・エラー一覧（`ErrorsScreen.tsx:421` `:435`）・清掃面積（`CleanAreaScreen.tsx:303` `:312`）です。

#### ③ 表の列幅 — 余りの幅をどの列に配るか（D-166・D-167）

どの表も `table-layout: fixed`＋`<colgroup>` で、★JP/EN を切り替えても列幅が動かない★作りです（2026-10-01 のユーザー指摘以来）。そのうえで「画面が広いときに余った幅をどの列が受け持つか」を決め直しました。
★以前はどの表も「施設名（または住所）の1列だけ幅を指定せず、余りを全部受け持つ」形で、広い画面ではその列だけが極端に広くなっていました★（例: 清掃詳細ログの施設名が 1920px で ≒600px）。

| 画面（表） | CSS（`.table` の位置） | 余りの配り方 | 幅を持たせた列 | 表の最小幅 | 決定 |
|---|---|---|---|---|---|
| ★エラー一覧★ | `components/features/errors/ErrorsScreen.module.css`（`.table`） | ★詳細が残りの幅すべて（最小 196px）・施設名と種類は表の幅の 9%★（2026-10-27 指示書BE §12） | 検知時刻 120 / シリアル 140 / レベル 90 / 紐づく予定 110 / 対応状況 150 / 備考 120 | ★≒1129px★（旧 1156 → 1170 → 1130 → 1129） | D-166 ＋ 追記 ＋ 指示書BE §12 |
| 清掃詳細ログ（通常） | `components/features/detail-log/CleanLogTable.module.css:37` | ★全列に幅の比で配る★ | 施設名 110（ほか10列も px 指定） | 1134px | D-167 / D-180 項10 |
| ダッシュボード（同じ表の縮小表示） | 同上 `.compact`（`:60`） | 全列に幅の比で | 施設名 240 / 予定 200 / 出発 160 / 完了率 200 / 判定 200 | 1000px | D-167 追記 |
| レポートの施設別パフォーマンス | `components/features/reports/FacilityPerformanceTable.module.css:30` | 全列に幅の比で | 施設名 200（`.colFacility` `:39`） | 1040px（旧 990） | D-167 追記 |
| 清掃面積 | `components/features/clean-area/CleanAreaScreen.module.css:220` | 全列に幅の比で | 名前の列 240（`.colName` `:229`） | 1048px（旧 988） | D-167 追記 |
| D-Secログ | `components/features/ds-logs/DsLogScreen.module.css:202` | 全列に幅の比で | 施設名 200（`.colFacility` `:224`） | 800px（据え置き） | D-167 追記 |
| 施設ロボットマスタ | `components/features/facilities/FacilitiesScreen.module.css:45` | 全列に幅の比で | 住所 170（`.colAddress` `:79`） | 1300px（据え置き。1440px の画面では横スクロール） | D-167 追記 |
| スケジュール | `components/features/schedules/SchedulesScreen.module.css:43` | 全列に幅の比で | 施設名 190（`.colFacility` `:66`） | 700px（据え置き） | D-167 追記 |

- ★「全列に幅の比で配る」は `table-layout: fixed` の既定の動き★です。★全列に px の幅が付いていると、余りは各列の幅に比例して足される★。1列でも幅を付けない列があると、余りはその列が全部受け持ちます
- ★エラー一覧だけ配り方が違う★のは、列が少なく、全列に配ると空の備考の列が右に大きく空き（「右がすごい空いてる」）、施設名だけに配ると 1500px 前後で施設名が細って店名が3行に折り返したためです（D-166 追記）。清掃詳細ログは列が11と多く、全列に配っても1列あたりの増え方が小さいので全列方式です
- ★表の最小幅 ＝ 全列の幅の合計★（`min-width` の calc か固定値）。これより狭い画面では、表を包む `.tableWrap`（`overflow-x: auto`）が横スクロールになります
- D-Secアラート・アラートはカードの一覧なので対象外です

#### ④ 見出しと中身の寄せ（D-168）

全画面の表（11画面）で、列ごとに見出しと1行目の中身の寄せが一致することを測って確かめています（fixtures・画面幅 1920px。不一致 0）。直したのは2画面です。

| 画面 | 何がずれていたか | 直し方 |
|---|---|---|
| 清掃面積 | 数値の列の見出しに `.thNum`（右寄せ）を付けていたが、`.table th`（詳細度 0,1,1）の `text-align: left` に負けて★見出しだけ左寄せ★ | `.table .thNum` / `.table .thName`（`CleanAreaScreen.module.css:261` / `:265`）にして勝たせる |
| D-Secログ | 見出し `.th` に寄せの指定が無く★th の既定の中央寄せ★（中身は左寄せ） | `.th` に `text-align: left`（`DsLogScreen.module.css:262`） |

**変えると何が起きるか**: 列を足す・幅を広げるときは、★`<colgroup>` の `<col>` の順と、`.table` の `min-width` も同じだけ直します★（ずれると別の列に幅が付く・横スクロールの境目がずれる）。各 CSS の冒頭コメントに、見出しの EN の幅から決めた値の根拠が書いてあります。★寄せのクラスを足すときは詳細度に注意★してください（`.table th` のような指定に負けると、効いていないのにエラーは出ません）。


---

## 4. APIの入口 — `app/api/` の23本

「API ルート」とは、**ブラウザの画面がサーバーへ問い合わせるための窓口**です。D-Watch では `app/api/` の下に `route.ts` という名前で置かれているファイルがそれで、**フォルダの形がそのまま URL になります**（`app/api/clean-logs/route.ts` → `/api/clean-logs`）。

[CLAUDE.md](../CLAUDE.md) §2 の言う「APIサーバー」とは、外部の誰かが提供するものではなく、**この `app/api/` のことです**。TeamWorks が API を提供するわけではありません。ルートの中で Prisma が DB を直接読みます（D-046）。

### 4.1 23本の一覧（★改訂3 時点 25本・改訂4 時点 26本・改訂9 時点 27本★）

現在 `app/api/**/route.ts` は **23本**です（2026-10-05、`a1d7e43` で数えた）。1つのファイルが複数の HTTP メソッドを持つことがあるため、**窓口の数は25**です。
★改訂3（2026-10-06、`f02eb1d`）で2本増え **25本・窓口27** です★（下の表の 24・25）。★改訂4（2026-10-07、`f2b91fc`）で1本増え **26本・窓口28** です★（下の表の 26。レポートの CSV。D-163）。★改訂9（2026-10-27、未コミットの作業ツリー）で1本増え **27本・窓口29** です★（下の表の 27。エラー内容の選択肢。D-184）。

「HTTPメソッド」は操作の種類です。**GET = 読む / POST = 作る・送る / PATCH = 一部を直す**。実際に `export async function GET(...)` のように書かれている名前から判定しています。

#### ★第3版（10/01・22本）から増えた1本★

★**`app/api/prechecks/[id]/status/route.ts`（`PATCH`）**★です（指示書AT 段階5。2026-10-02 のコミット `8ddd9a8`）。アラート画面から**事前チェック異常の対応状況と備考**を保存する窓口で、★D-Watch が**メインDBに書き込む唯一の窓口**でした★（★改訂3 で `PATCH /api/errors/{error_id}/status` が加わり2本。どちらも同じ4列を同じ関数で書く★。書くのは `robot_error_reports` の4列だけ。★改訂9（D-184）で `/errors` の窓口だけが5列目の `error_list_id` も書くようになり、書く関数も分かれた（事前チェックは `writeRobotErrorColumns()`・エラー一覧は `writeErrorReportColumns()`）★。[章8](#8-データの出どころ) 8.10）。消えたルートはありません。

| # | パス | メソッド | 何をするか | 使うクエリ層・部品 | 主に使う画面（呼び出し箇所） |
|---|---|---|---|---|---|
| 1 | `/api/auth/login` | POST | 共有トークンを検証し `dwatch_session` cookie を発行 | `lib/auth/token` `lib/auth/cookie` | ログイン（`app/login/page.tsx:34`） |
| 2 | `/api/auth/logout` | POST | `dwatch_session` cookie を削除（204） | `lib/auth/cookie` | ★画面からの呼び出しは0件★（`components/` `app/` に該当なし。ログアウトのボタンが無い） |
| 3 | `/api/clean-area` | GET | 清掃面積の集計（総面積・粒度別の内訳・完了率・実施回数・異常値の見張り） | `lib/clean-area/query` `aggregate` `anomaly` / `lib/db/clean-area-source` | 清掃面積（`CleanAreaScreen.tsx:141`） |
| 4 | `/api/clean-logs` | GET | 清掃詳細ログ一覧（★他の一覧APIの見本★。★各行に `result_pending`（結果待ち）。結果待ちは `result=problem` / `critical` に出ない。D-173★） | `lib/clean-logs/query` / `lib/settings/display-limits` | 清掃詳細ログ（`DetailLogScreen.tsx:205`）。★ダッシュボードはもう呼ばない（D-159）★ |
| 5 | `/api/clean-logs/export` | GET | 同じ条件で CSV を出す（`page` / `per_page` は無視。20,000行超は 422） | `lib/clean-logs/query` `lib/clean-logs/csv` ★`lib/csv`★ | 清掃詳細ログ（`DetailLogScreen.tsx:516` のリンク） |
| 6 | `/api/clean-logs/[id]/advice` | GET | 行を開いたときの推定原因＋確認アクション（R1〜R4 / B1〜B3） | `lib/judge` / `lib/settings/advice` / `lib/settings/display-limits` | 清掃詳細ログ（`DetailLogScreen.tsx` の `toggleRow`）/ ダッシュボード（`DashboardScreen.tsx` の `toggleRow`。`cleanLogAdviceUrl` 経由） |
| 6′ | `/api/clean-logs/unscheduled/[report_id]/advice` | GET | ★予定なしの行★を開いたときのアドバイス（応答は上と同じ `CleanLogAdviceResponse`） | 同上 / `lib/db` の `unscheduledCleanLog()` | 清掃詳細ログ・ダッシュボード（`cleanLogAdviceUrl` 経由。指示書BI） |
| 7 | `/api/clean-logs/[id]/areas` | GET | エリア別完了率（施設×日付） | `lib/clean-logs/areas` | 清掃詳細ログ（`:286`）/ ダッシュボード（`:175`） |
| 8 | `/api/customers` | GET | 顧客一覧（分類マスタ。読み取り専用） | `lib/masters/query` | 分類の絞り込みがある全画面（`lib/masters/options.ts:100`） |
| 9 | `/api/dashboard` | GET | 清掃結果サマリ（★`summary.pending`＝結果待ち。D-173★） / ★コンパクト版の表（`problem_logs`）★ / スケジュール進捗 / 要レスキュー / セキュリティバナー。★`date` 省略時は直近モード（D-159）★ | `lib/dashboard/aggregate` / ★`lib/dashboard/latest-run`★ / `lib/clean-logs/query` | ダッシュボード（`DashboardScreen.tsx:87`） |
| 10 | `/api/ds-alerts` | GET | D-Secアラート一覧＋サマリ | `lib/ds-alerts/query` | D-Secアラート（`DsAlertsScreen.tsx:201`） |
| 11 | `/api/ds-devices` | GET | D-S制御盤の稼働状況一覧＋サマリ（備考を合成） | `lib/ds-devices/query` `lib/settings/ds-device-notes` | D-Secログ（`DsLogScreen.tsx:185`） |
| 12 | `/api/ds-devices/[serial_number]/note` | PATCH | D-S制御盤の備考を保存（★メインDBではなくファイルへ★） | `lib/settings/ds-device-notes` | D-Secログ（`sendDeviceNote()` `DsLogScreen.tsx:69-79`） |
| 13 | `/api/facilities` | GET | 施設ロボットマスタ（読み取り専用） | `lib/facilities/query` | 施設ロボットマスタ（`FacilitiesScreen.tsx:81`） |
| 14 | `/api/groups` | GET | ホールディングス一覧（分類マスタ。読み取り専用） | `lib/masters/query` | 分類の絞り込みがある全画面（`lib/masters/options.ts:99`） |
| 15 | `/api/prechecks` | GET | アラート（事前チェック異常＋出発失敗）一覧＋サマリ（★結果待ちの清掃は出発失敗に出さない。D-173★） | `lib/prechecks/query` | アラート（`AlertsScreen.tsx:232`） |
| 16 | ★`/api/prechecks/[id]/status`★ ★新（10/02）★ | ★PATCH★ | ★事前チェック異常の対応状況・備考を更新★（★`robot_error_reports` の4列へ書く窓口。10/06 から 25 も★。★`error_list_id` は送っても捨てる＝書けない（D-184）★） | `lib/prechecks/status-update` / `lib/db/robot-error-write` / `lib/logger/db-write` | アラート（`AlertsScreen.tsx:127`。★「保存（N件）」で変えた行だけ1行ずつ★。D-165） |
| 17 | `/api/reports/continuous-problems` | GET | 2日以上連続で△以下 or 出発失敗の施設 | `lib/reports/query` / `lib/db/report-mapping` | レポート（`ReportsScreen.tsx:237`） |
| 18 | `/api/reports/error-breakdown` | GET | エラー種別の内訳（件数の降順・★生値のまま★） | `lib/reports/query` | レポート（`ReportsScreen.tsx:243`） |
| 19 | `/api/reports/facility-performance` | GET | 施設別パフォーマンス（稼働率ワースト順） | `lib/reports/query` ★`lib/reports/facility-performance-load`（CSV と共有。D-163）★ | レポート（`ReportsScreen.tsx:234`） |
| 20 | `/api/reports/kpi` | GET | KPI4指標＋前期比（対象顧客が1社に定まらないと 422） | `lib/reports/query` `lib/reports/target-customer` | レポート（`ReportsScreen.tsx:232`） |
| 21 | `/api/schedules` | GET | スケジュール一覧（読み取り専用） | `lib/schedules/query` | スケジュール（`SchedulesScreen.tsx:83`） |
| 22 | `/api/settings/advice` | GET / PATCH | アドバイス文言の一覧 / 1ルール分の保存（`data/settings/advice.json`） | `lib/settings/advice` `lib/settings/advice-validation` | システム管理（GET のみ `SystemScreen.tsx:116`）。★PATCH を呼ぶ画面は0件★ |
| 23 | `/api/settings/display-limits` | GET / PATCH | 表示上限の一覧 / 1件更新・既定値へ戻す（`data/settings/display-limits.json`） | `lib/settings/display-limits` | システム管理（`SystemScreen.tsx:154` / `:193`） |
| 24 | ★`/api/errors`★ ★新（10/06）★ | GET | ★エラー一覧★（`robot_error_reports` の全行。★削除済みの施設・清掃エリアの行は除く（`meta.total` も。U8。D-180 項11）★。既定は未解消・対応中。`include_resolved=true` で解消も。★小文字の `true` / `false` 以外は422★）。★10/27 から各行に `error_list_id`（null＝未選択）・`error_list`（名前解決。★無効化・削除済みの選択肢でも解決する★。`error_lists` に無い id なら null）・`error_list_selectable`（`D-S_CONTROL_FAILURE`・`SECURITY_ON` の行だけ false）の3項目（D-184）★ | `lib/error-reports/query` / `lib/db/error-report-mapping` / ★`lib/db/error-list-source`★ / `lib/facilities/resolve-by-name` `resolve-by-master` `intersect-ids` | エラー一覧（`ErrorsScreen.tsx:239`） |
| 25 | ★`/api/errors/[error_id]/status`★ ★新（10/06）★ | ★PATCH★ | ★エラーの対応状況・備考を更新★（★`robot_error_reports` に行があれば書ける。事前チェックかどうかを問わない★。★削除済みの施設・清掃エリアの行は404（U8。D-180 項11）★。状態・日時・備考の規則は 16 と同じ）。★10/27 から本文に `error_list_id`（エラー内容。1以上の整数）も受け、★`robot_error_reports` の5列（4列＋`error_list_id`）を書く。`error_list_id` を書けるのはこの窓口だけ★（D-184）。★422 になる場合★: null・0・負数・小数・文字列／選べない行（`D-S_CONTROL_FAILURE`・`SECURITY_ON`）で id を変える／有効でない id（無い・無効化・削除済み）／★エラー内容か備考を変える要求で★、適用後が「その他」（131）なのに備考が空。★現在値と同じ id は検証せず書かない★・★状態だけの要求には備考必須を掛けない★（指示書BD §3-3） | ★`lib/errors/status-update`★（`lib/prechecks/status-update` の上に `error_list_id` を足したもの）/ `lib/db/robot-error-write`（★`writeErrorReportColumns()`★）/ `lib/logger/db-write` | エラー一覧（`sendErrorUpdate()`。★「保存（N件）」で変えた行だけ1行ずつ★。D-165。★エラー内容を変えた行は `error_list_id` も送る★。指示書BE） |
| 26 | ★`/api/reports/facility-performance/export`★ ★新（10/06）★ | GET | ★施設別パフォーマンスの CSV★（`/reports/facility-performance` と同じパラメータ。★`limit` は無視して絞り込み後の全施設★。並びも表と同じ稼働率ワースト順。UTF-8 BOM・CRLF・日本語固定。★`MAX_CSV_ROWS`（20,000）を超えたら 422★（`details: { total, max }`。清掃詳細ログの CSV と同じ形・同じ向き `>`）。ファイル名は `facility-performance_{period}_{期間}.csv`（例 `_monthly_2026-09` / `_weekly_2026-10-w1` / `_yearly_2025`）。`app/api/reports/facility-performance/export/route.ts`（83行）。D-163） | ★`lib/reports/facility-performance-load`（一覧と同じ1本）★ / `lib/reports/facility-performance-csv` / `lib/reports/facility-performance-labels` / `lib/csv` | レポート（`ReportsScreen.tsx:393` のリンク） |
| 27 | ★`/api/error-lists`★ ★新（10/27）★ | GET | ★エラー内容の選択肢★（`error_lists`。エラー一覧のセレクトリスト用）。★`inactive = 0` かつ `deleted_at IS NULL` の行だけ・id の昇順★。ページングしない（`meta` は `/groups` と同じ書き方）。各行に `note_required`（★選ぶと備考が必須。`ERROR_LIST_NOTE_REQUIRED_IDS` = 131「その他(該当なし)」★）。★分類名・詳細の文字列は DB の値のまま（変換しない。D-077 と同じ）★。`app/api/error-lists/route.ts`（46行）。D-184 / 指示書BD §3-1 | `lib/masters/query`（`masterListResponse()`）/ ★`lib/db/error-list-source`★ | エラー一覧（`ErrorsScreen` が編集に入ったとき1回。`/api/error-lists`。指示書BE）。★`PATCH /errors/{error_id}/status` の検証の中でも `db.errorLists()` を引く★（id が変わるときだけ） |

#### 各ルートが読むテーブル

ルートは DB を直接読まず、`db.〇〇()`（`lib/db/index.ts`）を呼びます。どのメソッドがどのテーブルを読むかは[章8](#8-データの出どころ)の 8.2 にまとめました。ここでは窓口ごとの要約だけを書きます。

| ルート | 呼ぶ `db` のメソッド | 主なテーブル |
|---|---|---|
| `/clean-logs` `/clean-logs/export` `/clean-logs/{id}/advice` | `cleanLogs()` | `clean_timetables` `clean_task_reports` `robot_error_reports` |
| `/clean-logs/{id}/areas` | `cleanLogs()` ＋ `areaCompletions()` | 上と同じ ＋ `sections` |
| `/dashboard` | `cleanLogs()` ★`cleanLogRunMeta()`（直近モードだけ）★ `schedules()` `dsAlerts({ open_only: true })`（★未解消だけ。D-182★） `rescueRequired()` | 上と同じ ＋ `clean_schedules` `robot_error_reports`（D-Sec アラート） |
| `/prechecks` | `prechecks()` | `clean_timetables` `clean_task_reports` `robot_error_reports` |
| `/prechecks/{id}/status` | `precheckItemById()` → `updatePrecheckErrorColumns()` → `precheckItemById()` | ★`robot_error_reports`（読んで・4列を書いて・読み直す）★ |
| ★`/errors`★ | `errorReports()`（★2段階で読む★）＋ `facilityNameIndex()` `facilityGroupIndex()` | `robot_error_reports` `clean_timetables` `clean_timetable_histories` `facilities` ★`error_lists`（名前解決。`id IN` を1回）★ |
| ★`/errors/{error_id}/status`★ | `errorReportById()` →（★id が変わるときだけ `errorLists()`★）→ `updateErrorReportColumns()` → `errorReportById()` | ★`robot_error_reports`（読んで・5列（4列＋`error_list_id`）を書いて・読み直す）★ ★`error_lists`★。★D-Sec 画面からも呼ばれる★（D-Sec は `error_list_id` を送らない。送っても選べない行なので422） |
| ★`/error-lists`★ ★新（10/27）★ | `errorLists()` | ★`error_lists`（有効な行だけ）★ |
| `/reports/kpi` | `customers()` `reportKpis()` | `customer_summaries`（確定期間）/ 詳細3テーブル（当期） |
| `/reports/facility-performance` ★`/reports/facility-performance/export`★ | `facilityPerformance()`（★両方とも `loadFacilityPerformance()` 経由★） | `facility_summaries`（確定期間）/ 詳細3テーブル（当期） |
| `/reports/continuous-problems` | `continuousProblems()` | `clean_timetables` `clean_task_reports` |
| `/reports/error-breakdown` | `errorBreakdown()` | `facility_error_summaries`（確定期間）/ `robot_error_reports`（当期） |
| `/clean-area` | `cleanArea()` | `clean_task_reports` `clean_timetables` `facilities` `customers` `groups` |
| `/ds-devices` `/ds-devices/{serial}/note` | `dsDevices()` `dsDeviceExists()` | `controls` `control_reports` ＋ `data/settings/ds-device-notes.json` |
| `/ds-alerts` | `dsAlerts({ open_only: false })` | `robot_error_reports` `facilities` `sections` |
| `/facilities` | `facilities()` | `facilities` `robots` `devices` `customers` `groups` |
| `/schedules` | `schedules()` | `clean_schedules` |
| `/groups` `/customers` | `groups()` `customers()` | `groups` `customers` |
| `/settings/*` `/auth/*` | （DB を読まない） | `data/settings/*.json` / cookie |

#### ★契約（`d-watch-api.yaml`）にあるが、ルートが無いもの★

`d-watch-api.yaml` には28のパスがあり、ルートがあるのは26です（`grep -c "^  /" d-watch-api.yaml` / `app/api/**/route.ts`。★改訂5: `PATCH /ds-alerts/{id}/status` を契約から削除したため、契約だけ残るのは2つ★）。残り2つは**契約だけ先に書かれていて、まだ作っていません**。

| パス | メソッド | 状態 |
|---|---|---|
| `/inspections` | GET / POST | 点検記録。画面は「準備中」（[章3](#3-画面カタログ) 3.8。D-150 の未決4件待ち） |
| `/inspections/{id}` | GET / DELETE | 同上 |

（`d-watch-api.yaml:1693` / `:1782`）

**変えると何が起きるか**: API の形（パス・パラメータ名・返す JSON のキー）を変えたくなったら、**先に `d-watch-api.yaml` を直してからコードに反映します**（[CLAUDE.md](../CLAUDE.md) §6-1）。実装だけ変えると、YAML を正としている型定義・テスト・TeamWorks への説明と静かに食い違います。

### 4.2 全ルート共通の「形」

23本すべてが同じ骨格でできています。見本は `app/api/clean-logs/route.ts` です（ファイル冒頭 `:4` が「これが動く見本です」と宣言しています）。

**① 必ず `withRequestId` で包む**

```ts
export async function GET(request: Request) {
  return withRequestId(request, async () => {
```
（`app/api/clean-logs/route.ts:47-48`）

`withRequestId()`（`lib/logger/context.ts:70`）は、**1回の問い合わせに通し番号を振る**関数です。この中で出たログにはすべて同じ番号が付くので、「このエラーはどの操作で起きたのか」を後から追えます。

**23本すべてに入っていることを確認済み**です（`withRequestId` を含まない `route.ts` は0件。2026-10-05）。

**変えると何が起きるか**: 包み忘れたルートは、ログの `request_id` が `-` になります。エラーは出ません。障害が起きた日に「追えない1本」としてだけ現れます。

**② `catch` で `logApiError()` を呼ぶ**

```ts
} catch (e) {
  const body: ApiError = {
    error: { code: "internal_error", message: "データの取得に失敗しました", details: null },
  };
  logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
  return errorResponse(body, 500);
}
```
（`app/api/clean-logs/route.ts:90-96`）

`logApiError()`（`lib/logger/api.ts:48`）の要点は、**クライアントへ返すのと同じオブジェクトをログにも渡す**ことです（`:3-7`）。ログ側で文言を組み立て直すと、画面に出た文言とログの文言がずれます。**生の例外メッセージはログの `error_message` に入りません**（`:7`）。

各ルートの先頭には `const ROUTE = "/api/..."` が置かれ、これをログに使います（23本中22本。`/api/auth/logout` だけは `ROUTE` 定数を持ちません）。コメントが「実際のパスを書く。`/api/v1/...` のような架空のパスを書かない」と念を押しています（`lib/logger/api.ts:15`）。

**③ 書き込みのルートだけ、もう1種類ログを出す**

`PATCH /api/prechecks/{id}/status` は、メインDBに書いたときに★監査ログ `db.write` を成功・失敗の両方で1件ずつ★出します（`app/api/prechecks/[id]/status/route.ts:150` `:155` `:159`。`lib/logger/db-write.ts`）。★備考の中身と DB のエラーメッセージは載せません★（[章8](#8-データの出どころ) 8.10）。

**④ 返す形は2通りだけ**

成功: `{ items, meta }`（一覧）または素のオブジェクト（単体）
失敗: `{ error: { code, message, details } }`

### 4.3 エラー応答の統一形式

[CLAUDE.md](../CLAUDE.md) §9.3 が全API共通と定めている形です。

```json
{ "error": { "code": "validation_failed", "message": "…", "details": { "week": ["1以上を指定してください"] } } }
```

`code` に入ってよい値は6種類に限定されています（`lib/types/index.ts:31-45` の `ErrorCode`）。

| code | 意味 | 誰が返すか |
|---|---|---|
| `validation_failed` | パラメータが不正（422） | 各ルート |
| `not_found` | 対象が無い（404） | `[id]` `[serial_number]` を取るルート |
| `internal_error` | 想定外の失敗（500） | 各ルートの `catch` |
| `unauthorized` | 未認証（401） | `proxy.ts:84` |
| `forbidden` | IP制限で拒否（403） | `proxy.ts:69` |
| `invalid_token` | ログイン時のトークン不一致 | `/api/auth/login` |

`"not_implemented"`（501）は**外されました**（`lib/types/index.ts:35-39`）。501 を返している実装が0件で、`d-watch-api.yaml` にも出てこないためです。

`PATCH /api/prechecks/{id}/status` の 404 は3通りあります — ①その id の行が無い／事前チェック異常の行ではない（出発失敗の id を含む）（`route.ts:126`）、②確認と書き込みの間に行が消えた（書いた件数が0。`:153-156`）、③書いた後に読み直せない（`:169`）。どれも同じ文言で返します。

**変えると何が起きるか**: ここに値を足すと、`d-watch-api.yaml` と型定義が乖離します。足すなら先に YAML です。

### 4.4 `lib/api/query-result.ts` の役割

「クエリパラメータの検査に失敗した」ことを、**`null` ではなく形のある値で返す**ための共通の型です（`lib/api/query-result.ts:23-25`）。

```ts
export type QueryParseResult<T> =
  | { ok: true; query: T }
  | QueryParseFailure;
```

失敗側は `{ ok: false, error, status }` を持ちます（`:17-21`）。`fail()`（`:32`）がそれを組み立てます。

なぜ `null` ではいけないか。`null` だと「**どのパラメータがなぜ不正か**」を呼び出し側へ渡せず、ルートは `details: null` の素っ気ない422しか返せません（`:4-7`）。画面は「何を直せばいいか」を利用者に言えなくなります。

置き場所が `lib/clean-logs/` ではなく `lib/api/` なのは、エラー形式が全API共通だからです（`:9-12`）。

使う側は `route.ts` の冒頭でこう書きます。

```ts
const parsed = parseCleanLogQuery(searchParams);
if (!parsed.ok) {
  return errorResponse(parsed.error, parsed.status);
}
```
（`app/api/clean-logs/route.ts:50-53`）

URL の中の `{id}` などは別の部品 `lib/api/path-params.ts` が検査します — `parsePathId()`（上限 `PATH_ID_MAX`）、`parsePathSerialNumber()`（100文字）、★`parsePathErrorId()`（50文字。10/02 に追加）★。上限の数値は `lib/config/index.ts` にあります（[章6](#6-変えると挙動が変わる数値--libconfig)）。

**変えると何が起きるか**: 検査を通さず `searchParams.get("date_from")` を直接データ層へ渡すと、検証されていない値が SQL の条件に届きます（`lib/clean-logs/query.ts:55-57` が明示的に禁止）。

### 4.5 一覧APIの `meta`

[CLAUDE.md](../CLAUDE.md) §9.3 は「一覧APIは必ず `meta: { total, page, per_page }` を返す」と定めています。

```ts
const body: ListResponse<CleanLogItem> = {
  items,
  meta: { total, page, per_page: perPage },
};
```
（`app/api/clean-logs/route.ts:106-109`）

`total` は**絞り込んだ後・ページで切り出す前**の件数です（`:100`）。画面はこれでページ数を計算します（`DetailLogScreen.tsx:205`）。

**意図的な例外**:

- `/api/reports/error-breakdown` は `meta` を返しません（`app/api/reports/error-breakdown/route.ts:31-41`）。エラー種別の内訳は集計結果で全件を返すため、ページングの概念がないからです。コメントは「型を合わせるために `meta` を捏造しない」と書いています
- `/api/reports/kpi` と `/api/dashboard` は一覧ではなく単体の集計なので、`items` / `meta` で包まず素のオブジェクトを返します（`app/api/reports/kpi/route.ts:6`）
- `/api/clean-area` は `meta` を返しますが、ページ送りが無いので常に `page: 1`・`per_page` ＝ 件数です（`app/api/clean-area/route.ts:123`）
- `PATCH` の3本（備考・表示上限・アラートの状態）は、更新後の1件を素のオブジェクトで返します。画面はその値で表示を書き換えます

**変えると何が起きるか**: `total` を「ページ切り出し後の件数」にすると、ページ送りが常に1ページで止まります。表示上限（連続日数の頭打ち）を `total` に反映させると、CSV出力の可否判定（`DetailLogScreen.tsx:181` の `csvBlocked`。`f2b91fc`）がずれます。

### 4.6 認証が要らないルート

D-Watch は全ページ・全APIに認証がかかります。担当するのは `proxy.ts`（リポジトリ直下、115行）です。

★**このファイルは `middleware.ts` ではありません**★。Next.js 16 で `middleware` は非推奨になり `proxy` に改名されました（`proxy.ts:4-7`）。`middleware.ts` を置くと dev サーバが警告を出します。

`proxy.ts` は**この順で**動きます（`:13-17`。逆にしてはいけません）。

1. 相関IDの発行（`:39`）… 弾いたリクエストのログにも番号が付くよう**必ず先**
2. IP制限 … 拒否なら 403（`:63-73`）
3. 認証 … 未認証なら、画面は `/login` へリダイレクト（`:92`）、APIは 401（`:81-85`）

**認証をかけないパスは2つだけ**です。

```ts
const PUBLIC_PATHS = ["/login", "/api/auth/login"] as const;
```
（`lib/proxy/authenticate.ts:25`）

ここを保護すると誰もログインできなくなるため、という理由がコメントに書かれています。

重要なのは、**この2つを `matcher`（proxy を走らせる対象の絞り込み）では除外していない**ことです（`proxy.ts:106-107`）。`matcher` から削ると「保護漏れの発見が難しくなる」ため、あえて関数の中で分岐して通しています。`matcher` が除外するのは静的ファイル（`_next/static` / `_next/image` / `favicon.ico` / `.svg`）だけです（`:112-113`）。

**IP制限は `X-Forwarded-For` の末尾を見ます**（`proxy.ts:63` の `clientIp(forwardedFor, TRUSTED_PROXY_HOPS)`）。先頭はクライアントが自由に書けるので使いません（[CLAUDE.md](../CLAUDE.md) §3 / D-077 項5）。
なお、許可リスト `ALLOWED_IPS` は空のままで、`ALLOW_ALL_WHEN_EMPTY = true` なので、★いまは IP 制限が事実上かかっていません★。これは検証段階の意図した状態で、★リリース直前のセキュリティ強化の対象として [CLAUDE.md](../CLAUDE.md) §3 に列挙済み★です（D-154。[章6](#6-変えると挙動が変わる数値--libconfig) 6-6）。

**変えると何が起きるか**: `PUBLIC_PATHS` に `/api` を足すと、全APIが無認証で読めるようになります。順序を入れ替えて認証をIP制限より前に置くと、IP制限で弾いたログに相関IDが付きません。`proxy.ts` に `runtime` を指定するとエラーになります（`:19-20`。proxy は Node.js ランタイム固定）。


---

## 5. アプリの頭脳 — 判定と計算

このアプリの「◎○△×」「L1〜L3」「KPI の数字」「対応アドバイス」「要レスキュー」は、すべて `lib/judge/` の6ファイルの中で決まります。画面も API も、ここが出した答えを並べているだけです。

| ファイル | 行数 | 何を決めるか |
|---|---|---|
| `lib/judge/grade.ts` | 76 | ◎○△× の判定（`judgeGrade()`）と表示用の丸め（`displayRate()`） |
| `lib/judge/kpi.ts` | 231 | ★1回ごとの完了率（`runCompletionRate()`）★・完了率の平均（`meanCleanRatio()`）・稼働率など3指標・前期比・施設ステータス |
| `lib/judge/escalation.ts` | 68 | 連続問題日数と L1〜L3 |
| `lib/judge/advice.ts` | 122（`f2b91fc`） | 対応アドバイス R1〜R4 / B1〜B3 の発火条件 |
| `lib/judge/rescue.ts` | 296 | 要レスキュー店舗の3条件 |
| `lib/judge/index.ts` | 22 | 上の5つをまとめて外に出す入口 |

判定そのものは上の6ファイルですが、★判定に渡す材料を作る係★が `lib/db/` などに8本あります（章5-10）。「どの行を数えるか」はそちらで決まるので、数字の意味を追うときは両方を見てください。画面ごとの「どのテーブルのどの列から来た数字か」は、次の[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)にまとめてあります。

★**完了率の出し方は、この1か月で2回変わりました**★。古い版を覚えている人は、この表を先に読んでください。

| 時期 | 清掃完了率の出し方 | 決定 |
|---|---|---|
| 〜2026-09-29 | Σ清掃面積 ÷ Σ清掃対象面積（期間の合計どうしの面積比） | 旧 `CLAUDE.md` §4.2 |
| 2026-09-30〜10-01 | `clean_task_reports.clean_ratio` 列の単純平均 | D-151（指示書AQ 決定4） |
| ★2026-10-02〜（現在）★ | ★**1回ごとの完了率**（`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て）の単純平均。予定外のタスク（0・NULL）も1件として入れる（2026-10-29 から。D-187。旧: 除く）★。★2026-10-05 から `task_area = 0` は 0%★ | ★D-155（ユーザー判断「一旦B」2026-10-02）/ 指示書AS → ★D-158 で確定（第11回 P-2=A・P-5=B）/ 指示書AU★ |

あわせて ◎○△× の境界も 2026-09-16 に変わっています（「超」→「以上」。D-139。章5-2）。

---

### 5.1 「純粋関数」とは — なぜこの作りにしているか

**純粋関数**とは「同じ数字を入れれば必ず同じ答えが返り、データベースも時計も見ない関数」のことです。`lib/judge/` の関数はすべてこれで、`judgeGrade(90.1)` は、いつ・誰が・どのパソコンで呼んでも必ず `◎` を返します。

こうしておくと**テストで答えを固定できる**のが最大の利点です。「完了率 90.1 なら ◎」という表を1枚作っておけば、機械が毎回自動で確かめてくれます。

- `lib/judge/` はサンプルデータと実DBの切り替え（`USE_FIXTURES`）の**外側**にあります（`lib/judge/index.ts:4-6`）。サンプルデータで動かしている間も本番とまったく同じ判定が走ります（D-044）
- `lib/judge/rescue.ts` は冒頭で「DBアクセスも `new Date()` も混ぜない」と明記しています
- `lib/judge/` は `lib/db/` を import しません。そのため `runCompletionRate()` の引数も「`clean_area` と `task_area` を持つもの」という形だけで受け取っています（`lib/judge/kpi.ts:163-165` の docstring / `:166-169`）

**変えると何が起きるか**: この中で DB や現在時刻を読むと、テストが日によって成功したり失敗したりするようになり、判定の正しさを誰も保証できなくなります。

---

### 5.2 ◎○△× の判定 — `judgeGrade()`

実体は `lib/judge/grade.ts:29-35` です。引数は清掃完了率（％）で、未実施は `null`（値が無いこと）。閾値は直書きせず、`lib/config/index.ts:18-25` の `GRADE_THRESHOLDS`（`excellent: 90` / `good: 70` / `poor: 10`）から読みます。

```mermaid
flowchart TD
  A["完了率 rate（丸める前の生値）"] --> B{"rate は null か<br/>（未実施）"}
  B -- はい --> X["× を返す"]
  B -- いいえ --> C{"rate >= 90<br/>（excellent）"}
  C -- はい --> D["◎ を返す"]
  C -- いいえ --> E{"rate >= 70<br/>（good）"}
  E -- はい --> F["○ を返す"]
  E -- いいえ --> G{"rate < 10<br/>（poor）"}
  G -- はい --> H["× を返す"]
  G -- いいえ --> I["△ を返す"]
```

#### 境界値の表（実装 `lib/judge/grade.ts:29-35` / 表示 `:43-46`）

★**比較は `>=`（以上）です**★。★**ちょうど 90.0 は ◎、70.0 は ○**★。以前は `>`（超）で 90.0 は ○ でしたが、ユーザー指示「◯、×は90%以上のように境はふくめてください」（2026-09-16）で変わりました（D-139。旧 D-020b は破棄）。★**× の境界だけは「10%未満」のまま**★（同日「10％未満を×としてください」で確定）なので、10.0 は △ です。

| 完了率（生値） | 判定 | 画面表示 `displayRate()` | なぜそうなるか | 旧（D-139 前） |
|---|---|---|---|---|
| 90.1 | ◎ | `90%` | `90.1 >= 90`（`grade.ts:31`） | ◎（変化なし） |
| ★90.0★ | ★◎★ | `90%` | `90.0 >= 90` が成立 | ★○ だった★ |
| 89.9 | ○ | `90%` | `>= 90` 不成立、`>= 70` 成立 | ○ |
| ★89.5★ | ★○★ | ★`90%`★ | ★表示は 90% だが判定は ○★（章5-3） | ○ |
| 70.1 | ○ | `70%` | `70.1 >= 70`（`grade.ts:32`） | ○ |
| ★70.0★ | ★○★ | `70%` | `70.0 >= 70` が成立 | ★△ だった★ |
| 69.9 | △ | `70%` | `>= 70` 不成立・`< 10` 不成立 | △ |
| 10.1 | △ | `10%` | 同上 | △ |
| 10.0 | △ | `10%` | `10.0 < 10` は不成立なので △ | △（変化なし） |
| 9.9 | × | `10%` | `9.9 < 10` が成立（`grade.ts:33`） | × |
| 0 | × | `0%` | 同上 | × |
| null（未実施） | × | `-` | 最初の分岐で即 ×（`grade.ts:30`） | × |

この表は `lib/judge/__tests__/grade.test.ts:24-37` の期待値表（12ケース）と同じです。表を変えたいときは、まずこのテストを直します。

- `GRADE_THRESHOLDS` を読むのは `judgeGrade()` だけではありません。★`isProblemCompletion()`（`lib/db/clean-log-mapping.ts:332-334`）は `rate === null || rate < GRADE_THRESHOLDS.good` という**別の書き方の同じ規則**★で「問題あり（△×）」を判定しています（連続日数の材料）。結果は `judgeGrade()` と常に一致しますが、★片方の比較演算子だけを動かすと食い違います★（`grade.ts:54-63` / `clean-log-mapping.ts:313-331`）
- アドバイス R2（`lib/judge/advice.ts:78`）も `GRADE_THRESHOLDS.poor` を直接読みます

**変えると何が起きるか**: `GRADE_THRESHOLDS` を1つ動かすと、全画面のバッジ・ダッシュボードの成功件数・アドバイス R2 / B1 の発火・連続問題日数（L1〜L3）・要レスキューの L3 判定が同時に動きます。70.0 の扱いが変わった影響は実際に出ており、`lib/judge/__tests__/advice.test.ts:150-155` が「70.0% では B1 が出なくなった」をテストとして記録しています（`lib/judge/advice.ts` は1行も変えずに挙動が変わった例）。

---

### 5.3 ★表示は四捨五入、判定は丸める前の生値★

`displayRate()`（`lib/judge/grade.ts:43-46`）は `Math.round()` で整数にして `%` を付けるだけの**表示専用**の関数です。`null` は `-` を返します。判定には絶対に使いません（[CLAUDE.md](../CLAUDE.md) §4.3）。

- **89.5** → 画面には `90%`、判定は **○**（`grade.test.ts:28` / `:52`）
- **90.4** → 画面には `90%`、判定は **◎**（`grade.test.ts:49`）

つまり画面に同じ「90%」と並ぶ2行が、片方は ◎ で片方は ○ ということが**正常に**起こります。

★**事故の向きが D-139 で逆になりました**★（`lib/judge/grade.ts:7-11` / `grade.test.ts:44-57`）。旧規則では「90.4 を丸めると 90 になり ◎ が**消える**」側でしたが、新規則では 90 も ◎ なので消えません。いまは ★**無いはずの ◎ が丸めで生まれる**★ 側が事故です — `Math.round(89.5)` は `90` で、それを判定に渡すと ○ が ◎ に化けます（`grade.test.ts:53`）。69.5 → 70 で △ が ○ に化けるのも同じ形です（`:56`）。**どちらにせよ「丸めた値で判定しない」は変わりません。**

★**丸めの種類は2つあり、混同しないこと**★:

| 丸め | どこで | 何に | 規則 |
|---|---|---|---|
| ★切り捨て★ | `runCompletionRate()`（`lib/judge/kpi.ts:166-175`） | ★1回ごとの完了率だけ★ | 小数第1位まで残して切り捨て（判定の材料。TeamWorks のサマリと同じ式） |
| ★四捨五入★ | `displayRate()`（`lib/judge/grade.ts:43-46`） | 画面に出すときだけ | 整数に四捨五入（判定には使わない） |

平均した完了率は★丸めずに★判定し、★表示のときだけ★整数に四捨五入します。例: 1回ごとの完了率 89.9 と 90.0 の平均 89.95 は、画面に「90%」と出ますが判定は ○ です（`lib/judge/__tests__/run-completion-rate.test.ts:34` の M2）。

**変えると何が起きるか**: 丸めてから判定に渡しても、例外も出ずテストも（他のテストは）通ります。丸めた値も「正しそうな数字」だからです。誰も気づかないまま評価が1段階ずれ続けます。

---

### 5.4 清掃完了率 — ★1回ごとの完了率の単純平均（2026-10-02 から。2026-10-05 に確定）★

★**清掃完了率は「1回ごとの完了率」の単純平均（清掃1回＝1票）です**★（★D-155。ユーザー判断「一旦B」2026-10-02★ / 指示書AS。★第11回 P-2=A で確定。D-158★）。

```
1回ごとの完了率 = min(clean_area ÷ task_area × 100, 100) を小数第1位まで切り捨て
清掃完了率       = Σ 1回ごとの完了率 ÷ 1回ごとの完了率が null でない清掃の回数
```

- `clean_area` … `clean_task_reports.clean_area`（清掃した面積。`prisma/schema.prisma:534`）
- `task_area` … `clean_task_reports.task_area`（清掃対象の面積。`prisma/schema.prisma:533`）
- どちらも実DDLは `double(12,6)`（小数6桁まで持てる数値）です（D-094）

画面ごとに「どの行を数えるか」の違いは[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)を見てください。ここでは式と、その根拠を説明します。

#### 決めている場所（★2つの関数だけ★）

| 決めている場所 | 何を決めているか |
|---|---|
| ★`runCompletionRate()`（`lib/judge/kpi.ts:166-175`）★ | ★1回ごとの完了率を決める唯一の関数★（指示書AS D1）。★`task_area` が 0 なら 0（第11回 P-5=B。旧: `null`）★。★戻り値は `number`（`null` を返さない。指示書AU）★ |
| `RUN_COMPLETION_RATE`（`lib/config/index.ts:36-41`） | 式の部品 — 上限 `cap: 100` / 残す小数の桁 `decimals: 1`（指示書AS D2） |
| `FLOAT_ERROR_DIGITS = 5`（`lib/judge/kpi.ts:131`） | 浮動小数の誤差の吸収幅（下の「誤差の吸収」）。★仕様の閾値ではなく計算の都合なので `lib/config` には置いていない★（`kpi.ts:127-130`） |
| ★`meanCleanRatio()`（`lib/judge/kpi.ts:122-125`）★ | ★複数回を畳む（単純平均する）唯一の関数★。件数0なら `null` |
| `completionRateFromReport()`（`lib/db/clean-log-mapping.ts:203-210`） | 清掃1回ぶん（清掃詳細ログの1行）の完了率。★レポート行が無ければ `null`、あれば `runCompletionRate()` を呼ぶだけ★ |

★**完了率を出している画面はすべて、この2関数を通ります**★ — 清掃詳細ログの1行・エリア別完了率（行と全体）・レポートの当期KPI・施設別パフォーマンスの当期・総清掃面積・要レスキュー R-3（`kpi.ts:91-96` の docstring が列挙）。2箇所で別々に畳むと、同じ施設がレポート画面と総清掃面積画面で違う完了率になります。

#### ★なぜ `clean_task_reports.clean_ratio` 列を使わないのか / なぜ暫定なのか★

`clean_task_reports` には `clean_ratio`（清掃率）という列があり、D-151（2026-09-30）ではこの列の平均を完了率にしていました。しかし TeamWorks に「この列はどう作られているか」と照会したところ、★「**計算式が間違っているので、訂正したら回答する**」★という回答でした（第10回 N-3。`docs/handoff/TeamWorks回答_第10回_20261002.md:67-69`）。式が間違っていると先方が認めている列を、判定の材料にはできません。

同じ回答の中で、TeamWorks は★サマリ（確定期間の `*_summaries.clean_ratio`）の作り方★を「清掃1回毎に `clean_area / task_area * 100`（100より大きい値は100、小数点第2位以下切捨て）で計算し、それを合算して平均」と説明しました（同 `:71-76`）。★当期（アプリがその場で集計する分）をこの式にそろえれば、前期比（サマリ）と当期（アプリ）が同じ式どうしの比較になります★。そこでユーザーは「一旦B」（この式にそろえる案）を選びました（D-155 項1）。却下した案A（`clean_ratio` 列の平均を続け、サマリ側を直してもらう）は、確定期間と当期で式が違い、前期比が「別の式どうしの差」になるためです。

★**2026-10-05 に確定しました**★（第11回 P-2=A「今の式のまま」。P-1: 訂正後の `clean_ratio` 列もこの式と同じ値になる。D-158）。以下は暫定だったときの作りで、いまも残しています — 式を変えるときに直すのは ★`runCompletionRate()` の中身だけ★で済むよう、呼び出し側は `clean_task_reports` の行（`clean_ratio` も持っている）を分解せずにそのまま渡す約束です（`kpi.ts:120-123` / 指示書AS D6）。`clean_ratio` 列は完了率には使っていませんが、型・`select`・サンプルデータからは消していません。

#### 1回ごとの完了率の例（期待値表 `lib/judge/__tests__/run-completion-rate.test.ts:13-27`）

| # | 清掃した面積 | 対象面積 | 1回ごとの完了率 | 確かめていること |
|---|---|---|---|---|
| R1 | 50 | 100 | 50.0 | 基本 |
| R3 | 150 | 100 | ★100.0★ | 上限 100 で切る |
| R5 | 89.95 | 100 | ★89.9★ | 四捨五入ではなく切り捨て |
| R6 | 0.57 | 1 | ★57.0★ | 浮動小数の誤差（56.99999…）で 56.9 にしない |
| R8 | 0 | 100 | 0.0 | 「0%で実施」（未実施ではない） |
| R9 | 10 | 0 | ★0.0★ | 対象面積 0 は 0%（★第11回 P-5=B。D-158 / 指示書AU 表1。旧: `null`★） |
| R12 | 0 | 0 | ★0.0★ | 両方 0 も 0%（指示書AU 表1） |
| R11 | 898.892509 | 1377.39502 | 65.2 | 65.26… を切り捨て（実データの桁） |

#### 誤差の吸収 — `0.57 ÷ 1 × 100` は 57 にならない

コンピュータの小数は2進数で持つため、`0.57 / 1 * 100` は `56.99999999999999` になります。これをそのまま「小数第1位まで切り捨て」ると 56.9 になってしまいます。`runCompletionRate()` は切り捨ての前に、★小数第6位で一度四捨五入してから★切り捨てます（`lib/judge/kpi.ts:173-174`）。

```ts
const absorbed = Math.round(raw * 10 ** (decimals + FLOAT_ERROR_DIGITS)) / 10 ** FLOAT_ERROR_DIGITS;
return Math.floor(absorbed) / 10 ** decimals;
```

つまり ★1e-6 ポイント（0.000001%）未満の差は誤差とみなします★（指示書AS D3）。帰結として、真の値が 89.9999995 のような「境目のごく近く」の値は 90.0 に繰り上がりますが、`double(12,6)` の面積からその桁の差が意味を持つことはない、という判断です。★`Math.floor(raw * 10) / 10` と書かないこと★（R6 が 56.9 になる）。★`toFixed()` は四捨五入なので使わないこと★（`kpi.ts:150-152`）。

#### ★承知している弱点（変えないこと）★

`clean_area` は★同じ場所を何回も清掃した面積の総和★です（第10回 N-4「clean_area は清掃した面積の総和」。`TeamWorks回答_第10回_20261002.md:79-81`）。そのため面積比は「どれだけ網羅したか」ではありません。
ユーザー提供CSV（`clean_task_reports.csv` 189行。2026-09-30 受領。★先方投入の実データ★。第10回 N-10 で「Pudu Cloud から取得した実データ」と回答）では ★`clean_area > task_area` が 129行（68.3%）★あり、★それらは全部 100% になります★（◎ が多めに出る）。
ユーザーはこれを承知で採用しました（D-155 項1）。★上限 100 を外したり、別の補正を入れたりしないこと★（`kpi.ts:158-161`）。

#### ★どの清掃を数えるか — 3つの条件★

| 条件 | 扱い | 根拠 |
|---|---|---|
| ★本清掃（`clean_timetables.precheck = 0`）に対応する行だけ★ | 事前チェック（`precheck` が 60 / 30 など）の予定に紐づく行は数えない | TeamWorks 第10回 N-3「事前確認の結果は含めない。`precheck` が0のデータのみ集計対象」（`TeamWorks回答_第10回_20261002.md:75`）。`CLEAN_TIMETABLE_PRECHECK.main = 0`（`lib/config/index.ts:272-275`） |
| ★予定外（`clean_timetable_id` が NULL か 0）も1件として入れる（今の期間だけ）★ | 予定 +1・開始 +1・`status = 4` なら正常終了 +1・1回ごとの完了率を平均に入れる。総清掃面積の面積の合計にも入る。★確定期間は TeamWorks のサマリのまま（サマリは予定外を除く）★ | D-155 項3 → **D-187**（指示書BG・A案。2026-10-29）。旧: 完了率・実施回数には入れない |
| ★`task_area = 0` の行は1回ごとの完了率を 0%★ | 分子に 0 を足し、分母（件数）にも入れる。「実施」にも数える | ★第11回 P-5=B（D-158）/ 指示書AU 段階2★。旧: `null`（分子にも分母にも入れない。D-155 項1 / 指示書AS D4） |

★予定外を数える仕組みは画面によって違います★（2026-10-29 から。D-187。旧: 予定から引くので「もともと入らない」）:

| 画面 | 予定外を数える仕組み |
|---|---|
| 清掃詳細ログ・ダッシュボード | 予定から引いた行に、`loadUnscheduledCleanLogRows()`（`lib/db/unscheduled-clean-log-source.ts`）で読んだ予定外の行を足す（期間は `started_at`）。ダッシュボードの直近の候補は、同じ清掃エリアで開始が同時刻なら予定のある行を採る（`preferOnTie()` `lib/dashboard/latest-run.ts:59`） |
| 連続日数・要レスキュー・連続問題施設 | `lib/db/consecutive-source.ts` が `UNSCHEDULED_TIMETABLE_ID_WHERE` で予定外を読み、施設×日付の結果に入れる（予定外だけの日もスケジュール存在日。`CLAUDE.md` §4.5） |
| レポート（今の期間）・エラー内訳 | `findUnscheduledReports()` / `findUnscheduledErrors()`（`lib/db/unscheduled-report-source.ts`）で足す。★確定期間はサマリのまま★ |
| ★総清掃面積（`/clean-area`）★ | 面積は全行の `groupBy`。完了率・実施回数は `scheduledTimetableId()` が null（0・NULL）の行を予定外1件＝実施1回として数える（`cleanAreaStatsFromRows()` `lib/db/clean-area-source.ts`） |
| アラート | ★出ない★ — 事前チェックの異常だけを出す（D-191） |

★予定は `clean_timetables` と `clean_timetable_histories` の両方から読みます★（TeamWorks 第10回 N-3「`clean_timetables` もしくは `clean_timetable_histories` の precheck が 0」/ 第11回 P-7=C。D-158 項4。指示書AU 段階3b。2026-10-05 から）。同じ id は `clean_timetables` 側を採ります（読み口は `lib/db/timetable-source.ts` の1か所。章2 の 2.4 の注）。

#### ★未実施と「`task_area = 0`」は別物★（D-156 項1）

| 清掃 | 出発したか（`departed`） | 1回ごとの完了率 | 実施回数 | 完了率の件数 | 未実施（出発失敗）に数えるか |
|---|---|---|---|---|---|
| レポート行が無い | `false` | `null` | 数えない | 数えない | ★数える★ |
| 行があり `task_area = 0` | ★`true`★ | ★0.0★（旧: `null`） | ★数える★ | ★数える★（旧: 数えない） | ★数えない★ |
| 行があり `task_area > 0` | `true` | 0.0〜100.0 | 数える | 数える | 数えない |

★**未実施は `completion_rate` ではなく `departed`（レポート行の有無）で数えます**★（要レスキュー R-1・連続問題施設の出発失敗日・ダッシュボードの「未実施」と「進捗」）。
`completion_rate === null` で数えると、レポート行がある清掃まで「出発失敗」になり、現場に誤った連絡が出るためです（D-156 項1 で却下した案C）。
不変条件は「`departed === false` なら `completion_rate === null`」です。★2026-10-05（指示書AU。P-5=B）から逆も成り立ちます★（`completion_rate === null` ⟺ レポート行が無い）。それでも★未実施は `departed` で数え続けます★（指示書AU D3）。

★ただし判定（◎○△×）と連続日数は完了率で見ます★。`task_area = 0` の清掃は完了率 0% ＝ 判定 × になり、ダッシュボードでは「失敗」、連続日数では「問題日」に入ります（`isProblemCompletion()` `clean-log-mapping.ts:332-334`）。

★**未実施（結果の行が無い清掃）は完了率の分子にも分母にも入れません**★（0% として混ぜない）。理由は ★稼働率で既に数えている失敗を、完了率でも二重に数えないため★です（`lib/db/report-mapping.ts:436-441`）。レポートの当期集計では `aggregateCleanStatsByFacility()`（`lib/db/report-mapping.ts:460-505`）が「実施と数えた行 **かつ** 結果行がある」ときだけ `runCompletionRate()` を呼び（`:492`）、`null` でなければ Σ と件数に足します（`:500-501`）。件数が 0 なら `null`（「0%で清掃した」ではなく「率を出せる清掃が1回も無い」）です。

#### なぜ「期間の合計どうしの面積比」には戻さないのか

2026-09-29 までは ★Σ清掃面積 ÷ Σ清掃対象面積★（期間の合計どうしの面積比）でした。D-151 でこれを廃止しました。ユーザー提供CSV（189行。先方投入）で、合計どうしの面積比 90.2% に対し `clean_ratio` 列の平均は 78.5%、個別行でも「面積比 13.8% ↔ `clean_ratio` 100.0%」「面積比 162.8% ↔ `clean_ratio` 61.0%」と相関しなかったためです（D-151 項3）。

D-155 の式は ★1回ずつ割って上限 100 で切ってから平均する★ので、合計どうしで割る旧方式とは別物です。★Σ`clean_area` ÷ Σ`task_area` を「完了率」と呼ばないこと★（`kpi.ts:114-117`）。面積の合計そのものは総清掃面積画面の**表示用**に残っています。

#### なぜ「面積で重み付けした平均」にしないのか

重みに使う `task_area` が実清掃面積を下回る行が約7割（上の 68.3%）あるため、★加重すると面積が別経路で完了率に効いてしまう★からです（`kpi.ts:98-104`）。ユーザーには具体例（1,000㎡×90% と 100㎡×10% → 単純平均 50.0% △ / 面積加重 82.7% ○）を示し、「単純平均でよいです」（2026-10-01）で確定しています（D-151 項4-3）。`lib/judge/rescue.ts` は、要レスキュー判定の入力型 `RescueRowInput`（`:61-86`）から面積の項目を**わざと外して**います — ★「面積で重み付けし直す」実装を型のレベルで書けなくするため★です。

#### なぜ「平均」ではなく「Σと件数」を持ち回るのか

★率は加重平均できない★からです（`kpi.ts:49-51`）。施設ごとの平均を足して割ると「平均の平均」になり、清掃回数の違いが無視されます。そこで `CleanRatioTally`（`kpi.ts:77-85`）は `cleanRatioSum`（1回ごとの完了率の合計）と `cleanRatioCount`（件数）の2つだけを持ち、★施設・顧客・ホールディングスをまたいで合算しても平均が壊れない★形にしてあります。合算は `sumCleanStatsBuckets()`（`lib/db/report-mapping.ts:333-339`）や `aggregateCleanArea()`（`lib/clean-area/aggregate.ts:165-248`）が、割り算は `meanCleanRatio()` だけが行います。

名前は `cleanRatioSum` / `cleanRatioCount`（`clean_ratio_sum` / `clean_ratio_count`）のままですが、★中身は `clean_ratio` 列ではありません★（`kpi.ts:45-47`。指示書AS D7 — 名前を変えると16ファイルの機械的な差分が中身の変更に混ざり、レビューで読めなくなるため。命名した当時は暫定の式だった。★式は第11回 P-2=A で確定★）。

#### 落とし穴3つ

- ★1回ごとの完了率は**すでに百分率**★なので、`× 100` する共通関数 `rate()` を通してはいけません（`kpi.ts:111`。180/2 は 90 であって 900 ではない。`lib/judge/__tests__/kpi.test.ts:94-96`）
- ★切り捨てを `Math.floor(raw * 10) / 10` と書かないこと★（上の「誤差の吸収」）
- ★`clean_area = 0`（`task_area > 0`）を `null` にしないこと★。「0%で実施した」が「未実施」に化けますが、判定はどちらも × なので画面もテストも赤くなりません（`clean-log-mapping.ts:187-190`）

**変えると何が起きるか**: 2箇所で別々に畳むと、同じ施設がレポート画面と総清掃面積画面で違う完了率になります（`kpi.ts:91-96`）。★完了率を複数行から出すときは、画面を問わず `runCompletionRate()` → `meanCleanRatio()` を通してください★。

---

### 5.5 他の KPI 3つ — `lib/judge/kpi.ts`

3指標はいずれも内部の共通関数 `rate(分子, 分母)`（`kpi.ts:30-33`。分母 0 なら `null`、それ以外は `分子 ÷ 分母 × 100`）を呼ぶだけの薄い関数です。

| 指標 | 関数 | 場所 | 計算式 | 当期（アプリが集計）の分子・分母 |
|---|---|---|---|---|
| 稼働率 | `operationRate(completed, planned)` | `kpi.ts:36-38` | ★**正常終了（`status = 4`）の回数** ÷ 清掃**予定**回数 × 100★ | 分子＝最新のレポート行の `status` が 4 の本清掃の数（`isCompletedTask()`。面積は見ない）/ 分母＝過去の本清掃の予定の数 |
| 出発成功率 | `launchRate(started, planned)` | `kpi.ts:44-46` | ★**開始できた回数**（レポート行がある回数）÷ 清掃**予定**回数 × 100。時刻の条件なし★ | 分子＝レポート行がある本清掃の数 / 分母＝稼働率と同じ |
| エラー発生率 | `errorRate(errors, planned)` | `kpi.ts:182-184` | ★エラー発生回数 ÷ 清掃**予定**回数 × 100。上限で切らない（100 を超えうる）★ | 分子＝本清掃の予定を指す `robot_error_reports` の行数 / 分母＝稼働率と同じ予定の数 |

★3つの式は TeamWorks のサマリ（`operation_ratio` / `launch_ratio` / `error_ratio`）と同じです★（第11回 P-6=A/A/B。ユーザー指示 2026-10-05「合わせてください」。D-158。指示書AU 段階3）。旧（〜2026-10-05）: 稼働率 ＝ 実施 ÷ 予定・出発成功率 ＝ 同じ値・エラー発生率 ＝ ÷ 実施回数。
★帰結: 当期の稼働率は以前より低めに出ます★（例: 予定10・開始9・正常終了7 → 90% から 70%）。施設ステータスの閾値（`FACILITY_STATUS_THRESHOLDS` good 90 / warning 75）は★据え置き★です（ユーザー判断 2026-10-05。D-158 項8）。

当期の分子・分母を数えているのは `aggregateCleanStatsByFacility()`（`lib/db/report-mapping.ts:460-505`）、4指標に変換するのは `kpiFromCleanStats()`（同 `:516-535`）です。確定期間（先週・先月・前年）はサマリテーブルの率4列をそのまま使い、★詳細テーブルから再計算しません★（`kpiFromSummaryRatios()` 同 `:231-241`。D-102）。

★**分母が 0 のときは `null`（0% ではない）**★（`kpi.ts:31`）。これは意図的で、「その期間に清掃予定が1件もなかった」と「予定はあったが全部失敗して 0% だった」を区別するためです（`kpi.test.ts:30-33`）。0% と表示すると、全店定休日の週が「大失敗した週」に見えます。完了率（`meanCleanRatio()`）も同じ規則で、件数 0 は `null` です。画面では `-` と出ます。

補助関数が2つあります。

| 関数 | 場所 | 役割 |
|---|---|---|
| `toKpiValue(value, prev)` | `kpi.ts:196-202` | 前期比を組み立てる。どちらかが `null` なら `diff` も `null`（0 にしない）。画面は ▲▼ の行ごと出さない |
| `judgeFacilityStatus(稼働率)` | `kpi.ts:224-231` | 良好 / 注意 / 要改善 / 判定なし。`FACILITY_STATUS_THRESHOLDS`（`lib/config/index.ts:553-558`。good 90 以上 / warning 75 以上 / それ未満は要改善）。★`null` は「要改善」ではなく「判定なし」★（決定1。2026-09-11 ユーザー確定）。★これは DFA 側の画面上の判断で、TeamWorks との合意事項ではない★（`kpi.ts:204-215`） |

#### ★注意1: 当期の稼働率は出発成功率以下になる★（2026-10-05 改訂）

★旧版の「当期は稼働率と出発成功率が同じ数字になる」は、指示書AU（2026-10-05）で解消しました★。稼働率の分子は★正常終了（`status = 4`）の回数★、出発成功率の分子は★開始できた回数（レポート行がある回数）★になり、★正常終了は開始の一部なので、稼働率 ≤ 出発成功率★が必ず成り立ちます（第11回 P-6=A。D-158）。
★サンプルデータ（作り値）もこの関係を守ります★ — 施設6 の出発成功率を 65.0 / 前期 68.0 から ★75.0 / 71.0★ に直し（`lib/fixtures/reports.ts`）、全行で 稼働率 ≤ 出発成功率 を確かめるテストを足しました（`lib/fixtures/__tests__/reports.test.ts`。指示書AU §11 Q5）。

#### ★注意2: サマリに「実施回数」の列が無い — 稼働率は検算できない★

`customer_summaries` / `facility_summaries` には `task_count`（予定回数）・`launch_count`・`error_count` はありますが、★**実施回数の列がありません**★（`prisma/schema.prisma:694-696` / `:728-730`。構造＝実DDL）。率4列（`operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio`）は実在します（`:707-710` / `:741-744`）。

帰結は2つです（[CLAUDE.md](../CLAUDE.md) §4.7）。

1. ★稼働率（`operation_ratio`）は、分子（正常終了の回数）が DB に無いので検算できません★
2. ★複数顧客の合算ができません★ — 率は加重平均できず、分子・分母のどちらかが欠けている指標があるためです。だからレポートの KPI は★1社に定まるときだけ★出し、2社以上が候補になると 422（指定が足りない）を返して「顧客を選んでください」と表示します（`lib/reports/target-customer.ts:1-40`。D-133）

| 指標 | サマリの分子 | サマリの分母 | 合算できるか |
|---|---|---|---|
| 清掃完了率 | ★1回ごとの完了率の合計が無い★ | ★件数が無い★ | できない |
| 出発成功率 | `launch_count` | `task_count`（★第11回 P-6=A で確定★） | 材料はある |
| 稼働率 | ★列が無い（正常終了の回数）★ | `task_count` | できない |
| エラー発生率 | `error_count` | ★`task_count`（第11回 P-6=B）★ | 材料はある |

（`lib/reports/target-customer.ts:10-15` の表を写したもの。★4指標を揃えて出せない（稼働率・清掃完了率の材料が無い）ので、合算しない方針は変えません★。指示書AU §11 Q3。D-158）

#### ★注意3: エラー発生率は「本清掃の予定を指すエラー」だけを数える★

分子は★本清掃の予定（`precheck = 0`）を指す `robot_error_reports` の行★だけです（`errorCountByMainTimetable()` `lib/db/report-mapping.ts:380-392`）。事前チェックの予定を指すもの・`clean_timetable_id` が NULL のもの・指す先の予定が無いものは数えません。根拠は TeamWorks 第8回 A7「事前チェックは本清掃を円滑に実施できるためのものなので、サマリには含めない」（D-114 項2）と、第10回の Q4-1（事前チェックのエラーは除く＝A）・Q4-2（`clean_timetable_id` が NULL のエラーは数えない＝B）です。

★制御盤・セキュリティのエラーの扱いは次フェーズです★（D-158 当方の対応5）。第11回の回答で、制御盤・セキュリティのエラーと「清掃を開始できなかった」エラーも `robot_error_reports` に入り（`robot_id` は NULL。P-8）、`clean_timetable_id` も入る（P-9=A）と分かりました。区分の見分け方と D-Security アラートの取得元の切り替え（P-10）は次フェーズで扱います。
★いまの備え★: 先方の DDL では `robot_id` が NULL 可になった（P-8 b）が `prisma/schema.prisma` は NOT NULL のままなので、★`robot_error_reports` の読み取りはすべて `select` で列を絞り、`robot_id` / `robot` を読みません★（`lib/db/robot-error-select.ts`。指示書AU 段階0-4）。全列を読むと、NULL の行が1件入っただけで読み取りが落ちます。

**変えると何が起きるか**: `rate()` が `null` の代わりに 0 を返すようにすると、予定のない期間が全部「稼働率 0%・要改善」になり、実害のある施設が埋もれます。

---

### 5.6 L1〜L3 のエスカレーション — `lib/judge/escalation.ts`

| 関数 | 場所 | 役割 |
|---|---|---|
| `countConsecutiveProblemDays()` | `escalation.ts:32-39` | 配列の**末尾（最新日）から遡って**問題が何日続いたか数える。最新日が問題なしなら 0 |
| `judgeEscalationLevel()` | `escalation.ts:52-59` | 0日→`null` / 1日→`L1` / 2日→`L2` / 3日以上→`L3`。閾値は `ESCALATION_THRESHOLDS`（`lib/config/index.ts:43-48`。`level2Days: 2` / `level3Days: 3`） |
| `judgeEscalation()` | `escalation.ts:62-68` | 上の2つをまとめて呼ぶ入口 |

「問題あり」とは ★△以下 または 出発失敗★です（`escalation.ts:22`）。実際の判定は `isProblemCompletion()`（`lib/db/clean-log-mapping.ts:332-334`）で、★完了率が `null`（未実施）または 70 未満★を問題日にします（`task_area = 0` は 0% なので 70 未満に入る）。

#### ★「連続」はカレンダー日ではなくスケジュール存在日で数える★

清掃予定が入っていない日は、連続を途切れさせません（`escalation.ts:7-15`。D-026）。

具体例。月・水・金だけ清掃する施設で、8/3(月)・8/5(水)・8/7(金) の3回すべてが失敗したとします。カレンダー上は5日間ですが、間の火・木はそもそも清掃予定がないので数に入れません。★**スケジュール存在日の連続は3日なので L3**★ です（`lib/judge/__tests__/escalation.test.ts:54-70`）。もし火・木を「問題なし」として配列に入れると、そこで連続が途切れて **L1** に落ちます（同 `:72-83` が、この誤りをアンチパターンとしてテストに固定しています）。

★**「どの日が清掃予定日か」を判断するのはこの関数ではありません**★（`escalation.ts:13-15`）。「予定日だけに絞り込み済みの配列を、古い順で」渡すのはデータ取得側の責任です。実際には次の3段で材料が作られます。

1. `loadConsecutiveInputs()`（`lib/db/consecutive-source.ts:388-463`）— 本清掃の予定と結果を、★表示期間より広く★読む（章5-10）
2. `foldProblemDaysByDate()`（`lib/db/clean-log-mapping.ts:404-416`）— ★同じ施設・同じ日の複数行を1日に畳む★。1台でも問題なら、その日は問題日（OR。ユーザー確定 2026-09-11 / D-109）
3. `computeConsecutiveProblemDays()`（同 `:463-489`）— 施設ごとに、各行の日付までの連続を数える。★分割キーは施設（`facility_id`）★（[CLAUDE.md](../CLAUDE.md) §4.5「同一施設で」）

★表示上限★: 画面に出す連続日数は `capConsecutiveDays()`（`lib/format/consecutive-days.ts:32-38`）で上限（既定 30。システム管理画面で変更可。`CONSECUTIVE_DAYS_DISPLAY` `lib/config/index.ts:150-157`）をかけ、超えたら「{n}日以上」と出します。★L1〜L3 と R1〜R4 は上限をかける前の生値で判定します★（[CLAUDE.md](../CLAUDE.md) §4.3 と同じ考え方）。

**変えると何が起きるか**: 渡す配列に予定のない日を混ぜると、L3 になるべき施設が静かに L1 に見えます。エラーは出ません。

---

### 5.7 要レスキュー店舗 — `lib/judge/rescue.ts`

毎朝いちばんに見るべき施設を選ぶ判定で、★3条件のどれかに当たれば要レスキュー★、★当たった理由は全部返す★（1つに丸めない）作りです（`judgeRescue()` `rescue.ts:268-296`）。

| # | 条件 | 実装 | 閾値 |
|---|---|---|---|
| R-1 | 未実施が3日以上連続（＝出発失敗と同義） | `countNotExecutedStreak()`（`rescue.ts:202-209`）。★未実施は `departed === false` で数える★（D-156 項1） | `RESCUE_THRESHOLDS.notExecutedDays = 3`（`lib/config/index.ts:87-90`） |
| R-2 | 連続問題レベルが L3 | `isEscalationL3()`（`rescue.ts:218-220`）。★`>= 3` と書き直さず `judgeEscalationLevel()` を呼ぶ★ | `ESCALATION_THRESHOLDS.level3Days` |
| R-3 | 完了率が前回比 20% 以上の急落 | `judgeCompletionDrop()`（`rescue.ts:246-255`） | `COMPLETION_DROP.dropRatio = 0.2`（`lib/config/index.ts:65-68`） |

- ★R-1 の 3 と L3 の 3 は別の設定値です★。たまたま同じ値ですが、片方を変えたときにもう片方まで動かないように分けてあります（`lib/config/index.ts:77-81`）
- ★**R-3 は「比」であって「差」ではありません**★（ユーザー確定 2026-09-15「比で」）。式は `(前回 − 当日) ÷ 前回 ≧ 0.2`。前日 30% → 当日 18% は「12ポイント低下」ですが「40% 低下」で、ユーザーは後者を拾う側を選びました。★「前回」はカレンダー前日ではなく前回のスケジュール存在日★です。前回が `null`・当日が `null`・前回が `0` のときは算出しません（`rescue.ts:250-252`。未実施を 0% とみなさない）
- ★**R-3 の完了率も `meanCleanRatio()` を通します**★（`facilityDayRate()` `rescue.ts:135-143`）。その日の★1回ごとの完了率の単純平均★で、レポート画面・総清掃面積と同じ定義です（D-155）。★以前は面積加重でした★。判定が変わるケースは `lib/judge/__tests__/rescue.test.ts:548` 以降の「表6」に、★R-1 を `departed` で数えるケース★は `:219-296` の「表5（指示書AS）」に記録されています
- 同じ日に複数行（施設に複数台）あるときは `foldRescueDays()`（`rescue.ts:160-189`）が1日に畳みます。★未実施フラグは OR、完了率は `completion_rate` が `null` でない行だけの合計と件数★ — 片方の台だけ未実施の日は「未実施の日」であり、かつ完了率も算出できます

組み立てと並べ替えは `buildRescueRequired()`（`lib/db/rescue-mapping.ts:105-198`）です。評価日は「その施設の、指定日以前で最も新しいスケジュール存在日」、並び順は「理由の数の降順 → 連続問題日数の降順 → 施設 ID の昇順」です。★件数では切りません★（画面が最初の5件だけを見せ、「もっと表示」で全件。`RESCUE_INITIAL_VISIBLE = 5` `lib/config/index.ts:119`）。

**変えると何が起きるか**: ここで連続日数を数え直すと、清掃詳細ログと食い違います（`rescue.ts:38-40`。D-109 の再発防止で、連続日数は引数で受け取るだけ）。

---

### 5.8 対応アドバイス（R1〜R4 / B1〜B3）— `lib/judge/advice.ts`

#### ★発火条件と文言は別のファイルにある★

| 何を直したいか | 触るファイル | 画面から編集できるか |
|---|---|---|
| **どんなときに出すか**（発火条件） | `lib/judge/advice.ts:59-106`（`f2b91fc`） | できない。コード変更＋テスト修正が必要 |
| **何と書くか**（見出し・推定原因・推奨アクション） | `lib/config/advice-rules.ts`（既定値）／`data/settings/advice.json`（上書き） | ★できる★。システム管理の「運用設定」画面から編集 |

文言を変えてもテストは1本も壊れません（`lib/judge/advice.ts:12-15`）。

#### 発火条件の表（`matchAdviceRules()` は該当したものを**すべて**返す）

| # | 発火条件（実装） | 場所 | レベル |
|---|---|---|---|
| R1 | 出発失敗 **かつ** 連続問題日数が 1 以下（初回のみ。2日目以降は R3/R4 に任せる） | `advice.ts:64-66` | 1 |
| R2 | 完了率が `null` でなく、`GRADE_THRESHOLDS.poor`（10）未満。未出発は `null` なので対象外 | `advice.ts:69-71` | 1 |
| R3 | 連続問題日数 **2 以上**（`ESCALATION_THRESHOLDS.level2Days`） | `advice.ts:74-76` | 2 |
| R4 | 連続問題日数 **3 以上**（`level3Days`）。R3 と同時に出るのは意図的 | `advice.ts:79-81` | 3 |
| B1 | 完了率が `null` でなく、`judgeGrade()` が **△** | `advice.ts:84-86` | 1 |
| B2 | 事前チェック1回目 or 2回目のステータスが `B2_HANDLE_UP_STATUSES` に一致（D-178 で文言を改めた。旧「1時間前または30分前」）。★`null`（その回の事前チェックの予定が無い。D-171）はどのルールにも当たらない★ | `advice.ts:89-91` | 1 |
| B3 | 同じく `B3_OFFLINE_STATUSES` に一致（`null` は当たらない） | `advice.ts:92-94` | 1 |

- ★`task_area = 0` の清掃★は出発しているので R1 は出ません。★完了率 0%（×）なので R2 が出ます★（2026-10-05 から。旧: `null` で R2 も B1 も出なかった）。連続が 2 日以上なら R3 / R4 も出ます
- 連続問題日数は施設単位です。★別の台のせいで施設の連続が 2 日以上になれば、正常終了した行にも R3/R4 が出ます★。逆に別の台のせいで、その台にとって初回の出発失敗でも R1 が出ないことがあります（`lib/db/clean-log-mapping.ts:435-438`。意図した結果）
- 比較は前後の空白を除去し大小文字を無視します（`matchesStatus()` `advice.ts:105-116`。`null` / `undefined` は一致しない）。レベル（1=注意 / 2=警告 / 3=重大）は `lib/config/advice-rules.ts` の各ルールが持ち、`maxAdviceLevel()`（`advice.ts:118-122`）が最大値を取って、清掃詳細ログの展開部分の見出し（「レベル3 — DFA介入を検討」など。`components/features/detail-log/CleanLogTable.tsx:43-63`）を決めます

★**B3 は 2026-10-05 時点でも空配列のままです**★（`lib/config/robot-status-match.ts:46`。確認済み）。★どんな値を渡しても絶対に発火しません★ — オフラインを表す値が実データで未判明のため、推測で埋めない方針です（`robot-status-match.ts:37-45`）。B2 の `["Handrail Lifted"]`（`:35`）も★暫定値★で、受領済みの `error_type` 24値のリストには含まれていません（`:28-34`）。

★**B2 の根拠は、エラーコードが決まったら本清掃のエラー内容に切り替えます**★（**D-174**。2026-10-07 ユーザー了承「その進め方でいいです」。きっかけは D-171 項3）。本清掃時にハンドルアップしていれば清掃開始失敗として `robot_error_reports` のエラー内容に載る、というユーザーの見立てによります。いまは事前チェックの一時的なハンドルアップでも出ます。★それまでは今のまま★（事前チェックの状態を見る。予定が無い回は `null` で当たらない。△× の行を開いたときだけ出て、判定には影響しない）。エラーコード（`error_type` の値）はユーザーが情報収集中です。

**変えると何が起きるか**: `B3_OFFLINE_STATUSES` に値を1つ足せば B3 が復活します。同時に `lib/judge/__tests__/advice.test.ts:211-221` のテストが落ちますが、★「落ちたら値を足した合図であり、バグではない」★ とテスト内に明記されています。

---

### 5.9 ダッシュボードの集計 — `lib/dashboard/aggregate.ts`（123行）

日次のダッシュボード専用の集計で、こちらも純粋関数のみです。★`lib/judge/kpi.ts` は使いません★（`aggregate.ts:6-8`）。kpi.ts は週次/月次/年次の集計期間の KPI、こちらは日次の**件数カウント**で、混ぜると分母の意味が変わるためです。

| 関数 | 場所 | 役割 |
|---|---|---|
| `computeSummary()` | `aggregate.ts:41-76`（`a961829`。★ファイルは D-173 で140行★） | 成功 / 失敗 / 未実施 / ★結果待ち（`pending`）★ / 成功率。★結果待ちの行は `departed` より先に見て `pending` に数え、他の3つにも成功率の分母にも入れない★（D-173。`:49-52`）。★未出発（`departed === false`）の行は判定を見ずに未実施に入れて打ち切る★（二重計上を防ぐため）。成功率の分母は3つの合計で、0 なら `null` |
| `computeScheduleProgress()` | `aggregate.ts:101-120` | スケジュール進捗。`planned` は★清掃ログではなくスケジュールマスタ（`clean_schedules`）由来★（`:107-111`）。ログの行数を使うと「本当に定休日」と「収集処理の障害」が区別できない。`completed` は「完了」ではなく**出発**した件数（`:112-113`。★結果待ちの行は出発していないので数えない。D-173 で変えていない★） |
| `computeSecurityBanner()` | `aggregate.ts:130-140` | セキュリティ復帰漏れバナー。施設名・エリア名・検知時刻の3つだけ返す（自由記述は載せない） |

成功／失敗の分類は `SUCCESS_GRADES = ["◎","○"]`（`aggregate.ts:26`）と `FAILURE_GRADES = ["△","×"]`（`:28`）という定数で決まります。★`SUCCESS_GRADES` は D-171 で `export` になり、アラートの `main_run_succeeded`（`mainRunSucceededFromReport()` `lib/db/precheck-mapping.ts:339`）も同じ定数で「本清掃は成功」を決めます★（別の場所に同じ配列を書かない）。★出発した `task_area = 0` の清掃は判定 × なので「失敗」に入ります★。

**変えると何が起きるか**: `SUCCESS_GRADES` に `△` を足すとダッシュボードの成功率が跳ね上がりますが、清掃詳細ログのバッジは △ のままなので、同じ日のデータが画面ごとに食い違います。★D-171 以降はアラートの「本清掃は成功」（`main_run_succeeded`）も一緒に変わります★。★`status_1h` / `status_30m` を `computeSummary()` の条件に足すと D-170 に反し、歯止めのテストが赤くなります★。

#### ★事前チェックは成功・失敗・未実施の判定に使わない（D-170。2026-10-07）★

ユーザー指示「事前チェックを行っていない清掃や事前チェックが失敗した清掃でも本清掃が成功していれば成功にしてください」。★調べた結果、すでにそうなっていました★（コードは変えず、歯止めのテストだけ足した）。

| 決めるもの | 見ているもの | 場所 |
|---|---|---|
| 成功 / 失敗 / 未実施（ダッシュボード） | ★`departed`（本清掃のレポート行の有無）と `grade`（完了率の判定）だけ★ | `computeSummary()`（`aggregate.ts:41-76`）。★結果待ち（D-173）は旗 `result_pending` で別に数える（5.9b）★ |
| 出発したか（未実施＝出発失敗） | 本清掃（`precheck = 0`）に対応する `clean_task_reports` の行があるか | `notExecutedFromReport()`（`lib/db/clean-log-mapping.ts:165`） |
| 連続日数・要レスキュー・レポートの KPI | 同じく `departed` と完了率 | 章5-4〜5-7 |

- ★`status_1h` / `status_30m`（1h前ST / 30分前ST）は表示だけの列★です。★事前チェックで異常が出ていても、予定が無くても、本清掃が ◎○ なら「成功」★。D-048（`status_1h` で失敗を分類しない）と同じ考え方です
- ★歯止めのテスト★: `lib/dashboard/__tests__/aggregate.test.ts:141-159`（`test.each` の4件。事前チェックで異常 `EmergencyStop` / `Offline`・1h前だけ異常・予定はあり異常なし `Standby.`・★予定が無い `null`★ でも ◎○ は success）。★4件目（`null`）はコミット `8a657a2` で足しました★（それまでは「予定が無い（実DBでは Standby. になる）」という D-171 前の名前の1件だった）。結果待ち（D-173）のテストは同じファイルの `:161-199`
- ★事前チェックに依存しているが判定ではないもの★（変えていない）: 1h前ST / 30分前ST の表示と CSV の列、アドバイス B2（△× の行を開いたときだけ。5.8）、アラート画面の事前チェック異常の行（本清掃が成功しても未解消のまま並ぶ。D-171 で「本清掃は成功」と添えて控えめに出すようになったが、対応状況・件数は変えない）
- ★ステージングの実測（2026-10-07・先方投入）★: レポート 87行のうち本清掃の予定に 80行・予定外（NULL）に 7行。★事前チェックの予定に紐づくレポートは0行★

★**何分前の事前チェックを画面に出すか**★（D-169。2026-10-07）: ★D-178（2026-10-16）で解消★: 清掃詳細ログの事前チェック2列は、設定 `PRECHECK_MINUTES_BEFORE` との一致ではなく★分数の大きい順（早い方が1回目）★で割り当てるので、★ステージングの本社の 30分前・10分前も 1回目 30・2回目 10 として出ます★（何分前かは `status_1h_minutes` / `status_30m_minutes`）。★旧（D-169 時点）: 60/30 と一致しない回は出ず「-」★。★アラート画面・エラー一覧は設定に無い分数（10分前など）も生の分数で出します★（D-105 項1・指示書AW D4）。表示する回を設定値で選べるようにするのは後で（ユーザー判断「このままで問題ない」）。

---

### 5.9b ★結果待ち — レポートがまだ届いていない清掃（D-173）★

★**改訂4で追加**★（D-173。2026-10-07。実装 `08ba9e9`、[CLAUDE.md](../CLAUDE.md) §4.4 の注記 `a961829`）。行番号は `a961829`。

**きっかけ**: レポート（`clean_task_reports` の行）は清掃の後 30 分より遅れて届くことがあります（ユーザー回答 2026-10-07。D-172）。それまでは、ダッシュボードの直近の清掃（終了＋30分で確定。D-159）以外のすべての経路で、★予定の開始時刻を過ぎた瞬間から★レポートの無い清掃を出発失敗として数えていました（清掃中も含む）。
ユーザー判断は「B」 — 一定時間はレポートが無くても「結果待ち」と出し、失敗に数えない。★待つ時間は一旦2時間・設定で変えられるように・全画面で揃える・ダッシュボードに「結果待ち」カードを足す★。

#### 定義（★この関数1本だけが決める★）

**結果待ち ＝ 本清掃のレポートが無く、予定の開始 ≤ 現在 ＜ 予定の終了（無ければ開始）＋ `RESULT_WAIT_MINUTES`（120分）**（清掃中も含む。境界ちょうどは確定＝結果待ちではない）。過ぎたら今までどおり出発失敗です。

| 何を | どこで |
|---|---|
| 判定 | ★`isResultPending()`（`lib/clean-logs/result-pending.ts:22`）★ — 純粋関数。期待値表は `lib/clean-logs/__tests__/result-pending.test.ts`（表1）。★`Date` は真の瞬間で渡す★（DB の値は `fromDbJst()` を通してから） |
| 待つ時間 | `RESULT_WAIT_MINUTES = 120`（`lib/config/index.ts:207`）。★`LAUNCH_GRACE_MINUTES`（30。未使用）とは別物で、流用しない★ |
| API の旗 | `CleanLogItem.result_pending: boolean`（`lib/types/index.ts:307`。契約は `d-watch-api.yaml:2114`）。★`departed` / `task_status` / `completion_rate` / `grade` の値は変えない★（結果待ちでも `departed: false`・`grade: "×"` のまま）— ★旗を見ない古い処理は安全側＝出発失敗のまま★になる |
| 旗を詰める | Prisma 版 `cleanLogs()`（`lib/db/index.ts:1141`。`clean_timetables.end_at` を読む）。★サンプルデータは旗を直接書く★（時刻で決めない。施設11 川下店の今日 03:30 の1行だけ。`lib/fixtures/clean-logs.ts:342-353`。スケジュール id 34 を足した `lib/fixtures/schedules.ts`） |

★**D-091 項1（当方で「予定＋猶予」を計算しない）は、この件に限りユーザー判断で上書き**★しました。D-091 の懸念（設定を変えると過去のログの判定まで変わる）は、待つ時間を過ぎた清掃には効かないので、影響は直近2時間に限られます。★第12回（★R-2★。`04f974a` で送付版）で、レポートが清掃の終了から最大でどれくらい遅れるかを聞きます★（`RESULT_WAIT_MINUTES` の見直し）。

#### 画面と集計での扱い（★全画面で揃える★）

| 画面・集計 | 結果待ちの扱い | 場所 |
|---|---|---|
| 清掃詳細ログの1行 | 出発「…」（`--tx-m`。✓✗ の色を使わない）・清掃結果「結果待ち」・完了率「-」・判定なし・★行を開けない★ | ★見え方は `cleanLogRowView()`（`lib/format/clean-result.ts:98`）の1か所★。表は `CleanLogTable.tsx:212-293`、文言は `CLEAN_RESULT_PENDING_LABEL`（`clean-result.ts:73`。EN "Pending"） |
| 清掃詳細ログの絞り込み | ★「問題あり（△×）」「×のみ」から外す★（「全件」には出る） | `filterCleanLogs()`（`lib/clean-logs/query.ts:182`） |
| 清掃詳細ログの CSV | 出発・清掃結果が「結果待ち」、★完了率・判定は空のセル★ | `buildCleanLogCsv()`（`lib/clean-logs/csv.ts:62-95`）。★画面と同じ `cleanLogRowView()` を使う★ |
| ダッシュボードの数値カード | ★「結果待ち」カード★に数える（成功・失敗・未実施に入れない・★成功率の分母に入れない★） | `computeSummary()`（`lib/dashboard/aggregate.ts:41-76`）、カードは `DashboardScreen.tsx:263-271`（`StatCard` の中立色 `mt`） |
| ダッシュボードの直近の清掃 | ★候補は「予定の終了 ≤ 現在」★（★`settleMinutes`（終了＋30分）は廃止★）。結果待ちも選ぶ | `pickLatestRunPerSection()`（`lib/dashboard/latest-run.ts:69`。条件は `:96`） |
| ダッシュボードのコンパクト版の表 | ★出さない★ | `app/api/dashboard/route.ts:222` |
| アラートの「出発失敗」 | ★出さない★（確定した出発失敗だけ出す） | Prisma 版 `prechecks()`（`lib/db/index.ts:1588`） |
| 連続問題日数（詳細ログ・要レスキュー・連続問題施設） | ★未来の予定と同じく入力から外す★（問題日にも非問題日にも数えない） | `consecutiveInputsFromRows()`（`lib/db/consecutive-source.ts:268`。`end_at` を読むようになった。約1.3MB 増）。サンプルデータ側は `lib/db/rescue-mapping.ts:61` |
| レポートの当期の率 | ★未来の予定と同じく数えない★（予定回数にも入れない。施設のバケットは作る） | `aggregateCleanStatsByFacility()`（`lib/db/report-mapping.ts:531`。本清掃の型は `CleanStatsTimetableLike` `:376`） |
| ★変えないもの★ | スケジュール進捗（結果待ちは出発していないので「完了」に数えない）・`main_run_succeeded`（レポート行が無ければ false）・清掃面積（実績だけを数える） | — |

**変えると何が起きるか**: `RESULT_WAIT_MINUTES` を延ばすと、確定した出発失敗が画面に出るまでが遅れます（アラートの出発失敗・ダッシュボードの未実施も同じだけ遅れる）。縮めると、レポートが遅れた清掃が一時的に出発失敗に見えます。★旗を見ずに `departed` や `grade` だけで「失敗」を数える処理を新しく書くと、結果待ちが失敗に混ざります★（例外もテスト失敗も出ない）。新しい集計を作るときは `isResultPending()` か `result_pending` を必ず見てください。

---

### 5.10 判定に渡す材料を作る係 — `lib/db/` ほか

判定の本体は `lib/judge/` ですが、★「どの行を数えるか」「何日ぶん読むか」は次のファイルで決まります★。どれも「DB から読んだ行を、判定が使える形に整える」係です。数字が合わないときは、判定よりもこちらを先に疑ってください。

| ファイル | 行数 | 役割 | 主な関数 |
|---|---|---|---|
| `lib/db/clean-log-mapping.ts` | 489 | 清掃1回ぶんの★未実施・完了率・連続日数★の規則 | `notExecutedFromReport()` `:165-169` / `completionRateFromReport()` `:203-210` / `latestReportByTimetableId()` `:241-255` / `isFutureSchedule()` `:308-310` / `isProblemCompletion()` `:332-334` / `computeConsecutiveProblemDays()` `:463-489` |
| `lib/db/report-mapping.ts` | 971 | レポート（KPI・施設別・連続問題・エラー内訳）の組み立て | 下の表 |
| `lib/db/consecutive-source.ts` | 463 | ★連続日数の入力を作る唯一の場所★ | `consecutiveLookbackCalendarDays()` `:202-207` / `consecutiveInputsFromRows()` `:265-302` / `loadConsecutiveInputs()` `:388-463` |
| `lib/db/rescue-mapping.ts` | 198 | 要レスキューの組み立て・並べ替え | `buildRescueRequired()` `:105-198` |
| `lib/db/clean-area-source.ts` | 513 | 総清掃面積の材料（面積・実施回数・完了率の Σ と件数） | `cleanAreaStatsFromRows()` `:287-327` / `loadCleanAreaFacilityStatsWithRowCount()` `:378-426` / `countDuplicatedTimetables()` `:456-469` |
| `lib/clean-area/aggregate.ts` | 264 | 総清掃面積のロールアップ（店舗 / 顧客 / ホールディングス） | `aggregateCleanArea()` `:165-248` |
| `lib/clean-logs/areas.ts` | 184 | エリア別完了率（行の並べ替え・全体率） | `overallCompletionTally()` `:101-112` / `buildAreaCompletions()` `:148-175` |
| `lib/dashboard/aggregate.ts` | 123 | ダッシュボードの件数（章5-9） | — |
| ★`lib/dashboard/latest-run.ts`★（新・10/06） | 120 | ★ダッシュボードの数値カード・表に「どの清掃を出すか」★（清掃エリアごとの直近1件。D-159） | `latestRunWindow()` `:37`（DB から読む日付範囲。今日を含む8日）/ `pickLatestRunPerSection()` `:69`（★メタの無い行は選ばずルートがログ（`missingRunMetaIds()`）、メタの重複は例外★。D-182） |

#### `lib/db/report-mapping.ts` — レポートの組み立て

| 関数 | 場所 | 役割 |
|---|---|---|
| `periodOfSummaryRow()` | `:100-104` | サマリ行が週次か月次か★年次★かを決める |
| `customerSummariesByPeriodKey()` | `:185-214` | サマリ行を期間キーの表にする。★異常を見つけたら止まる★（下記） |
| `kpiFromSummaryRatios()` | `:231-241` | ★確定期間★の KPI。DB の率4列をそのまま使い、★詳細テーブルから再計算しない★ |
| `aggregateCleanStatsByFacility()` | `:460-505` | ★当期★（進行中の週・月・年）を詳細テーブルからその場集計する |
| `kpiFromCleanStats()` | `:516-535` | 当期のバケットを4指標に変換。完了率だけ `meanCleanRatio()` を通す |
| `errorCountByMainTimetable()` | `:380-392` | エラー発生率の分子（本清掃の予定を指すエラーだけ） |
| `continuousProblemsInPeriod()` | `:769-802` | 連続問題施設の1期間ぶん |
| `errorBreakdownItemsFromCounts()` / `errorBreakdownRowsFromSummary()` / `countMainErrorTypesInRange()` | `:885-895` / `:906-931` / `:955-971` | エラー種別内訳（確定期間はサマリの合算、当期は詳細テーブルの数え上げ） |

★**`periodOfSummaryRow()` は `month` も見ること**★（`:100-104`）。`week === 0` だけで判定すると ★年次行（`month=0 かつ week=0`）が「月次」に化けて混入★し、重複検知で 500 になります。エラー内訳側は★年次の件数を月次に足し込んだまま黙って通る★ので、気づけるとは限りません。

★**顧客が混ざっていたら例外で止まります**★（`:185-214`）。「レポートの KPI は運用担当が判断に使う数字。別会社の数字で判断されるより、画面が出ないほうがまし」というユーザー判断（D-133）です。同じ期間キーが2行来た場合も止まります（サマリに一意制約が無く、後勝ちで不定になるため）。

#### `lib/db/consecutive-source.ts` — ★連続日数の入力を作る唯一の場所★

連続日数は「その施設を過去に遡って数える」ので、★表示したい期間だけを読むと連続が途切れます★（9/8× 9/9× 9/10× を「9/10〜9/10」で引くと1日＝L1 に化ける）。そこで★ここだけ、表示期間より広く読みます★。

- 遡り幅 ＝ `(表示上限 + 1) × 7` カレンダー日（`consecutiveLookbackCalendarDays()` `:175-180` / `CONSECUTIVE_LOOKBACK` `lib/config/index.ts:184-189`）。★1スケジュール存在日＝最大7カレンダー日★として安全側に換算しています。★月1回しか清掃しない施設が出ると遡りが足りず、連続日数が実際より小さく出ます★（例外は出ない）
- 本清掃だけに絞り、★未来の予定を外します★（`consecutiveInputsFromRows()` `:235-265`）
- ★清掃詳細ログ・施設別パフォーマンス・連続問題施設・要レスキューの4経路が同じ1本を使います★。以前は2箇所に似たコードがあり、同じ「連続3日」が画面によって違う数になりました（D-103 / D-109）

★**知っておくべき挙動（直すものではない）**★: `clean_task_reports.clean_timetable_id` が NULL か 0 の行（予定外のタスク）は、予定の id の `in` に一致しないので、ここには返って来ません（`:211-226` / `:373-377`）。★予定外は別の読み口（`lib/db/unscheduled-clean-log-source.ts`・`consecutive-source.ts` の `UNSCHEDULED_TIMETABLE_ID_WHERE`）で足します★（2026-10-29 から。D-187。旧: 完了率・実施回数に入らないのが正しい扱いとしていた。D-155 項3）。ただし、予定された清掃の結果が何かの理由で NULL のまま届くと、その予定は「レポート行が無い」＝★未実施に化けます★。例外もテスト失敗も出ないまま稼働率だけが静かに下がるので、未実施が多すぎるときは NULL の行数を疑ってください。

**変えると何が起きるか**: `isFutureSchedule()` の除外を外すと、まだ来ていない1週間ぶんの予定（`clean_timetables` には1週間分がまとめて登録される。D-106）が全部「出発失敗」になり、★全施設が常時 L3★ になります（D-106 / D-107 / D-110）。

---

### 5.11 ★コードと文書に残っている古い記述（2026-10-05 確認。直っていない）★

動作には影響しませんが、読んだ人が古い前提を信じる恐れがあるものです。★直すのは本書の範囲外★なので、場所だけ記録します。

| 場所 | 古い記述 | いまの実態 |
|---|---|---|
| `lib/db/clean-area-source.ts:440` | 「`clean_timetable_id` は★TeamWorks がまだ埋めていない★（今後埋める前提）」 | NULL は「予定外のタスク」で、埋める想定ではない（D-152 項3-3 / 第10回 N-1） |
| `lib/fixtures/clean-task-reports.ts:24` | 同上「今後 TeamWorks が埋める前提」 | 同上 |
| `lib/db/report-mapping.ts:249` | 「`clean_timetables.started_at` は常に予定時刻と同値でシード」 | `clean_timetables` に `started_at` 列は無い（`start_at`）。当期の稼働率と出発成功率が同じ分子になる理由（定刻より遅れた出発を区別する列が無い）そのものは正しい |
| `lib/db/report-mapping.ts:278-280` | 「`clean_error` が 2 / 3 の清掃は `robot_error_reports` の行が作られない」 | `clean_error` は旧取得元（`clean_status_histories`）の列で、いまは読んでいない。制御盤・セキュリティのエラーの入れ方は第10回 N-5 で方針が出て DDL 変更待ち |
| ~~`lib/dashboard/__tests__/aggregate.test.ts:143`~~ ★直した（`8a657a2`）★ | ~~「事前チェックの予定が無い（実DBでは Standby. になる）」~~ | ★コミット `8a657a2`（2026-10-07）で「予定はあり異常なし」（`Standby.`）と「予定が無い（null）」（`null`）の2件に分けた★（D-171 後の挙動に合わせた。5.9） |

> **古いコメントではないもの**: 清掃詳細ログの凡例「（閾値はシステム設定で変更可能）」（`CleanLogTable.tsx:175`）は、いまのシステム管理画面では閾値が「編集不可」なので食い違って見えますが、**モックの文言どおり（`d-watch-mock.jsx:312`）で、閾値の編集は F-10 で P1 に実装する予定**です（`docs/spec.md:114-119`、`lib/config/index.ts:8`）。連続日数の上限で作法を確定させてから広げる、という段階的な進め方の途中です（2026-09-13 ユーザー判断 A案 / 指示書AF §H-1）。

---

### 5.12 なぜ触ってはいけないと書かれているのか

[docs/architecture.md](architecture.md) の「7. 触ってはいけないもの」に、`lib/judge/**` とそのテスト、および `lib/dashboard/aggregate.ts` とそのテストが挙げられています（`docs/architecture.md:175-176`）。理由は単純で、★**ここは間違えても壊れたように見えない場所**★だからです。画面の色がおかしければ誰でも気づきますが、`>` を `>=` に1文字変えただけの間違いは、エラーも出さず動作も止めず、ただ全施設の評価を静かに1段階ずらします。清掃が実際にどうだったかを知る手段は画面しかないので、間違ったまま数か月運用されても誰も気づけません。

だから直す順番が決まっています ── ★**まず期待値の表（テスト）を直し、それから実装を直す**★（[CLAUDE.md](../CLAUDE.md) §0-2 / §6-2 / §9.4、`lib/judge/index.ts:7-10`）。テストを後回しにすると、テストは「実装が正しいこと」ではなく「実装が今そうなっていること」を確認するだけの飾りになります。★期待値表の確定は分担しません★（[CLAUDE.md](../CLAUDE.md) §0-2）。

D-139（境界が「以上」になった）も D-151（完了率が単純平均になった）も D-155（完了率が1回ごとの完了率の平均になった）も、★**この順番で進めたので挙動の変化がテストに記録されています**★。

| 変化 | 記録しているテスト |
|---|---|
| 90.0 が ◎、70.0 が ○ に（D-139） | `lib/judge/__tests__/grade.test.ts:26` / `:30` |
| 70.0 で B1 が出なくなった（D-139） | `lib/judge/__tests__/advice.test.ts:150-155` |
| 面積加重と単純平均で R-3 の結果が変わる（D-151） | `lib/judge/__tests__/rescue.test.ts:548` 以降（表6） |
| ★1回ごとの完了率の式・切り捨て・誤差の吸収・`task_area = 0`（D-155 / D-158）★ | ★`lib/judge/__tests__/run-completion-rate.test.ts`（表1・表2。指示書AS 段階1 で実装より先に「落ちるテスト」として入れた。★R9・R12（`task_area = 0` は 0.0）は指示書AU 段階1★）★ |
| ★R-1 の未実施を `departed` で数える（D-156 項1）★ | ★`lib/judge/__tests__/rescue.test.ts:219-296`（表5（指示書AS））★ |

閾値そのもの（90 / 70 / 10、猶予30分、L2=2日 / L3=3日）は TeamWorks との合意事項です。変えたくなったら [docs/decisions.md](decisions.md) を確認し、必要なら合意を取り直してください（[CLAUDE.md](../CLAUDE.md) §4 冒頭 / §7）。

---

## 5-B. 画面に出る数字の出どころ — テーブル・列・式・根拠

この章は、**画面に出る数字を1つずつ**取り上げて、次の5つを表にしたものです。

1. **どのテーブルのどの列から来たか**（DB の物理名。`prisma/schema.prisma` で確認したもの）
2. **どういう式で計算しているか**（関数名と `ファイル:行`）
3. **どの行を数えているか**（対象にする行の条件）
4. **なぜその根拠なのか**（決定番号・誰がいつ決めたか・TeamWorks の何回目の回答か）
5. **確定か暫定か**

判定の考え方そのもの（◎○△× の境界・L1〜L3 など）は[章5](#5-アプリの頭脳--判定と計算)に書いてあります。この章は「画面のこの数字はどこから来たのか」を引くための辞書です。

**読み方の約束**

- **テーブル.列** は DB の物理名です（例: `clean_task_reports.clean_area` ＝「清掃実績」表の「清掃した面積」列）。列の意味が分からないときは章13の用語対応表を見てください
- ★**DB の時刻は、列の型（TIMESTAMP / DATETIME）によらず JST の壁時計の値です**★（[CLAUDE.md](../CLAUDE.md) §3）。アプリは `jstIsoFromDb()`（`lib/format/date.ts:101-103`）で「DB の生の値 ＝ 画面の時刻」になるように読みます。★生の値に9時間を足しません★。`DB_DATETIME_OFFSET_MINUTES = 9 * 60`（`lib/config/index.ts:236`）は、読み取り（`fromDbJst()` `date.ts:72-74`）と書き込み（`toDbJst()` `:87-89`）で同じ値を引いて足すための設定で、★表示の時刻は DB の生の値のまま★になります（`date.ts:94-100`）
- ★**パーセントの表示は、小数第1位を四捨五入した整数です**★（`displayRate()` `lib/judge/grade.ts:43-46`）。値が無い（`null`）ときは `-` です。★判定は丸める前の生値で行います★（章5-3）
- ★**面積・件数の表示は、整数に四捨五入して3桁区切り**★です（`formatNumber()` `lib/format/number.ts:46-52`。`341635.4` → `341,635`）。★面積の単位は `AREA_UNIT = "㎡"`★（`lib/config/index.ts:815`）。★単位は DFA の判断で、TeamWorks の確認は取れていません★（第9回 5章で照会中。`config/index.ts:792-803`）
- ★「本清掃」★＝ `clean_timetables.precheck = 0` の予定（`CLEAN_TIMETABLE_PRECHECK.main` `lib/config/index.ts:272-275`）。`precheck` が 60 / 30 などの行は「何分前の事前チェック」の予定で、清掃そのものではありません（TeamWorks 第7回 A5-2。D-112）
- ★「未実施」＝「出発失敗」＝ 本清掃の予定に対応する `clean_task_reports` の行が無いこと★（`notExecutedFromReport()` `lib/db/clean-log-mapping.ts:165-169`。ユーザー確認 2026-09-30。指示書AQ §1-1）。★まだ予定時刻が来ていない予定は、どの画面でも数えません★（`isFutureSchedule()` `clean-log-mapping.ts:308-310`。`clean_timetables` には1週間分の予定がまとめて登録されるため。D-106）
- ★サンプルデータ（`USE_FIXTURES` が `"false"` 以外）のとき★は、DB ではなく `lib/fixtures/` の値が出ます。判定（`lib/judge/`）は同じものが走りますが、★材料の値の出どころは変わります★。画面ごとの違いは各表の下に書きました

---

### 5B-1. ★清掃完了率の計算 — 図で見る★

清掃完了率は、清掃詳細ログ・エリア別完了率・レポート（当期）・施設別パフォーマンス（当期）・総清掃面積・要レスキューの★全画面で同じ式★です（D-155）。ただし★どの行を集めるか★は画面によって少しずつ違います（5B-12）。

```mermaid
flowchart TD
  S["本清掃の予定<br/>clean_timetables ＋ clean_timetable_histories<br/>（precheck = 0。同じ id は本表）"] --> T{"予定時刻を<br/>過ぎているか"}
  T -- "いいえ" --> T0["数えない<br/>（まだ来ていない予定）"]
  T -- "はい" --> U{"この予定を指す<br/>clean_task_reports の行があるか"}
  U -- "無い" --> U0["未実施（＝出発失敗）<br/>完了率 null・判定 ×<br/>完了率の分子にも分母にも入れない"]
  U -- "ある" --> M{"2行以上あるか"}
  M -- "はい" --> M1["ended_at（無ければ started_at）が<br/>最新の1行だけを採る<br/>※総清掃面積だけは全行を数える"]
  M -- "いいえ" --> E
  M1 --> E{"task_area = 0 か"}
  E -- "はい" --> E0["1回ごとの完了率 = 0.0<br/>実施回数にも完了率の件数にも数える<br/>（第11回 P-5=B。旧: null）"]
  E0 --> I
  E -- "いいえ" --> F["raw = min(clean_area ÷ task_area × 100, 100)"]
  F --> G["小数第6位で一度四捨五入<br/>（浮動小数の誤差を吸収）"]
  G --> H["小数第1位まで切り捨て<br/>＝ 1回ごとの完了率<br/>runCompletionRate()"]
  H --> I["Σ（合計）と件数に足す"]
  I --> J["単純平均 = Σ ÷ 件数<br/>meanCleanRatio()<br/>件数 0 なら null"]
  J --> K["◎○△× の判定<br/>judgeGrade()<br/>★丸めない生値で★"]
  J --> L["表示<br/>displayRate()<br/>★整数に四捨五入するのはここだけ★"]

  R["clean_task_reports の行<br/>（実績側から見た場合）"] --> N{"clean_timetable_id は NULL か"}
  N -- "はい（予定外のタスク）" --> N0["完了率・実施回数に入れない<br/>★総清掃面積の面積の合計にだけ入れる★"]
  N -- "いいえ" --> P{"指す予定は<br/>precheck = 0 か"}
  P -- "いいえ（事前チェックの予定・<br/>予定が見つからない）" --> P0["完了率・実施回数に入れない<br/>（総清掃面積の面積の合計には入る）"]
  P -- "はい" --> M
```

| 段 | 何をしているか | 関数 `ファイル:行` | 根拠 |
|---|---|---|---|
| 本清掃だけ | `precheck = 0` の予定だけを集める | 各取得関数の `where: { precheck: CLEAN_TIMETABLE_PRECHECK.main }`（`lib/db/index.ts:917` 他） | TeamWorks 第10回 N-3「事前確認の結果は含めない。precheck が0のデータのみ集計対象」 |
| 予定外を外す | `clean_timetable_id` が NULL の行は予定の id に一致しないので自然に落ちる。`latestReportByTimetableId()` も NULL を捨てる | `lib/db/clean-log-mapping.ts:247` | D-155 項3（ユーザー判断 2026-10-02）。サマリが NULL を除く（N-3「当然 clean_timetable_id が NULL のデータも集計対象外」）のにそろえた |
| 1行に絞る | 同じ予定に複数行あれば `ended_at`（無ければ `started_at`）が最新の1行 | `latestReportByTimetableId()` `clean-log-mapping.ts:241-255` / `reportOrderKey()` `:266-268` | 指示書AQ §1-4。★1予定に何行できるかは第10回 N-8 で「A（想定どおり）」の回答★ |
| 1回ごとの完了率 | `min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て | `runCompletionRate()` `lib/judge/kpi.ts:166-175` / `RUN_COMPLETION_RATE` `lib/config/index.ts:36-41` | ★D-155 項1（ユーザー判断「一旦B」2026-10-02）★。TeamWorks 第10回 N-3 のサマリの式「100より大きい値は100、小数点第2位以下切捨て」と同じにした。★第11回 P-2=A で確定・`task_area = 0` は 0%（P-5=B）。D-158★ |
| 平均 | Σ ÷ 件数（清掃1回＝1票） | `meanCleanRatio()` `kpi.ts:122-125` | D-151 項4-3（ユーザー回答 2026-10-01「単純平均でよいです」） |
| 判定 | ◎ 90 以上 / ○ 70 以上 / △ 10 以上 / × 10 未満・未実施 | `judgeGrade()` `lib/judge/grade.ts:29-35` | D-139（ユーザー指示 2026-09-16） |
| 表示 | 整数に四捨五入 | `displayRate()` `grade.ts:43-46` | [CLAUDE.md](../CLAUDE.md) §4.3 |

---

### 5B-2. ★主要テーブルと、それぞれから取っている値の早見表★

アプリが実際に読んでいるテーブルだけを並べました（`lib/db/` 内の Prisma の問い合わせを全件検索して確認。2026-10-05）。

| テーブル（日本語の意味） | 取っている列 | 何の数字になるか | 読んでいる画面 |
|---|---|---|---|
| `clean_timetables`（清掃の予定表） | `id` / `facility_id` / `section_id` / `parent_id` / `start_at` / ★`end_at`（結果待ちの判定。D-173）★ / `precheck` | 予定回数・予定時刻・日付・「何分前のチェックか」。未実施を見つける起点 | 清掃詳細ログ / ダッシュボード / アラート / エリア別完了率 / レポート（当期・連続問題施設） / 要レスキュー / 総清掃面積（本清掃かの確認だけ） |
| `clean_task_reports`（清掃の実績） | `clean_timetable_id` / `clean_area` / `task_area` / `status` / `started_at` / `ended_at` / `facility_id` | ★1回ごとの完了率・面積・清掃結果・実施したか★ | 上と同じ全画面 / ★総清掃面積★ |
| `robot_error_reports`（ロボットのエラー） | `error_id` / `clean_timetable_id` / `error_type` / `errored_at` / `created_at` / `status` / `comment` / `responded_at` / `resolved_at` / `facility_id` / `serial_number` | 事前チェックの状態・アラート・エラー発生率の分子・エラー内訳（当期）。★D-Sec アラート（`D-S_CONTROL_FAILURE`）★。★4列だけ書き込む★ | 清掃詳細ログ（1h前ST / 30分前ST）/ アラート / レポート（当期）/ D-Secアラート / ダッシュボード（バナー） |
| `customer_summaries`（顧客ごとのサマリ） | `operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio` / `year` / `month` / `week` / `customer_id` | ★確定期間の KPI 4種と、当期の前期比★ | レポート（KPI） |
| `facility_summaries`（施設ごとのサマリ） | 同じ率4列 / `year` / `month` / `week` / `facility_id` | ★確定期間の施設別パフォーマンスと、当期の前期比★ | レポート（施設別パフォーマンス） |
| `facility_error_summaries`（施設ごとのエラー件数サマリ） | `error_type` / `error_count` / `year` / `month` / `week` / `facility_id` | ★確定期間のエラー種別内訳★ | レポート（エラー種別の内訳） |
| `clean_schedules`（清掃スケジュールのマスタ） | `weeks` / `start_time` / `end_time` / `inactive` / `deleted_at` | ★ダッシュボードの「予定」件数★・スケジュール一覧 | ダッシュボード（進捗）/ スケジュール |
| `facilities`（施設マスタ） | `facility_name` / `contract` / `prefecture_name` / `addr1` / `addr2` / `phone` / `manager_name` / `charger_name` / `email` / `customer_id` / `deleted_at` | 施設名・契約（監視 / D-Sec）・所属 | 全画面 |
| `sections`（清掃エリア） | `section_name` | エリア名 | エリア別完了率 / D-Secアラート |
| `robots` / `devices`（ロボット・機種） | `robots.serial_number` / `devices.maker_id` / `devices.model_name` | シリアル・機種名 | 清掃詳細ログ / アラート / 施設ロボットマスタ / スケジュール / 施設別パフォーマンス |
| `customers` / `groups`（顧客・ホールディングス） | `customer_name` / `group_id` / `group_name` | 絞り込みの選択肢・総清掃面積のまとめ方 | 全画面の絞り込み / 総清掃面積 |
| `controls`（D-S制御盤） | `serial_number` / `status` / `facility_id` / `deleted_at` | 接続状態・台数 | D-Secログ |
| `control_reports`（制御盤の生死報告） | `control_id` / `reported_at` | 最終通信の時刻（★制御盤ごとの最大値★） | D-Secログ |
| `control_error_histories`（制御盤の異常の履歴） | — | ★アプリは読まない（D-180）★ | — |

★**読んでいないテーブル**★: `clean_status_histories`（D-151 で取得元を `clean_task_reports` に一本化）、`customer_error_summaries`（エラー内訳は施設版で確定。D-102 項4）、`clean_statuses` / `security_statuses` / `security_status_histories` など。★`clean_timetable_histories` は 2026-10-05 から読みます★（予定の履歴。D-158 項4）。★`customers.contract`（削除依頼済み）と `clean_schedules.contract`（用途が違う。第10回 N-13）も読みません★（[CLAUDE.md](../CLAUDE.md) §4.6）。

★**サマリテーブルの面積（`*_summaries.task_area` / `clean_area`）は読みません**★。総清掃面積の画面も常に `clean_task_reports` から集計します（`lib/db/clean-area-source.ts:35-38`）。コードのコメントには「実データで `task_area` が全行 1000 固定で、期間内の合計になっていない」とあります（同所・`lib/config/index.ts:808`）が、★いつ・どのデータで見たかはコードに書かれていません★（第9回 Q2-1 で照会中）。

---

### 5B-3. ★暫定の数字の一覧★ — 何が決まったら変わるか

| 暫定のもの | 影響する画面・数字 | いまの扱い | 決まったら何が変わるか | 照会先 |
|---|---|---|---|---|
| ~~清掃完了率の式~~ ★確定★ | 完了率を出す全画面・◎○△× の判定・ダッシュボードの成功 / 失敗・連続日数・要レスキュー R-3 | 1回ごとの面積比（上限100・小数第1位切り捨て）の単純平均（D-155） | ★第11回 P-2=A「今の式のまま」で確定（D-158）★。P-1: 訂正後の `clean_ratio` 列も同じ式。訂正の時期は無回答 | 第11回 P-1・P-2（回答済み） |
| ~~`task_area = 0` の行~~ ★確定★ | 完了率・判定（× になる） | ★1回ごとの完了率を 0%★（指示書AU 段階2。旧: `null`） | ★第11回 P-5=B で確定（D-158）★ | 第11回 P-5（回答済み） |
| ~~清掃結果 `status` の表に無い値（CSV実測の `6`）~~ ★確定★ | 清掃詳細ログの「清掃結果」列・CSV | ★6 は 3 と同じ「清掃キャンセル」★（`CLEAN_TASK_STATUS_LABELS`）。7 以上など表に無い値は「不明(7)」（`cleanTaskStatusLabel()`。画面の英語は "Unknown (7)"） | ★ユーザー指示 2026-10-30「6はキャンセルだそうです」★（第11回 P-4 の時点では先方も意味不明。D-158） | 解決済み |
| ~~「正常終了」なのに清掃面積 0 の行~~ ★確定★ | 完了率 0.0 → 判定 × | そのまま 0%（`clean_area = 0` は「0%で実施」） | ★第11回 P-3=A「清掃せずに終わった。0% でよい」（D-158）★。稼働率には正常終了として入る（指示書AU 表2 K8） | 第11回 P-3（回答済み） |
| ★中断・キャンセル・異常終了（`status` 2 / 3 / 5）★ | 判定 | ★判定には使わない★。完了率で ◎○△× を出す（D-155 項2） | [CLAUDE.md](../CLAUDE.md) §4.4「中断で返る＝失敗」を実データで見直す | ユーザー判断待ち（実データ待ち） |
| ~~当期の稼働率・出発成功率・エラー発生率~~ ★確定★ | レポート（KPI・施設別）の当期 | ★サマリと同じ式★（正常終了 ÷ 予定 / 開始 ÷ 予定 / エラー ÷ 予定。指示書AU 段階3） | ★第11回 P-6=A/A/B で確定（D-158）★ | 第11回 P-6（回答済み） |
| エラー発生率・エラー内訳の分子 | レポート | 本清掃の予定を指す `robot_error_reports` の行だけ | 制御盤・セキュリティ・清掃開始失敗のエラーも `robot_error_reports` に入る（第11回 P-8〜P-10）。★区分の見分けと数え方は次フェーズ★（D-158 当方の対応5） | 第11回 P-8〜P-10（回答済み・対応は次フェーズ） |
| 面積の単位 `㎡` | 総清掃面積 | DFA の判断（ユーザー指示 2026-09-16「単位は㎡でよいです」） | TeamWorks の回答が違えば `AREA_UNIT` の1行を直す | 第9回 5章 |
| ~~判定の対象を `clean_timetable_histories` からも引くか~~ ★対応済み★ | 予定を起点にする全画面の母集団 | ★`clean_timetables` と `clean_timetable_histories` の両方を読む。同じ id は本表を採る★（`lib/db/timetable-source.ts`。指示書AU 段階3b） | ★第11回 P-7=C・D-158 項4★。予定がいつ履歴に移るかは未確認（次回の確認事項の候補。D-158 項7） | 第11回 P-7（回答済み） |
| 「本番」の時刻 | 全画面の時刻 | DB の生の値 ＝ 画面の時刻（ステージングで JST に統一済みと先方が申告。当方の再実測は未了） | ★第11回 P-11=A「本番DBも JST」・P-12=A「既存行も JST に直した」（D-158）★。当方の再実測は未了（ステージングに接続できない） | 第11回 P-11・P-12（回答済み・再実測待ち） |
| 施設ステータスの閾値（良好 90 / 注意 75） | 施設別パフォーマンスのステータス・セル色 | `FACILITY_STATUS_THRESHOLDS`（`lib/config/index.ts:553-558`。コメントに「暫定値」） | DFA が別の基準を決めたら | DFA 内部（TeamWorks 照会なし） |
| 未実施の猶予30分（`LAUNCH_GRACE_MINUTES`） | ★いまはどの数字にも使っていない★ | 「予定＋猶予」をアプリで計算しない（D-091 項1）。★D-173 の結果待ちはこの件に限り D-091 項1 を上書きしたが、この定数ではなく `RESULT_WAIT_MINUTES` を使う★ | — | — |
| ★結果待ちの長さ 120分（`RESULT_WAIT_MINUTES`）★（D-173） | 清掃詳細ログ・CSV・ダッシュボード・アラートの出発失敗・連続日数・要レスキュー・当期の率（[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)） | 予定の終了から120分は、レポートが無くても「結果待ち」とし失敗に数えない（ユーザー判断「一旦2時間」） | ★第12回（★R-2★。`04f974a` で送付版）で TeamWorks にレポートの最大の遅れを聞く★。答えに合わせて値を変える（直近の清掃の見え方だけが変わる） |

---

### 5B-4. ダッシュボード（`/dashboard`）

★取得は `GET /api/dashboard`（`app/api/dashboard/route.ts`）の1本です★（`components/features/dashboard/DashboardScreen.tsx:87`。2026-10-06 から。D-159 / 指示書AV）。旧: コンパクト版ログ用に `GET /api/clean-logs?result=problem&date_from=当日&date_to=当日&per_page=5` も呼んでいた。
★`date` を省略すると「直近モード」★: 数値カードと表は ★清掃エリアごとに、本清掃のうち「予定の開始 ≥ 現在 − 7日」かつ「予定の終了 ≤ 現在」（境界はどちらも含む）を満たす予定の中で開始が一番新しい1件★から作ります（`pickLatestRunPerSection()` `lib/dashboard/latest-run.ts:69`。7 は `DASHBOARD_LATEST_RUN.lookbackDays` `lib/config/index.ts:217`。`a961829`）。★旧（10/06〜10/07 朝）: 「予定の終了 ＋ 30分（`settleMinutes`）≤ 現在」。D-173 で `settleMinutes` を削除★し、終わってからレポートが届くまでの清掃は ★結果待ち（`result_pending`）★として選んだうえで「結果待ち」カードに数えます（[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)）。
★実データの清掃は 10:00 や 19:00〜22:00（前日の日付）なので、当日の日付で数えると翌朝に空になるため★（D-159）。★スケジュール進捗・要レスキュー・セキュリティバナーは当日のまま★（「当日」は `toJstIso(now)` の日付。`route.ts:70-71`）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 成功 | `8` | `clean_timetables.id` / `start_at`、`clean_task_reports.clean_area` / `task_area` | 出発した（レポート行がある）行のうち判定が ◎○ の件数。`computeSummary()` `lib/dashboard/aggregate.ts:41-76`（`SUCCESS_GRADES` `:26`。`a961829`）。★結果待ちの行は数えない★ | ★直近モード（上の段落）で選んだ行★（`route.ts:131-164` / `:183`）。`?date=` 指定時はその日の予定で、予定時刻を過ぎたもの。予定外のタスクは入らない | D-049（ダッシュボードの値の定義）・★D-159（母集団）★。成功＝◎○ とするのは判定基準（D-139）に従うため | 確定（完了率の式も D-158 で確定） |
| `/ N件`（成功の補足） | `/ 12件` | 同上 | 成功＋失敗＋未実施＋★結果待ち★（`DashboardScreen.tsx:194-207`。D-173）。★直近モードでは、7日以内に終わった予定がある清掃エリアの数★。★成功率の分母とは違う（結果待ちを含む）★ | 同上。★1行＝1ロボット×1回の予定★で、施設数ではない | D-049 | 確定 |
| 失敗 | `3` | 同上 | 出発した行のうち判定が △× の件数（`FAILURE_GRADES` `aggregate.ts:28`）。★`task_area = 0` の清掃は完了率 0%・判定 × なのでここに入る★ | 同上 | D-049。未出発の行を失敗にも数えると二重計上になるので、出発した行に限る（`aggregate.ts:33-34`） | 確定 |
| 未実施（予定時刻経過・実績なし） | `1` | `clean_timetables.id`、`clean_task_reports.clean_timetable_id`（行の有無） | `departed === false` の件数（★結果待ちを除く★。`aggregate.ts:54-57`）。`departed` は `!notExecutedFromReport()`（`lib/db/index.ts:1196`） | 同上 | 指示書AQ §1-1 / ユーザー確認 2026-09-30「出発失敗と未実施は同じ扱い」。★予定の終了から `RESULT_WAIT_MINUTES`（120分）以内でレポートが無い清掃は、未実施ではなく結果待ち（D-173）★。`clean_task_reports` には未実施を表す列が無い | 確定 |
| 成功率 | `67%` | 上の3つ | 成功 ÷（成功＋失敗＋未実施）× 100。★結果待ちは分母に入れない★（D-173）。3つの合計が 0 なら `-`（`aggregate.ts:67-75`） | 同上 | D-049。0 件の日を 0% と出さないのは「予定が無い日」と「全部失敗した日」を区別するため | 確定 |
| ★結果待ち★（D-173） | `1` | `clean_timetables.start_at` / `end_at`、`clean_task_reports` の行の有無 | `result_pending === true` の件数（`computeSummary()` の `pending`。`aggregate.ts:49-52`）。旗は `isResultPending()`（`lib/clean-logs/result-pending.ts:22`）で、★予定の開始 ≤ 現在 ＜ 予定の終了＋120分 かつレポートが無い★。カードは中立色（`StatCard` の `mt`。`DashboardScreen.tsx:263-271`。副題「レポートの到着待ち」） | 同上（直近モードでは予定の終了 ≤ 現在の行なので、清掃中の行は入らない） | ★ユーザー判断 2026-10-07（「B」・カードを足す）。D-173★。★モックに無いカード★ | 確定（120分は暫定。第12回（★R-2★。`04f974a` で送付版）でレポートの最大の遅れを聞く） |
| 目標: 95% | `目標: 95%` | （DB ではない） | `SUCCESS_RATE_TARGET = 95`（`lib/config/index.ts:577`） | — | DFA の設定値（コメントに出所の記載なし） | DFA 設定 |
| スケジュール進捗 `N / M 完了` と棒グラフ | `11 / 12 完了` | ★M（予定）＝ `clean_schedules.weeks` / `inactive` / `deleted_at`★、★N（完了）＝ `clean_timetables` × `clean_task_reports` の行の有無★ | M＝その曜日の桁が `"1"` で有効（`inactive = 0`）なスケジュールの数、N＝当日の出発した件数、棒＝N ÷ M × 100（`computeScheduleProgress()` `aggregate.ts:101-120`）。M が 0 なら「本日の清掃予定はありません」（`ScheduleProgressCard.tsx:21-22`） | M: `clean_schedules` の `deleted_at` が NULL の行（`lib/db/index.ts:2192-2193`）。`weeks` は「日月火水木金土」の7桁（`lib/format/schedule.ts:4`）。N: 上の「成功」と同じ母集団 | D-049「予定はスケジュール由来にする」— ログの行数を使うと、収集処理が止まって当日の行が0件のとき「本当に定休日」と「収集処理の障害」が区別できない。★「完了」と書いてあるが中身は「出発」★（`aggregate.ts:112`。★結果待ちも出発していないので数えない★。D-173 で変えていない） | 確定 |
| D-Sec `N件`（右上の札）とセキュリティ無効バナー | `D-Sec 1件` / `⚠ セキュリティ無効: ○○店 1F（03:10以降）` | `robot_error_reports`（`dsAlerts()`） | 種別が `security_off` で対応済みでないものの件数と一覧（`computeSecurityBanner()` `aggregate.ts:131-141`）。時刻は `detected_at` | ★実データでは常に0件 — `security_off` を DB から作らない（U1・D-180 項7）★ | D-049 / D-180 項7。★意図した挙動（コードは残す — U7）★ | 確定 |
| 要レスキュー店舗 `N件` | `3件` | `clean_timetables` × `clean_task_reports`（`loadConsecutiveInputs()` 経由）、施設名は `facilities.facility_name` | 3条件のどれかに当たった施設の数（`buildRescueRequired()` `lib/db/rescue-mapping.ts:105-198` → `judgeRescue()` `lib/judge/rescue.ts:268-296`）。★件数では切らない★（画面は最初の5件だけ見せる。`RESCUE_INITIAL_VISIBLE` `lib/config/index.ts:119`） | ★当日に予定が無い施設も含む★（数値カードとは母集団が違う。`DashboardScreen.tsx:269-270`）。評価日は「当日以前で最も新しいスケジュール存在日」。施設名は `deleted_at` で絞らない | ユーザー指示 2026-09-15 / 指示書AI・AJ | 確定 |
| 要レスキューの理由「{n}日連続で未実施」 | `3日連続で未実施` | 同上（`clean_task_reports` の行の有無） | 最新日から遡って `departed === false` の日が続いた数（`countNotExecutedStreak()` `rescue.ts:202-209`）。3 以上で該当（`RESCUE_THRESHOLDS.notExecutedDays` `lib/config/index.ts:87-90`） | 同じ日に複数台あれば、1台でも未実施ならその日は未実施（OR。`foldRescueDays()` `rescue.ts:160-189`）。★`task_area = 0` の清掃は未実施に数えない★ | ユーザー指示 2026-09-15（R-1）/ D-156 項1（未実施は `departed` で数える） | 確定 |
| 「連続問題レベル3（{n}日連続）」 | `連続問題レベル3（4日連続）` | 同上 | 評価日時点の連続問題日数（`computeConsecutiveProblemDays()` `lib/db/clean-log-mapping.ts:463-489`）が L3（3日以上） | 施設単位・スケジュール存在日で数える（章5-6） | [CLAUDE.md](../CLAUDE.md) §4.5 / D-026 | 確定 |
| 「完了率が前回比{n}%低下」 | `完了率が前回比40%低下` | 同上（`clean_area` / `task_area`） | `(前回 − 当日) ÷ 前回`。0.2 以上で該当（`judgeCompletionDrop()` `rescue.ts:246-255`）。表示は × 100 を四捨五入（`RescueRequiredCard.tsx:63-71`） | 前回＝前回のスケジュール存在日。前回・当日が `null`、前回が 0 なら算出しない | ユーザー確定 2026-09-15「比で」（差ではない） | 確定 |
| 判定日 | `2026-10-04` | `clean_timetables.start_at` | その施設の、当日以前で最も新しいスケジュール存在日（`rescue-mapping.ts`） | — | 指示書AI | 確定 |
| 完了率（前回 N%） | `完了率 18%（前回 30%）` | `clean_task_reports.clean_area` / `task_area` | その日の1回ごとの完了率の単純平均（`facilityDayRate()` `rescue.ts:135-143`） | その施設のその日の本清掃（複数台なら全台）。`null` の行は除く | D-155 / D-151（面積加重をやめて単純平均に）/ D-158（確定） | 確定 |
| 清掃詳細ログ（コンパクト版） | — | 清掃詳細ログと同じ | 5B-5 と同じ。★「予定」のセルは `M/D HH:mm`（例 `10/6 03:30`）★ | ★数値カードと同じ母集団（直近モード）の △×（★結果待ちを除く★。`problem_logs`。`route.ts:203`）の、データソースの順の先頭5件★。旧: 当日・△×のみ・先頭5件 | D-159 / 指示書AV D7・D11 | 確定 |

**サンプルデータのとき**: 数値カード・進捗・要レスキューは `lib/fixtures/clean-logs.ts` の清掃ログと `lib/fixtures/schedules.ts` から、★同じ関数で★計算します。★清掃ログの完了率は `lib/fixtures/clean-logs.ts` に直接書かれた値★（90.1 / 90.0 / 89.5 などの境界値）で、面積から計算したものではありません。★セキュリティ無効バナーはサンプルデータでも出ません★（`lib/fixtures/ds-alerts.ts` は削除。D-Sec アラートは `errorReportRows` の `D-S_CONTROL_FAILURE` 行から作り、`security_off` は無い — D-180 項9）。★描画は `lib/dashboard/__tests__/aggregate.test.ts`（テスト内で作った `DsAlert`）と `app/api/__tests__/master-filter-dashboard-reports.test.ts`（`db.dsAlerts()` を差し替え。指示書AZ 段階3b）で確かめている★。

---

### 5B-5. 清掃詳細ログ（`/detail-log`）

1行＝★本清掃の予定1回★（`clean_timetables` の `precheck = 0` の1行）です。取得は `prismaSource.cleanLogs()`（`lib/db/index.ts:990-1262`）。★予定外のタスク（`clean_timetable_id` が NULL の実績）は行として出ません★（D-151 項7 で「予定起点のまま」か「実績起点」かは★保留★）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 日付 / 予定 | `10/4` / `02:00` | `clean_timetables.start_at`（`prisma/schema.prisma:597`。DATETIME） | `jstIsoFromDb()`（`lib/db/index.ts:1191`）→ `formatMd()` / `formatHm()`（`lib/format/date.ts:8-22`） | 本清掃の予定。予定時刻を過ぎたもの（`index.ts:1126-1128`） | ★予定の時刻であって、実際に清掃を始めた時刻（`clean_task_reports.started_at`）ではない★。日付の絞り込みもこの列（`index.ts:932`） | 確定 |
| 1h前ST / 30分前ST | `Standby.` / `EmergencyStop` / ★`-`★ | `robot_error_reports.error_type` / `errored_at`、`clean_timetables.precheck` / `parent_id` | その本清掃の子の事前チェック予定を★分数の大きい順に1回目・2回目★（D-178）とし、それぞれを指すエラーのうち `errored_at` が最新の `error_type`。予定行があってエラーが無ければ `Standby.`。★予定行そのものが無ければ `null` → 画面は「-」・CSV は空のセル★（`robotStatusFromErrors()` `lib/db/clean-log-mapping.ts:54-60` / `statusColumnsFromPrecheckRows()` `lib/db/precheck-mapping.ts:147-` / 画面の分岐 `precheckStatusCell()` `lib/format/precheck-status.ts:22`。`a961829`） | 本清掃の子の事前チェック行すべて（★分数によらない★。3つ以上なら開始に近い2つ）。★D-178 から★。旧: `PRECHECK_MINUTES_BEFORE = [60, 30]` に一致する行だけで、10分前など設定に無い回は出なかった（D-169） | D-065 Q2 / D-098。★`error_type` は変換しない★（D-077。[CLAUDE.md](../CLAUDE.md) §4.8）。`Standby.` は TeamWorks の `robots.status` コード表の原文（第2回回答）。★`null` は D-171（ユーザー判断 2026-10-07「事前チェックがないものは-にしてください」）★ | ★`Standby.` は「エラーが無い」の読み替えで、`robots.status` を実際に読んだ値ではない★。★判定には使わない★（D-170） |
| 出発 `✓` / `✗` | `✓` | `clean_task_reports` の行の有無（`clean_timetable_id` で突き合わせ） | `departed = !notExecutedFromReport(report)`（`lib/db/index.ts:1196`） | 同上 | 指示書AQ §1-1。「予定＋猶予30分」をアプリで計算しない（D-091 項1） | 確定 |
| ★結果待ち★（出発「…」・清掃結果「結果待ち」・完了率「-」・判定なし） | `…` / `結果待ち` | `clean_timetables.start_at` / `end_at`、`clean_task_reports` の行の有無 | `result_pending = isResultPending({ startAt, endAt, now, hasReport })`（`lib/db/index.ts:1207` → `lib/clean-logs/result-pending.ts:22`）。見え方は `cleanLogRowView()`（`lib/format/clean-result.ts:98`）。★`departed` / `grade` は出発失敗と同じ値（`false` / `×`）のまま★ | 予定の開始 ≤ 現在 ＜ 予定の終了＋`RESULT_WAIT_MINUTES`（120分）で、レポート行が無い本清掃。★行を開けない・△×／×のみの絞り込みに出ない★ | ★ユーザー判断 2026-10-07「B」・待つ時間は一旦2時間（D-173）★。D-091 項1 をこの件に限り上書き | 確定（120分は暫定。第12回 R-2 で見直す） |
| 清掃結果 | `清掃正常終了` / `6` / `出発失敗` | `clean_task_reports.status`（`prisma/schema.prisma:529`） | `formatCleanTaskStatus()`（`lib/format/clean-result.ts:61-67`。`a961829`）→ `cleanTaskStatusLabel()`（`lib/config/index.ts:386-388`）。0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了。表に無い値は数字のまま。行が無ければ「出発失敗」 | 同上。行が複数あれば `ended_at`（無ければ `started_at`）が最新の1行 | ★TeamWorks 第10回 N-2（表示名の原文）/ D-155 項2（ユーザー判断 2026-10-02「推奨」）★。★判定には使わない★ | 表示名は確定。`6` の意味は未受領（第11回 P-4） |
| 完了率 | `90%` / `-` | `clean_task_reports.clean_area` / `task_area`（`prisma/schema.prisma:533-534`） | `completionRateFromReport()`（`lib/db/clean-log-mapping.ts:203-210`）→ `runCompletionRate()`（`lib/judge/kpi.ts:166-175`）→ `displayRate()` | 同上。★レポート行が無いときだけ `-`★（`task_area = 0` は 0%。第11回 P-5=B。旧: `-`） | ★D-155 項1（ユーザー判断「一旦B」2026-10-02）★。TeamWorks のサマリと同じ式にそろえるため（第10回 N-3）。`clean_ratio` 列は先方が式の誤りを認めたため使わない。★第11回 P-2=A で確定（D-158）★ | 確定 |
| 判定 | `◎` | 同上 | `judgeGrade(completion_rate)`（`lib/judge/grade.ts:29-35`。生値で判定） | 同上 | D-139（境界は上のランク）/ [CLAUDE.md](../CLAUDE.md) §4.1 | 確定 |
| 判定基準の凡例（「判定 ?」） | `◎ 90%以上` | （DB ではない） | `GRADE_THRESHOLDS`（`lib/config/index.ts:18-25`）を表示（`components/features/detail-log/CleanLogTable.tsx:158-183`） | — | D-139。凡例の「（閾値はシステム設定で変更可能）」は**モックの文言どおり**（`d-watch-mock.jsx:312`）。閾値の編集は **F-10 で P1 に実装する予定**（`docs/spec.md:114-119`）だが、★いまは段階的に実装している途中で、編集できるのは連続日数の表示上限だけ★（2026-09-13 ユーザー判断 A案）。判定に使う閾値を編集可能にする前に、「いつから有効か」「過去の表示をどうするか」をユーザーに確認する決まり（指示書AF §H-1） | 確定 |
| 行を開いたときの見出し（レベル1〜3） | `🔶 レベル2 — DFA運用担当がアクション` | 上の値すべて＋連続問題日数 | 該当アドバイスの最大レベル（`maxAdviceLevel()` `lib/judge/advice.ts:127-131`）。アドバイスは `matchAdviceRules()`（`:59-106`） | △× の行だけ開ける（`CleanLogTable.tsx:195`） | [CLAUDE.md](../CLAUDE.md) §4.5（R1〜R4）。B1〜B3 は Phase B | 確定（B3 は値が未判明で空。章5-8） |
| 連続問題日数・L1〜L3 | （★画面に数字では出ない★） | `clean_timetables` × `clean_task_reports` | `computeConsecutiveProblemDays()`（`lib/db/clean-log-mapping.ts:463-489`）→ `judgeEscalationLevel()`（`lib/judge/escalation.ts:52-59`） | ★表示期間より前も遡って読む★（`loadConsecutiveInputs()` `lib/db/consecutive-source.ts:388-463`）。施設単位・スケジュール存在日 | D-026 / D-109 / [CLAUDE.md](../CLAUDE.md) §4.5 | 確定。アドバイス R3/R4 と見出しのレベルに効く |
| 件数「全{total}件中 {from}〜{to}件」 | `全213件中 1〜50件` | 上の行 | 絞り込み後の行数（`meta.total`。`app/api/clean-logs/route.ts:100`）。1ページ 50件（`PAGINATION.defaultPerPage` `lib/config/index.ts:410-411`） | 日付・施設・機種・シリアル・判定（△×のみ / ×のみ / 全件。`lib/clean-logs/query.ts:184-189`。★結果待ちは「全件」にだけ出る（`:186`。D-173）★）で絞ったあと | 指示書AA | 確定 |
| CSV の「完了率」「出発」「清掃結果」「1h前ST / 30分前ST」 | `90%` / `出発` / `清掃正常終了` | 同上 | 画面と同じ関数（`buildCleanLogCsv()` `lib/clean-logs/csv.ts:62-95`。`a961829`）。出発は「出発 / 未出発」、清掃結果は日本語の表示名。★完了率は `csvRate()`（`lib/csv.ts:69`）で、値が無ければ空のセル★（D-164）。★1h前ST / 30分前ST も `null` は空のセル★（D-171）。★結果待ちの行は出発・清掃結果が「結果待ち」、完了率・判定は空のセル★（D-173。`cleanLogRowView()` を画面と共有）。旧: どちらも `-` を出し、数式対策（D-125）で `'-` になって Excel に見えていた | 絞り込み条件に一致する全件。★20,000行を超えると出せない★（`MAX_CSV_ROWS` `lib/config/index.ts:471`。`a961829`） | 指示書AO / 指示書AS D11（CSV は日本語固定）/ D-163（共通部品 `lib/csv.ts`）/ D-164 / D-171 | 確定（行数の上限は暫定値） |

**サンプルデータのとき**: 行・完了率・連続問題日数・1h前ST などは `lib/fixtures/clean-logs.ts` に書かれた値です（完了率は面積から計算しない。判定だけ `judgeGrade()` を通す。`clean-logs.ts:137-138`）。清掃結果は `lib/fixtures/task-status-rule.ts` の巡回列（CSV実測の分布 4:161 / 6:22 / 5:4 / 3:2）で決まります。★1h前ST / 30分前ST は既定が「待機中」で、施設11（バリューマート川下店）の履歴だけ `null`（事前チェックの無い施設。D-171）★ — ★既定値を `??` で入れないこと★（`null` まで「待機中」に化ける）。

#### 行を開いて「エリア別詳細を見る →」— エリア別清掃完了率（モーダル）

取得は `GET /api/clean-logs/{id}/areas`（`app/api/clean-logs/[id]/areas/route.ts`）。`{id}` の行から ★施設と日付（JST）★を取り出し、★同じ施設・同じ日の本清掃の予定すべて★を並べます（`prismaSource.areaCompletions()` `lib/db/index.ts:1358-1443`）。★1エリア＝1ロボット★で、1回の清掃の中の売り場別の内訳ではありません（`lib/clean-logs/areas.ts:11-12`）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 全体 `N%` と判定 | `全体 78% ○` | `clean_task_reports.clean_area` / `task_area` | 各エリアの完了率（`null` を除く）の単純平均（`overallCompletionTally()` `lib/clean-logs/areas.ts:101-112` → `meanCleanRatio()`）。判定は `judgeGrade()` | ★「問題エリアのみ」に切り替える前の全エリア★（`areas.ts:134-135`）。全エリアが `null` なら `-` / × | D-155 / 指示書AQ §5-5（以前は全体だけ面積の合計どうしの比だった。指示書Z 決定7 を変更）/ D-158（確定） | 確定 |
| 各エリアの完了率と判定 | `売場A 45% △` | `sections.section_name`、`clean_task_reports.clean_area` / `task_area` | `completionRateFromReport()`（清掃詳細ログの1行と★同じ関数★。`lib/db/index.ts:1424`）。並びは完了率の昇順・`null` が先頭（`areas.ts:75-87`） | その施設のその日の本清掃。予定時刻を過ぎたもの | 指示書Z / D-155 / D-158（確定） | 確定 |
| 「問題エリア（n / m エリア）」/「全 n エリア」 | `問題エリア（2 / 5エリア）` | 同上 | n＝判定が △× のエリア数、m＝全エリア数（`buildAreaCompletions()` `areas.ts:145-171`） | 同上 | 指示書Z 決定8・決定12 | 確定 |

**サンプルデータのとき**: `lib/fixtures/areas.ts` の行です。面積は清掃ログの完了率から逆算して作り（`clean_area = task_area × completion_rate ÷ 100`。`areas.ts:69`）、★完了率そのものは清掃ログと同じ値★です。

---

### 5B-6. アラート（`/alerts`）

1枚のカードは次の★2種類★のどちらかです（`prismaSource.prechecks()` `lib/db/index.ts:1491-1744`。`a961829`）。

- ★事前チェック異常★ — 事前チェックの予定（`precheck` が 60 / 30 など）を指す `robot_error_reports` の1行
- ★出発失敗★ — 本清掃の予定のうち、予定時刻を過ぎてもレポート行が無いもの（清掃詳細ログの「出発 ✗」と★同じ出来事★。D-103）

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 未解消（要対応） | `5` | `robot_error_reports.status`（`prisma/schema.prisma:332`）、出発失敗は `clean_timetables` | 全件 − 解消済み（`summarizePrechecks()` `lib/prechecks/query.ts:182-188`）。★「対応中」と★出発失敗のすべて★を含む★（出発失敗は書き込み先が無いので常に未解消。`lib/db/index.ts:1727-1729`） | ★日付範囲・分類で絞ったあと、対応状況で絞る前★（`app/api/prechecks/route.ts:72-75`）。日付は親の本清掃の予定日 | D-054（サマリは対応状況の絞り込みに追従させない）/ D-105 | 確定 |
| 解消済み（対応完了） | `2` | `robot_error_reports.status = 2` | `resolutionStatusFromRobotErrorStatus()`（`lib/db/precheck-mapping.ts:38-51`。0 未解消 / 1 対応中 / 2 解消） | 同上 | D-065 Q1-3 と同じ値域（TeamWorks 第2回回答） | 確定 |
| 合計（全アラート） | `7` | 上の2つ | 件数（`query.ts:181`） | 同上 | D-054 | 確定 |
| 種別「{n}分前異常」/「出発失敗」 | `30分前異常` | `clean_timetables.precheck`（`prisma/schema.prisma:604`） | `precheck` の値そのもの（分）を `precheckMinutesFromCode()`（`precheck-mapping.ts:123-126`）で読み、`precheckSeverityLabel()`（`lib/prechecks/label.ts`）で文言に | エラー行が指す事前チェック予定 | ★TeamWorks 第7回 A5-2（`precheck` は「本清掃開始の何分前か」の数値そのもの）/ D-112★。`20` なら「20分前異常」 | 確定 |
| ロボット状態（札） | `EmergencyStop` / `Standby.` | `robot_error_reports.error_type` | 変換しない（`precheckItemFromErrorRow()` `precheck-mapping.ts:279-318`）。出発失敗の行は `Standby.`（`robotStatusFromErrors([])`。`lib/db/index.ts:1720`） | — | D-077（[CLAUDE.md](../CLAUDE.md) §4.8） | 確定 |
| 予定 `HH:MM` | `予定: 02:00` | `clean_timetables.start_at`（★親の本清掃の予定★） | `jstIsoFromDb()` → `formatHm()` | — | 指示書S 3-2 項5（事前チェック予定の時刻ではなく本清掃の時刻を出す） | 確定 |
| 検知 `M/D HH:MM` | `検知: 10/4 01:31` | ★`robot_error_reports.errored_at`、無ければ `created_at`★。出発失敗は `clean_timetables.start_at` | `detectedAtFromError()`（`precheck-mapping.ts:246-248`）→ `jstIsoFromDb()` | — | 指示書S 3-4 / D-105。★`created_at` は TIMESTAMP で、2026-10-02 以前のステージングでは UTC で書かれていた★（第10回 N-14b）。`errored_at` が空の行だけ影響する | `errored_at` が空の行の時刻は再実測待ち |
| 残量 `N%` | （★出ない★） | （`robots.battery` は読まない） | 常に `null`（`precheck-mapping.ts:314`） | — | D-090 項8 — `robots.battery` は現在値だけで履歴が無く、過去のアラートに今の値を貼ることになるため | 確定 |
| ★「本清掃は成功」の Pill とカードの薄さ★ | `本清掃は成功` | 親の本清掃の `clean_task_reports`（`clean_area` / `task_area`。最新の1行） | ★`main_run_succeeded` ＝ レポート行があり、完了率の判定が ◎○★（`mainRunSucceededFromReport()` `precheck-mapping.ts:339-343` — `notExecutedFromReport()` → `completionRateFromReport()` → `judgeGrade()`（生値）→ `SUCCESS_GRADES`）。true ならカードの表示を不透明度 0.6 にし Pill を足す（`AlertsScreen.tsx:107-112` / `:592`） | 事前チェック異常の行。★出発失敗の行は常に false★（`lib/db/index.ts:1710`）。親のレポートは本清掃の id でまとめて引いた Map から取る（1行ずつクエリしない） | ★D-171（ユーザー判断 2026-10-07。案2）★。成功の定義はダッシュボードと同じ（`SUCCESS_GRADES`） | 確定。★表示だけ — 対応状況・件数は変えない★ |
| 対応状況・備考（★編集モード★） | `未解消` / `対応中` / `解消` | `robot_error_reports.status` / `responded_at` / `resolved_at` / `comment`（★書き込む4列★） | ★通常は表示だけ。「編集」→ プルダウンと入力欄で下書き → 「保存（N件）」で変えた行だけを1行ずつ PATCH★（下書きは `lib/prechecks/edit-drafts.ts`、日時の規則は `lib/prechecks/status-update.ts`。書き込みは `lib/db/robot-error-write.ts`） | ★事前チェック異常の行だけ★。出発失敗は札のまま | D-152（ステージングへの書き込み解禁）/ D-156 項3〜4 / 指示書AT / ★D-161（プルダウン）→ D-165（編集モード）★ | 確定 |
| 件数「全{total}件中…」 | `全7件中 1〜7件` | — | 対応状況で絞ったあとの件数（`meta.total`） | — | D-054（`summary.total` と別物） | 確定 |

**サンプルデータのとき**: `lib/fixtures/prechecks.ts` の行に、画面からの更新をメモリ上に重ねたものです（`lib/db/fixture-precheck-overrides.ts`。再起動すると消える）。★`main_run_succeeded` は各行に明示してあり、true は id 6（「30分前までに充電完了、出発成功」）だけ★（id 7 は完了率62%＝△なので false。D-171）。

---

### 5B-6b. エラー一覧（`/errors`）

1行は `robot_error_reports` の1行です（`errorReportItemFromRow()` `lib/db/error-report-mapping.ts:163`）。★事前チェック・本清掃・予定なし・`robot_id` が NULL（制御盤・セキュリティ・清掃開始失敗）を問わず★出します（D-160）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 検知時刻 | `10/5 19:05` | ★`robot_error_reports.errored_at`、無ければ `created_at`★ | `detectedAtFromError()` → `jstIsoFromDb()`（`/alerts` と同じ） | — | D-105 / 指示書AW D3 | 確定 |
| 施設名 | — | `facilities.facility_name` | そのまま | — | — | 確定 |
| シリアル / 種類 / レベル | `0` / `SECURITY_ON` / `CRITICAL` | `robot_error_reports.serial_number` / `error_type` / `error_level` | ★変換しない・`t()` にも通さない★ | — | D-077 / D-160 項3（区分を作るのは次フェーズ） | 確定 |
| 紐づく予定 | `事前チェック（30分前）` | `robot_error_reports.clean_timetable_id` → `clean_timetables` / `clean_timetable_histories` の `precheck` | `errorScheduleLink()`（`error-report-mapping.ts:134`）: NULL → 予定なし / 2表に無い id → 予定不明 / `precheck` が本清掃 → 本清掃 / それ以外 → 事前チェック（★生の分数★）。文言は `errorScheduleLabel()`（`lib/error-reports/label.ts:27`） | 予定は2表から引く（同じ id は本表） | 指示書AW D4・D5 | 確定 |
| 対応状況・備考（★編集モード★） | `未解消` / `対応中` / `解消` | `robot_error_reports.status` / `responded_at` / `resolved_at` / `comment`（★書き込む4列★） | `/alerts` と同じ（★「保存（N件）」で変えた行だけ1行ずつ★ → `planPrecheckStatusUpdate()` → `writeRobotErrorColumns()`。下書きは `lib/prechecks/edit-drafts.ts`） | ★行があれば全部★ | D-160 項2 / 指示書AT / ★D-161 → D-165★ | 確定 |
| 件数「全{n}件」 | `全10件` | — | 絞り込み後・ページ分け前の件数（`meta.total`） | ★既定は `status` が 未解消(0)・対応中(1)★（`errorReportStatusCodes()` `lib/error-reports/query.ts:123`。0/1/2 を直書きしない）。3以上の行は出さず例外にもしない | D-160 項1 / 指示書AW 表2 F12 | 確定 |

★並び★は検知時刻の降順、同時刻は `error_id` の昇順（`compareErrorReportKeys()` `query.ts:134`）。★絞り込みの正は `filterSortErrorReportKeys()`（`query.ts:151`）で、SQL の `where`（`errorReportListWhere()` `error-report-mapping.ts:205`）はその写し★です（種類の部分一致は SQL に降ろさない）。

**サンプルデータのとき**: `lib/fixtures/error-reports.ts` の20行（DB の行の形。うち見える行は18行）に、画面からの更新をメモリ上に重ねたものです（`lib/db/fixture-error-report-overrides.ts`。再起動すると消える。`/alerts` の上書き表とは別）。既定15行・「解消済みも表示」で18行（削除済みの施設・清掃エリアの2行はどちらでも出ない。U8。[docs/fixtures.md](fixtures.md) §13）。

---

### 5B-7. レポート（`/reports`）

期間は週次 / 月次 / 年次から選び、‹ › で前後に動かせます。★週は日曜〜土曜で、月をまたぐ週は1日で切ります★（週の長さは1〜7日。`periodRange()` `lib/format/period.ts:155-197`。D-071）。★期間が「確定期間」か「当期（進行中）」かで、数字の取得元が変わります★。

| | 確定期間（先週・先月・前年など） | 当期（今週・今月・今年） |
|---|---|---|
| 判定 | `isCurrentPeriod()`（`period.ts:204-217`）が偽 | 今日（JST）が期間内 |
| KPI | ★`customer_summaries` の率4列をそのまま★ | ★`clean_timetables` × `clean_task_reports` × `robot_error_reports` からその場で集計★ |
| 施設別パフォーマンス | ★`facility_summaries` の率4列をそのまま★ | 同上（施設ごと） |
| エラー種別の内訳 | ★`facility_error_summaries.error_count` の合計★ | `robot_error_reports` の数え上げ |
| 連続問題施設 | ★どちらも詳細テーブルから算出★（対応するサマリが無い。D-102 項4） | 同左 |
| 前期比の「前期」 | 1つ前の期間のサマリ行 | ★1つ前の期間（＝確定期間）のサマリ行★ |
| 根拠 | ★TeamWorks のバッチが作ったサマリを正とし、履歴から再計算しない★（D-102 / [CLAUDE.md](../CLAUDE.md) §7）。年次は D-151 で解禁 | ★進行中の期間はサマリに存在しない★（[CLAUDE.md](../CLAUDE.md) §4.7。バッチは週次が毎週日曜6:00、月次が毎月1日6:00、年次が毎年1/1 6:00。第10回 N-12） |

サマリ行がどの粒度かは `month` / `week` が 0 かどうかで決めます（`week ≥ 1` 週次 / `week = 0` 月次 / `month = 0 かつ week = 0` 年次。`periodOfSummaryRow()` `lib/db/report-mapping.ts:100-104`。第5回 X-2 で TeamWorks 承認。D-074 項3）。★当期にもサマリ行があった場合（バッチが先に走った場合）は、サマリ行が優先されます★（`lib/db/index.ts:2596-2598`）。

**「何日の清掃か」の決め方**: 当期の集計は予定の日付（`clean_timetables.start_at`）で期間に振り分けます（`aggregateCleanStatsByFacility()` `lib/db/report-mapping.ts:473-474`）。TeamWorks のサマリ（確定期間）は `clean_task_reports.started_at` の日付で決めます（第10回 N-9 = A）。★2つは一致します★ — ★予定のある清掃では `started_at` に予定の `start_at` と同じ値が入るため★です（ユーザー回答 2026-10-05「A」。[CLAUDE.md](../CLAUDE.md) §4.7）。例: 9/30 23:50 の予定が 10/1 00:10 に始まっても、`started_at` は 9/30 23:50 なので、どちらも 9/30 の清掃として数えます。

★その代わり、予定のある清掃の**実際の開始時刻**はこの列からは分かりません★（定刻出発の判定には使えない）。

#### KPI（4枚のカード）

★KPI は1社（顧客）に定まるときだけ出します★。候補が2社以上なら「顧客を選んでください」と出ます（`lib/reports/target-customer.ts:1-37`。サマリに稼働率・清掃完了率の分子・分母が無く、複数社を合算できないため。D-133。第11回 P-6 で出発成功率・エラー発生率の材料はそろったが、方針は据え置き。D-158）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 稼働率（確定期間） | `92%` | `customer_summaries.operation_ratio`（`prisma/schema.prisma:707`） | そのまま（`kpiFromSummaryRatios()` `lib/db/report-mapping.ts:231-241`） | `customer_id` が対象の1社、`year` / `month` / `week` が一致する行（`lib/db/index.ts:2402-2417`） | D-102。★式は正常終了（status 4）の回数 ÷ 予定回数★（第11回 P-6=A。D-158）。分子の列はサマリに無い | 値は TeamWorks のバッチが正 |
| 稼働率（当期） | `88%` | `clean_timetables` ＋ `clean_timetable_histories` の `id` / `start_at` / `facility_id`、`clean_task_reports.status` | ★正常終了（`status = 4`）の回数 ÷ 予定回数 × 100★（`operationRate()` `lib/judge/kpi.ts:36-38`。正常終了かは `isCompletedTask()`。集計は `aggregateCleanStatsByFacility()` `report-mapping.ts:460-505` を全施設ぶん合算 `sumCleanStatsBuckets()` `:333-339`） | 対象の1社の施設の本清掃で、期間内・予定時刻を過ぎたもの。予定外は入らない | [CLAUDE.md](../CLAUDE.md) §4.2 / ★第11回 P-6=A（D-158。指示書AU 段階3）★ | 確定 |
| 出発成功率（確定期間） | `95%` | `customer_summaries.launch_ratio`（`:708`） | そのまま | 同上 | D-102。TeamWorks「`launch_count` は清掃タスクを開始できた回数」（第10回 Q6-2 / Q11-3） | 値は TeamWorks が正 |
| 出発成功率（当期） | `90%` | 稼働率（当期）と同じ表の、`clean_task_reports` の行の有無 | ★開始できた回数（レポート行がある回数）÷ 予定回数 × 100。時刻の条件なし★（`launchRate()` `kpi.ts:44-46`） | 同上 | ★第11回 P-6=A（D-158）★。サマリの `launch_ratio` と同じ式 | 確定 |
| 清掃完了率（確定期間） | `81%` | `customer_summaries.clean_ratio`（`:709`） | そのまま | 同上 | ★TeamWorks のサマリの式＝1回ごとの面積比（上限100・小数第2位以下切り捨て）の平均、本清掃のみ・`clean_timetable_id` が NULL の行は除く★（第10回 N-3） | 値は TeamWorks が正 |
| 清掃完了率（当期） | `79%` | `clean_task_reports.clean_area` / `task_area` | 1回ごとの完了率の単純平均（`kpiFromCleanStats()` `report-mapping.ts:516-535` → `meanCleanRatio()`。1回ごとの値は `runCompletionRate()`。`:492`） | 上と同じ本清掃のうち、実施した（レポート行がある）もの。★`task_area = 0` は 0% として含める（P-5=B）★ | ★D-155 項1★ — 確定期間（サマリ）と同じ式にそろえ、前期比が同じ式どうしの比較になるようにした。★第11回 P-2=A で確定（D-158）★ | 確定 |
| エラー発生率（確定期間） | `4%` | `customer_summaries.error_ratio`（`:710`） | そのまま | 同上 | D-102 | 値は TeamWorks が正 |
| エラー発生率（当期） | `5%` | `robot_error_reports.clean_timetable_id`、分母は上の予定回数 | ★エラー件数 ÷ 予定回数 × 100。上限なし★（`errorRate()` `kpi.ts:182-184`。第11回 P-6=B）。件数は★本清掃の予定を指すエラー行★の数（`errorCountByMainTimetable()` `report-mapping.ts:380-392`） | 事前チェックの予定を指すもの・`clean_timetable_id` が NULL のもの・指す先が無いものは数えない | TeamWorks 第8回 A7（事前チェックはサマリに含めない。D-114 項2）/ 第10回 Q4-1=A・Q4-2=B | 確定。★制御盤・セキュリティのエラーの扱いは次フェーズ★（第11回 P-8〜P-10。D-158 当方の対応5） |
| 前期比「▲N% 前週比」 | `▲3% 前週比` | 今期と前期の値 | ★今期 − 前期（パーセントの差＝ポイント）★（`toKpiValue()` `kpi.ts:196-202`）。矢印・数・色は `kpiDeltaView()`（`lib/format/kpi-delta.ts`。`KpiCard.tsx:72`）— 数は絶対値を四捨五入し、★丸めて0なら「→」・色なし★（施設別の表の「変化」と同じ。D-183。旧: 生値の符号で「▼0%」を改善の色で出していた）。エラー発生率だけ「下がると良い」 | どちらかが `null` なら行ごと出さない | 指示書F 5-4。★「3% 上がった」ではなく「3ポイント上がった」の意味★ | 確定 |
| 「前週: N%」（前期の値） | `前週: 89%` | ★前期のサマリ行★（`customer_summaries` の率） | そのまま | 1つ前の期間（`shiftPeriod()` `lib/format/period.ts:267-`） | D-102 | 確定 |

当期の KPI と確定期間の KPI は、★4つとも同じ式★（完了率は D-155、3つの率は D-158 以降）ですが★別の人が計算した数字★です。確定期間は TeamWorks のバッチ、当期はアプリです。

#### 連続問題施設

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| Lv と「{n}日」 | `Lv3 / 4日` | `clean_timetables` × `clean_task_reports`（`loadConsecutiveInputs()`） | ★期間の最終スケジュール日時点★の連続問題日数（`consecutiveProblemDaysAtPeriodEnd()` `report-mapping.ts:594-603`）と L1〜L3（`judgeEscalationLevel()`）。上限を超えたら「{n}日以上」（`capContinuousProblems()` `:826-849`） | ★2日以上★の施設だけ（`filterContinuousProblems()` `lib/reports/query.ts:431-437`。`ESCALATION_THRESHOLDS.level2Days`）。`facilities.deleted_at` が NULL の施設（★ロボット0台でも出す★）。連続日数の降順。最初の10件＋「他{n}件を表示」 | ★対応するサマリテーブルが無い★（D-102 項4）ので、確定期間も当期も詳細テーブルから / [CLAUDE.md](../CLAUDE.md) §4.5 | 確定 |
| 説明文「{n}日連続で△以下（うち{m}日は出発失敗）」 | `4日連続で△以下（うち1日は出発失敗）` | 同上 | m＝連続のうち `departed === false` の行がある日の数（`launchFailedDaysInRun()` `report-mapping.ts:648-665`）。文言の選び方は `continuousProblemSummary()`（`:683-690`） | 連続は期間をまたいで遡る | 指示書W（混在のときの文言は C案。ユーザー確定 2026-09-11）/ D-156 項1（出発失敗は `departed` で数える） | 確定 |

#### エラー種別の内訳

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 種別名 | `EmergencyStop` | 確定: `facility_error_summaries.error_type` / 当期: `robot_error_reports.error_type` | 変換しない（空のときだけ代替文言。`formatRawLabel()`） | — | D-077 | 確定 |
| 件数（確定期間） | `12` | `facility_error_summaries.error_count`（`prisma/schema.prisma:785`） | 同じ期間・同じ `error_type` を全施設ぶん足す（`errorBreakdownRowsFromSummary()` `report-mapping.ts:906-931`） | `year` / `month` / `week` が一致し、絞り込んだ施設のもの（`lib/db/index.ts:2985-2995`） | ★施設版のサマリで確定（`customer_error_summaries` は読まない。D-102 項4）★ | 値は TeamWorks が正 |
| 件数（当期） | `3` | `robot_error_reports.clean_timetable_id` / `error_type`、`clean_timetables.start_at` | ★本清掃の予定を指すエラー行★を `error_type` ごとに数える（`countMainErrorTypesInRange()` `report-mapping.ts:955-971`） | 期間の判定は★エラーが指す本清掃の予定日★（`errored_at` ではない） | 第8回 A7 / 決定7（エラー発生率の分母が清掃の回数なので、分子も清掃を基準にする） | 確定（制御盤分は未定） |
| 割合 `N%` と棒 | `40%` | 上の件数 | ★その種別の件数 ÷ 全種別の件数の合計 × 100★（`errorBreakdownItemsFromCounts()` `report-mapping.ts:885-895`）。表示は四捨五入（`ReportsScreen.tsx:816-818`） | 同上 | ★「エラー発生率」ではなく「内訳の構成比」★ | 確定 |
| 見出しの「この期間のエラー発生件数」 | `この期間のエラー発生件数（種別ごと）` | — | 固定文言（`ReportsScreen.tsx:782`）。期間は上の期間ナビに出ている | — | ★旧: 週次 / 月次 / 年次だけを見て「今週 / 今月 / 今年」と出し、‹ で先週に戻っても「今週」のままだった★（D-183 で「この期間の」に直した） | 確定 |

#### 施設別パフォーマンス（稼働率ワースト順）

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 稼働率 / 出発成功率 / 完了率 / エラー率（確定期間） | `88%` | `facility_summaries.operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio`（`prisma/schema.prisma:741-744`） | そのまま（`kpiFromSummaryRatios()`） | その期間のサマリ行がある施設。★`facilities.deleted_at` が NULL で、ロボットが1台以上★（`lib/db/index.ts:2631-2649`） | D-102 | 値は TeamWorks が正 |
| 同上（当期） | `88%` | `clean_timetables` × `clean_task_reports` × `robot_error_reports` | KPI（当期）と同じ式を施設ごとに（`aggregateCleanStatsByFacility()` → `kpiFromCleanStats()`） | 期間内に本清掃の予定がある施設（未来の予定しか無い施設も「判定なし」で出す。`report-mapping.ts:427-431`） | KPI と同じ | 確定（D-158） |
| 前週 / 前月（稼働率・完了率の横） | `90%` | 前期の `facility_summaries` | そのまま | 同じ施設の1つ前の期間 | D-102 | 確定 |
| 変化 `▲N` / `▼N` / `→` | `▲2` | 上の差 | ★（稼働率の差 ＋ 完了率の差）÷ 2 を四捨五入★（`overallDiff()` `lib/reports/facility-performance-labels.ts:22-27`。★D-163 で表から移し、画面と CSV で共有★）。どちらかが無ければ `→` | — | モック :795-797 / 指示書F 5-7 | 確定 |
| ステータス | `注意` | 稼働率 | 稼働率が 90 以上 良好 / 75 以上 注意 / それ未満 要改善 / 値なし 判定なし（`judgeFacilityStatus()` `lib/judge/kpi.ts:224-231`）。表示名は `FACILITY_STATUS_LABEL`（`lib/reports/facility-performance-labels.ts:42`。画面と CSV で共有。D-163） | — | ★DFA の画面上の判断（TeamWorks 照会なし）★。`FACILITY_STATUS_THRESHOLDS` は暫定値。「判定なし」はユーザー確定（決定1。2026-09-11） | ★暫定★ |
| セルの色 | 赤字 | 稼働率・出発成功率・エラー率 | 稼働率・出発成功率は上と同じ 90 / 75（`rateTone()` `FacilityPerformanceTable.tsx:78-84`）。エラー率は 8 を超えると赤（`FACILITY_TABLE_THRESHOLDS.errorRateWarning` `lib/config/index.ts:571-574`） | — | モックの 80 / 90 を設定値に寄せた（意図的な逸脱。`FacilityPerformanceTable.tsx:58-77`） | 表示上の目安 |
| 「N施設」 | `10施設` | — | 状態・施設名・シリアルで絞ったあとの件数（`meta.total`）。最初の10件＋「全N施設を表示」。★全件表示でも上限（200）で切れたら注記★（D-183） | 並びは稼働率の昇順・値なしは末尾（`sortFacilityPerformance()` `lib/reports/query.ts:380-395`） | 指示書F | 確定 |

**サンプルデータのとき**: KPI・施設別パフォーマンス・連続問題施設・エラー内訳は★`lib/fixtures/reports.ts` に直接書かれた作り値★です（計算していない）。KPI は★顧客1の数字としてだけ★持っていて、他の顧客を選ぶと空になります（`reports.ts:203-214`）。★エラー内訳はサンプルデータでは施設で絞り込めず、全社ぶんが出ます★（`lib/db/index.ts:815-844`）。

#### CSVエクスポート

★**施設別パフォーマンスの表を CSV で出します**★（D-163。2026-10-06。★それまでは押しても何も起きない仮の実装★だった — API 契約に出力の口が無く、指示書F の「やらないこと」だったため。D-069 項8）。ユーザー判断は「A ＝ 施設別パフォーマンスの表」（却下: KPI・両方）。

| 項目 | 中身 | 場所 |
|---|---|---|
| 口 | `GET /api/reports/facility-performance/export`（★表と同じパラメータ。`limit` は無視★） | `app/api/reports/facility-performance/export/route.ts:25`。契約は `d-watch-api.yaml:1042` |
| どの施設を、どの順で | ★表と同じ1本 `loadFacilityPerformance()` が「取得 → 絞り込み → 並べ替え → 連続日数の上限処理」まで済ませた全件★（`limit` で切らない）。稼働率ワースト順 | `lib/reports/facility-performance-load.ts:38`。一覧のルートは戻り値を `slice(0, limit)` するだけ（`app/api/reports/facility-performance/route.ts:33` / `:50`） |
| 列 | ★画面の9列と同じ順＋施設名の直後にシリアル★: 施設名 / シリアル / 稼働率 / 稼働率（前週・前月・前年）/ 出発成功率 / 完了率 / 完了率（前◯）/ エラー率 / 変化 / ステータス | `facilityPerformanceCsvHeader()`（`lib/reports/facility-performance-csv.ts:29`） |
| 値 | 率は `csvRate()`（四捨五入の整数＋`%`。★値が無ければ空のセル★。D-164）。「変化」は `overallDiff()` / `diffText()`、ステータスは `FACILITY_STATUS_LABEL`（★画面と同じ関数・同じ表★。`lib/reports/facility-performance-labels.ts`）。★ここで判定し直さない★ | `buildFacilityPerformanceCsv()`（`facility-performance-csv.ts:56`） |
| 形式 | 清掃詳細ログの CSV と同じ（UTF-8 BOM・CRLF・★全セルに数式インジェクション対策 D-125★・画面の言語によらず日本語） | `csvDocument()`（`lib/csv.ts:88`） |
| ファイル名 | `facility-performance_monthly_2026-09.csv` の形（週は `_2026-10-w1`、年は `_2025`）。★parse 済みの数値から組み立てる★（生のクエリ文字列を入れない） | `facilityPerformanceCsvFilename()`（`facility-performance-csv.ts:87`） |
| 行数の上限 | 清掃詳細ログと同じ `MAX_CSV_ROWS`（20,000。★`>` で超えたら 422★、`details: { total, max }`）。施設数なので実際には起きない（歯止めを揃えるだけ） | `route.ts:55-68` |

★**表と CSV が食い違わない仕掛けは2段**★です。画面側はクエリ文字列を1か所（`facilityFilterQuery`）から作り、表だけに `limit` を足す（`ReportsScreen.tsx:187-211`）。サーバー側は一覧も CSV も `loadFacilityPerformance()` を呼ぶ。★片方だけ絞り込みを足すと、例外もテスト失敗も出ずに表と CSV の施設がずれます★。

却下: 画面に出ていない列（連続問題日数・前期の出発成功率／エラー率など）を足す（ユーザー未確認。要望があれば足す）。

---

### 5B-8. 清掃面積（`/clean-area`）

取得は `GET /api/clean-area`（`app/api/clean-area/route.ts`）→ `prismaSource.cleanArea()`（`lib/db/index.ts:2324-2372`）→ `aggregateCleanArea()`（`lib/clean-area/aggregate.ts:165-248`）。★この画面だけ、完了率の材料と面積の材料で読む行が違います★（D-155 項3 / 指示書AS D9）。

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 清掃面積（カード・表） | `341,635㎡` | `clean_task_reports.clean_area`（`prisma/schema.prisma:534`） | 施設ごとの合計（`groupBy` + `_sum`。`loadCleanAreaFacilityStatsWithRowCount()` `lib/db/clean-area-source.ts:378-426`）を粒度ごとに足す。表示は整数＋`㎡` | ★全行（予定外のタスク・事前チェックの予定に紐づく行も含む）★。期間は★`clean_task_reports.started_at`（実際に清掃を始めた時刻）★で絞る（`index.ts:2331-2350`）。★期間未指定なら全期間★（データ保持2年ぶん）。施設マスタに無い `facility_id` は合計からも外してログに出す | ユーザー指示 2026-09-16（総清掃面積・指定期間の絞り込み）/ 指示書AN。★予定外を含めるのは「実際に清掃した広さ」だから★（D-155 項3。ユーザー判断 2026-10-02） | 確定（単位は DFA 判断） |
| 清掃対象面積 | `350,000㎡` | `clean_task_reports.task_area`（`:533`） | 同上 | 同上 | 同上 | 確定 |
| 完了率 | `78%` | `clean_task_reports.clean_area` / `task_area` / `clean_timetable_id`、`clean_timetables.precheck` | 1回ごとの完了率の単純平均（`cleanAreaStatsFromRows()` `clean-area-source.ts:287-327` で Σ と件数 → `meanCleanRatio()`。`aggregate.ts:226-229` / `:239-242`） | ★`clean_timetable_id` が NULL でなく、その予定が本清掃（`precheck = 0`）の行だけ★（`clean-area-source.ts:294-295`）。`task_area = 0` は除く。期間は `started_at` | ★D-155 項1・項3★ — 完了率の定義は全画面で1つ（D-151 項4-4）。★面積の合計どうし（Σ`clean_area` ÷ Σ`task_area`）では割らない★ / D-158（確定） | 確定 |
| 清掃実施回数 | `1,024` | 同上 | 完了率と同じ母集団の行数（`clean-area-source.ts:306`）。★`task_area = 0` の行も1回に数える（完了率は 0%）★ | 同上 | D-155 項3（予定外は数えない）。★2026-10-02 に意味が変わった★（旧: 予定外を含む全行数） | 確定 |
| 内訳の行（店舗 / 顧客 / ホールディングス）と「{n}件」 | `店舗 12件` | 施設マスタ `facilities` / `customers` / `groups` | 粒度ごとに Σ と件数を足してから1回だけ割る（`aggregateCleanArea()`）。並びは清掃面積の降順（`aggregate.ts:258-264`）。最初の10件（`CLEAN_AREA_INITIAL_VISIBLE` `lib/config/index.ts:823`） | 施設マスタは `deleted_at` で絞らない（`clean-area-source.ts:41-61`。過去の実績が合計から消えないように）。所属なしの顧客は「グループなし」の1行 | 指示書AN / D-011 | 確定 |
| 注記「清掃面積には予定外の清掃を含みます…」 | — | — | 固定文言（`CleanAreaScreen.tsx:377-382`。`f2b91fc`） | — | D-156 論点7（面積はあるのに実施回数0の施設を見た人が不具合と思わないように） | 確定 |

★**他の画面と違う点が3つあります**★（5B-12 で理由つきで並べます）: ①予定外のタスクの面積を含む ②期間の基準が `started_at`（★予定のある清掃では予定の時刻と同じ値なので、他の画面と同じ日に数える★。違うのは予定外のタスクだけ） ③同じ予定にレポートが2行あると、★2行とも数える★（他の画面は最新の1行だけ）。③は「番人」が件数だけ数えてログ（`clean_task_report_rows_per_timetable`）に出します（`countDuplicatedTimetables()` `clean-area-source.ts:456-469`）。

**サンプルデータのとき**: `lib/fixtures/areas.ts` の面積行から作ります（`cleanAreaStatsFromFixtureAreas()` `lib/db/fixture-clean-area.ts`）。★サンプルデータには予定外のタスクがありません★。未実施の行（完了率 `null`）は数えません。番人は常に 0 です。

---

### 5B-9. D-Secログ（`/ds-logs`）

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 合計 / 正常 / 異常 | `12` / `11` / `1` | `controls.status`（`prisma/schema.prisma:211`） | 台数を数える（`summarizeDsDevices()` `lib/ds-devices/query.ts:147-152`）。0 → 正常、1 → 異常（`connectionStatusFromControlStatus()` `lib/db/ds-status.ts`）。★0 / 1 以外の制御盤は一覧に出さない（`isListedControlStatus()`。ログを残す。D-182）★ | `controls.deleted_at` が NULL（`inactive` では絞らない。`lib/db/index.ts:1528-1531`）。分類で絞ったあと、検索語・状態で絞る前（`app/api/ds-devices/route.ts:86-96`） | TeamWorks 第2回回答（D-065 Q1-2）/ D-054 | 確定 |
| 接続状態（札） | `正常` | 同上 | 同上 | 同上 | 同上 | 確定 |
| 最終通信 | `10/5 09:12` | `control_reports.reported_at`（`:373`。TIMESTAMP） | 制御盤ごとの★最大値★（`groupBy` + `_max`。`lib/db/index.ts:1535-1539`）→ `jstIsoFromDb()` | 制御盤ごと。★報告が1件も無い制御盤は `last_reported_at: null`（画面は「-」）でログを残す★（★旧: 例外で画面ごと止まった。D-182★） | ★`MAX(id)` ではなく `MAX(reported_at)`★ — `id` の順と時刻の順が一致する保証が無いため（`index.ts:1511-1520`）。TeamWorks 第10回 Q8-3「`control_reports` は制御盤1台1行（最新の報告だけ）」 | ★2026-10-02 以前のステージングでは UTC で書かれていた★（第10回 N-14a）。JST に統一済みと先方申告、当方の再実測は未了 |
| 備考 | `電源工事中` | ★DB ではない★（`data/settings/ds-device-notes.json`） | — | — | DB に対応する列が無い（D-065 Q1-1） | 確定 |
| 件数 | `全12件中 1〜12件` | — | `meta.total` | 絞り込み後 | — | 確定 |

**サンプルデータのとき**: `lib/fixtures/ds-devices.ts` の値です。

---

### 5B-10. D-Secアラート（`/ds-alerts`）

| 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|
| 未解消 / 対応中 / 解消済み（★3枚目のラベルはアラート画面に揃えた。D-181★） | `2` / `1` / `5` | `robot_error_reports.status` | 0 未解消 / 1 対応中 / 2 解消 の件数（`summarizeDsAlerts()` `lib/ds-alerts/query.ts:141` / `resolutionStatusFromRobotErrorStatus()` `lib/db/precheck-mapping.ts`） | ★行の施設・清掃エリアの `deleted_at` が NULL（U8）★。分類で絞ったあと、種別・状態で絞る前（`app/api/ds-alerts/route.ts`） | D-180 / D-181 | 確定 |
| 種別（札） | `通信断` | （固定） | ★DB からは常に「通信断」（`alert_type: "disconnected"`。D-180 項7）★ | — | `SECURITY_ON` は出さない（U1）。セレクトの「セキュリティ無効」は残す（U6） | 確定 |
| 検知 | `10/4 23:40` | `robot_error_reports.errored_at` / `created_at` | `errored_at ?? created_at`（`detectedAtFromError()`）→ 月日・時刻 | — | — | 確定 |

★編集モードで書ける（`PATCH /errors/{error_id}/status`）★（D-180）。

**サンプルデータのとき**: ★サンプルデータは `errorReportRows` の `D-S_CONTROL_FAILURE` 行（8行・出るのは7行）。セキュリティ無効の行は無い★。

---

### 5B-11. 施設ロボットマスタ（`/facilities`）・スケジュール（`/schedules`）・システム管理（`/system`）

この3画面は計算をほとんどしません。数字に見えるものの出どころだけ書きます。

| 画面 | 画面上の表示名 | 例 | テーブル.列 | 計算式（関数名 `ファイル:行`） | 対象にする行の条件 | 根拠 | 確定 / 暫定 |
|---|---|---|---|---|---|---|---|
| 施設ロボットマスタ | 監視 / D-Sec（●） | `●` | `facilities.contract`（`prisma/schema.prisma:141`。10桁の文字列） | 2桁目が `"1"` なら監視、1桁目が `"1"` なら D-Sec（`parseContract()` `lib/format/contract.ts:26-31` / `CONTRACT_DIGITS` `lib/config/index.ts:751-754`） | `facilities.deleted_at` が NULL（`inactive` では絞らない。`lib/db/index.ts:2141-2156`） | D-080。★数値として比較しない★（[CLAUDE.md](../CLAUDE.md) §4.6） | 確定 |
| 施設ロボットマスタ | ロボット（メーカー / 機種 / シリアル） | `PUDU / CC1 / 8111R…` | `devices.maker_id` / `model_name`、`robots.serial_number` | `maker_id` は `MAKER_NAMES`（1=PUDU / 2=Keenon。`lib/config/index.ts:715-718`）、表に無い番号は数字のまま | `robots.deleted_at` が NULL | D-085（番号の割り当ては DFA の暫定決定。`makers` テーブルが提供されない） | ★`maker_id` の割り当ては暫定★ |
| 施設ロボットマスタ | 件数 | `全12件中…` | — | `meta.total` | — | — | 確定 |
| スケジュール | 曜日 | `月水金` / `毎日` | `clean_schedules.weeks`（`:564`。「日月火水木金土」の7桁） | `weekdaysLabel()`（`lib/format/schedule.ts:17-`） | `clean_schedules.deleted_at` が NULL | TeamWorks 回答 B-5 | 確定 |
| スケジュール | 時間 | `02:00 - 翌04:30` | `clean_schedules.start_time` / `end_time`（`:566` / `:569`。4桁の文字列） | `end_time` の時が 24〜29 なら「翌」を付けて 24 を引く（`formatEndTime()` `schedule.ts:51-61`） | 同上 | TeamWorks 回答 B-5（日跨ぎを 24〜29 時で表す）/ D-060 | 確定 |
| スケジュール | 有効（● / ○） | `●` | `clean_schedules.inactive`（`:572`） | `inactive = 0` なら有効（`lib/db/index.ts:2223`。0 が有効なので論理が逆） | 同上 | — | 確定 |
| システム管理 | 事前チェック「第1回 60分前 / 第2回 30分前」 | `60分前` | ★DB ではない★ | `PRECHECK_MINUTES_BEFORE = [60, 30]`（`lib/config/index.ts:261`） | — | ★DFA 側の設定値★。実際に何分前のチェックだったかは DB の `clean_timetables.precheck` が正（D-105 項13-2）。1清掃につき2回で固定（ユーザー回答 2026-09-11） | 確定（編集不可） |
| システム管理 | 完了率の判定閾値 | `90% 以上` | ★DB ではない★ | `GRADE_THRESHOLDS`（`config/index.ts:18-25`） | — | D-139 | 確定（編集不可） |
| システム管理 | エスカレーション閾値 | `Lv2 2日以上連続` | ★DB ではない★ | `ESCALATION_THRESHOLDS`（`config/index.ts:44-49`） | — | [CLAUDE.md](../CLAUDE.md) §4.5 | 確定（編集不可） |
| システム管理 | 連続日数の表示上限 | `30` | ★DB ではない★（`data/settings/display-limits.json`） | 保存が無い・壊れているときは既定 30（`loadConsecutiveDaysMax()` `lib/settings/display-limits.ts:223-`）。3〜365 | — | ユーザー指示 2026-09-13（指示書AF）。★判定には効かない★（表示の上限と、遡って読む日数だけ） | ★この画面で唯一編集できる★ |

---

### 5B-12. ★数字が画面によって食い違って見えるとき★

次の食い違いは★仕様どおり★です。不具合を疑う前に、ここに当てはまらないか確かめてください。

| 見え方 | 理由 | 根拠 |
|---|---|---|
| ★清掃面積の画面だけ、清掃の回数や面積が他の画面より多く出る★ | 清掃面積の「面積」は★予定外のタスク（`clean_timetable_id` が NULL）と事前チェックの予定に紐づく行も含めて★全行を足す。他の画面は本清掃の予定から引くので予定外は出ない | D-155 項3（ユーザー判断 2026-10-02「実際に清掃した広さなので含める」） |
| ★清掃面積の画面で「面積はあるのに実施回数 0」の施設がある★ | 面積は予定外を含み、実施回数・完了率は含まないため。予定外の清掃しかしていない施設はこうなる | D-155 項3 / D-156 論点7。画面に注記あり |
| ★同じ期間を指定しても、清掃面積と清掃詳細ログで対象の清掃がずれる★ | 清掃面積は★実績の `clean_task_reports.started_at`★、清掃詳細ログ・レポートの当期は★予定の `clean_timetables.start_at`★で期間を絞る。深夜0時をまたぐ清掃や、予定より遅れて始まった清掃は別の日に入りうる | `lib/db/index.ts:2331-2350`（清掃面積）/ `:932`（詳細ログ）/ `report-mapping.ts:473-474`（レポート当期） |
| ~~月末・週末の境目で、レポートの確定期間（サマリ）と当期（アプリ）の数え方がずれうる~~ ★**解決済み・ずれない**★ | サマリは `clean_task_reports.started_at`、アプリの当期は予定の `start_at` で振り分けるが、★予定のある清掃では `started_at` に予定の `start_at` と同じ値が入る★ので一致する | ユーザー回答 2026-10-05「A」（[CLAUDE.md](../CLAUDE.md) §4.7）。★2026-10-05 にこの文書の第4版が「ずれうる」と書いたが、確定した事実により取り下げ★ |
| ★同じ予定にレポートが2行あると、清掃面積だけ2回数える★ | 他の画面は最新の1行（`ended_at` ?? `started_at`）だけを採る。清掃面積は `groupBy` の合計なので全行が入る。番人がログに件数を出す | `latestReportByTimetableId()` / `countDuplicatedTimetables()`。TeamWorks 第10回 N-8 は「1予定1行の想定（A）」 |
| ★2026-10-01 時点のステージングでは、清掃面積だけ2回分の数字が出て、詳細ログ・ダッシュボード・アラートは空★ | 予定（`clean_timetables`）が0行で、実績2行はどちらも `clean_timetable_id` が NULL だった（2026-10-01 実測・先方投入）。予定起点の画面には1行も出ない | [CLAUDE.md](../CLAUDE.md) §3 / D-151 項7（詳細ログの起点は保留）。★その後のデータ投入（第10回 N-6「10/5 中に入る」）は当方未確認★ |
| ★ダッシュボードの「進み具合」の日付と件数★ | ★対象日は日付を指定しないとき日本時間の前日★（清掃結果は前夜の分のため）。予定件数は★予定表（`clean_timetables` / 履歴）の本清掃の行★から数える（旧: `clean_schedules` の曜日。`clean_log_rows_vs_scheduled` の警告は廃止） | ユーザー判断 2026-10-29「B」/ CLAUDE.md §4.2.1 / `lib/dashboard/aggregate.ts` の `scheduleProgressTargetDate()` |
| ★ダッシュボードの数値カードと要レスキューの件数が合わない★ | 要レスキューは★当日に清掃予定が無い施設も含む★（前日以前のスケジュール存在日で判定する）。数値カードは★各清掃エリアの直近の清掃（7日以内）★（D-159。旧: 当日の予定だけ） | 指示書AI §1-5 / D-159 |
| ★ダッシュボードの数値カードとスケジュール進捗の件数が合わない★（2026-10-06 から） | 数値カードは★直近モード★（各清掃エリアの直近1件・前日の清掃も入る）、進捗は★当日★の予定と出発 | D-159 / 指示書AV 4-1。★意図した違い★ |
| ★レポートの当期で、稼働率が以前より低く出る★（2026-10-05 から） | 稼働率の分子が「レポート行がある回数」から★正常終了（`status = 4`）の回数★に変わった。中断・キャンセル・異常終了・status 6 の清掃は稼働率に入らない（出発成功率には入る）。★施設ステータスの閾値は据え置き★ | 第11回 P-6=A。D-158 項1・項8。指示書AU 段階3 |
| ★レポートで、今週と先週を比べると、同じ施設でも完了率の出し方が「人」で違う★ | 今週（当期）はアプリ、先週（確定期間）は TeamWorks のバッチが計算する。式は同じ（D-155）にし、`task_area = 0` の扱い（P-5=B）・`clean_timetable_histories` を読むこと（P-7=C）も第11回の回答に合わせた（D-158）。★ただし先方のバッチの実挙動を当方は確かめていない★ので、完全に一致する保証は無い | D-155 項1 / D-158 |
| ★施設別パフォーマンスに出ない施設がある★ | ★ロボットが0台の施設は出さない★。確定期間は、その期間のサマリ行がある施設だけ | `lib/db/index.ts:2631-2649` |
| ★連続問題施設には、施設別パフォーマンスに無い施設が出ることがある★ | 連続問題施設は★ロボット0台の施設も出す★（決定5）。施設別パフォーマンスは出さない | `report-mapping.ts:758-760` / `index.ts:2631-2649` |
| ★清掃詳細ログで ✓（出発した）なのに完了率が `0%` で判定が × の行がある★ | `task_area = 0` の清掃（または清掃面積 0 の清掃）。★2026-10-05 から 0% と出る★（旧: `-`）。ダッシュボードでは「失敗」、アラートの出発失敗・要レスキューの未実施には★入らない★ | 第11回 P-3=A・P-5=B（D-158）/ D-156 項1 |
| ★「清掃正常終了」なのに判定が △ や × の行がある / 「清掃中断」なのに ◎ の行がある★ | ★清掃結果（`status`）は判定に使わない★。判定は完了率だけで決める | D-155 項2（ユーザー判断 2026-10-02）。[CLAUDE.md](../CLAUDE.md) §4.4 の「中断で返る＝失敗」は実データを見て見直す |
| ★◎ が多めに出る★ | `clean_area` は同じ場所を何回も清掃した面積の総和なので、1回ごとの面積比が 100 を超えやすく、上限 100 で切られる（ユーザー提供CSV 189行のうち 129行＝68.3%、先方投入の実データ） | D-155 項1（承知のうえで採用）/ 第10回 N-4 |
| ★画面の「90%」が ◎ の行と ○ の行に分かれる★ | 表示は四捨五入、判定は生値。89.5 は表示 90% で ○ | [CLAUDE.md](../CLAUDE.md) §4.3（章5-3） |
| ★エラー種別の内訳の「%」を足すと 100 になり、KPI のエラー発生率と一致しない★ | 内訳の % は★種別ごとの構成比★（全エラー件数に対する割合）。エラー発生率は★エラー件数 ÷ 予定回数★（第11回 P-6=B） | `report-mapping.ts:885-895` / `kpi.ts:182-184` |
| ★アラートの「未解消」が、対応しても減らない行がある★ | ★出発失敗の行は書き込み先が無いので、ずっと「未解消」★に数えられる | D-105 / `lib/db/index.ts:1726-1736` |
| ★ダッシュボードのセキュリティ無効バナーが、サンプルデータでは出るのに実DBでは出ない★ | 実DBの読み取りは通信断しか返さない（セキュリティ無効に対応する列がどのテーブルにも無い） | D-090 項3-9 |
| ★サンプルデータに切り替えると、レポートの数字が「計算されていない」★ | サンプルデータのレポートは `lib/fixtures/reports.ts` の作り値。顧客1以外を選ぶと KPI は空、エラー内訳は施設で絞れない | `lib/db/index.ts:799-844` |

---

## 6. 変えると挙動が変わる数値 — `lib/config/`

**業務ルールの数値は、コードのあちこちに散らばっていません。** `lib/config/` の5ファイル（`index.ts` 812行 / `advice-rules.ts` 133行 / `env-check.ts` 192行 / `allowed-ips.ts` 63行 / `robot-status-match.ts` 46行）に集約されています。[CLAUDE.md](../CLAUDE.md) §7 が「閾値（90/70/10、猶予30分、L2=2日/L3=3日）のハードコード禁止」と定めているためで、判定ロジックも画面も、数値を直接書かずここを読みに行きます。

**見分け方: `export const` で大文字スネークケース（`GRADE_THRESHOLDS` のような名前）のものが設定値です。**

この章では各定数に「**根拠**」の列を付けました。**誰が・いつ・何に基づいて決めた値か**です。読み方は次のとおりです。

| 根拠の書き方 | 意味 | 変えるときに要るもの |
|---|---|---|
| **TeamWorks 合意** / **TeamWorks 回答（第n回）** | TeamWorks（DBとサマリ集計の担当）と合わせた値。サマリ側も同じ値で計算している | ★TeamWorks との合意の取り直し★。片側だけ変えると確定期間（サマリ）と当期（アプリ）で数字の定義が割れる |
| **ユーザー指示（日付）** | daichi（D-Watch の責任者）が決めた値 | ユーザーの確認 |
| **DFA の判断** | 当方（開発側）が理由を付けて決めた値。多くは暫定 | コメントの理由が今も成り立つかの確認 |
| **DB の列の型** | DB の列の長さ・型から機械的に決まる値 | DB の型が変わったら追随 |

★**判定に効く数値（6-1）を変えると、過去の清掃の判定まで変わります**★（昨日 ◎ だった清掃が今日は ○ になる）。画面に保存された判定は無く、毎回計算し直しているからです。

### ★第3版（10/01）からの増減★

| | 2026-09-14（`8f2f29e`） | 2026-10-01（第3版 `d23e203`） | ★2026-10-05（第4版 `a1d7e43`）★ | 10/01 からの増減 |
|---|---|---|---|---|
| `lib/config/index.ts` の `export const` | 29本 | 36本 | ★41本★ | ★+5★ |
| `allowed-ips.ts` | 4本 | 4本 | 4本 | ±0 |
| `robot-status-match.ts` | 2本 | 2本 | 2本 | ±0 |
| `advice-rules.ts` | 1本 | 1本 | 1本 | ±0 |
| `env-check.ts` | 0本（関数3本のみ） | 0本（関数3本のみ） | 0本（★関数4本★） | ±0 |
| **合計** | **36本** | **43本** | ★**48本**★ | ★**+5**★ |

本章は ★`export const` だけを数えています★（`index.ts` の関数 `cleanTaskStatusLabel()` は数えない。★`cleanErrorLabel()` と `CLEAN_ERROR_LABELS` は 2026-10-19 に削除。D-182★）。数え方は `grep -c "^export const" lib/config/index.ts`（＝41）です。

★**10/01 → 10/05 で増えた5本**★（すべて `index.ts`。指示書AS・AT。git で `d23e203` と比較）:

| 増えた定数 | 何のため | 根拠 |
|---|---|---|
| `RUN_COMPLETION_RATE` | 1回ごとの完了率の式の部品（上限100・小数第1位まで） | TeamWorks 回答 第10回 N-3 / D-155 項1 |
| `CLEAN_TASK_STATUS` | 清掃結果（`clean_task_reports.status`）のコード値 | TeamWorks 回答 第10回 N-2 / D-155 項2 |
| `CLEAN_TASK_STATUS_LABELS` | 同・表示名 | 同上 |
| `PRECHECK_NOTE_MAX_LENGTH` | アラートの備考の上限 | 指示書AT §3-6 / D-156 |
| `PATH_ERROR_ID_MAX_LENGTH` | `PATCH /prechecks/{id}/status` の `{id}` の上限 | DB の列の型（`robot_error_reports.error_id` が `varchar(50)`） |

関数 `cleanTaskStatusLabel()` も増えました。`env-check.ts` には関数 `databaseWriteCredentials()` が増えました。★**消えた定数・値が変わった定数はありません**★（`git diff d23e203 HEAD -- lib/config/index.ts` で削除行はコメントだけ）。

---

### 6-1. 判定に効く数値 — `lib/config/index.ts`

| 定数 | 行 | 現在の値 | 何を決めるか | 変えると何が起きるか | 根拠 |
|---|---|---|---|---|---|
| `GRADE_THRESHOLDS` | `:18-25` | excellent 90 / good 70 / poor 10 | ◎○△×の境目。★90.0 は ◎、70.0 は ○、10.0 は △★ | 清掃詳細ログ・アラート・レポート・アドバイス R2/B1・要レスキューが**全部同時に**変わる。唯一の判定者は `judgeGrade()`（`lib/judge/grade.ts`） | 値は TeamWorks 合意（[CLAUDE.md](../CLAUDE.md) §4.1）。★境界を「以上」にしたのはユーザー指示 2026-09-16（D-139。D-020b を破棄）★。× だけは「10%未満」のまま |
| `RUN_COMPLETION_RATE` ★新（10/02）★ | `:36-41` | cap 100 / decimals 1 | 1回ごとの完了率 ＝ `min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て。消費者は `runCompletionRate()`（`lib/judge/kpi.ts:166`）と fixtures だけ | 全画面の完了率と ◎○△× が同時に変わる。★TeamWorks のサマリと同じ式なので、片側だけ変えると前期比（サマリ）と当期（アプリ）が別の式になる★ | TeamWorks 回答 第10回 N-3（「100より大きい値は100、小数点第2位以下切捨て」）。ユーザー判断「一旦B」（D-155 項1）。★**確定**★（第11回 P-2=A「今の式のまま」。D-158） |
| `ESCALATION_THRESHOLDS` | `:43-48` | level2Days 2 / level3Days 3 | 何日連続（スケジュール存在日）で L2 / L3 に上げるか | 警告の出やすさ。アドバイス R3/R4、要レスキュー R-2、連続問題施設の抽出、`CONSECUTIVE_DAYS_DISPLAY.minMax` も連動 | TeamWorks 合意（[CLAUDE.md](../CLAUDE.md) §4.5。Phase A の R1〜R4） |
| `COMPLETION_DROP` | `:64-67` | dropRatio 0.2 | 完了率が「前回」比で何割下がったら「急落」か（要レスキュー R-3）。★差（ポイント）ではなく比★。「前回」は前回のスケジュール存在日 | 要レスキュー R-3 の件数 | ユーザー指示 2026-09-15（「比で」）。指示書AI / D-135 |
| `RESCUE_THRESHOLDS` | `:86-89` | notExecutedDays 3 | 未実施（出発失敗）が何日連続で要レスキューか（R-1） | 要レスキュー R-1 の件数。★`ESCALATION_THRESHOLDS.level3Days` とは別物（たまたま両方3）★ | ユーザー指示 2026-09-15（「出発失敗3日」「未実施3日連続」）。D-135 |
| `CONSECUTIVE_DAYS_DISPLAY` | `:150-157` | defaultMax 30 / minMax ＝ `level3Days`（3）/ maxMax 365 | 連続日数の**表示**上限（超えたら「30日以上」） | ★表示だけ。判定（L1〜L3・R3/R4）は上限前の生値★。既定値は画面から変更でき、保存先は `data/settings/display-limits.json` | ユーザー指示 2026-09-13（指示書AF / D-130）。30・365 の理由はコメント `:133-150`（DFA の判断） |
| `CONSECUTIVE_LOOKBACK` | `:184-189` | calendarDaysPerScheduleDay 7 / plusDays 1 | 連続日数のために表示期間より前を何日さかのぼって読むか（＝表示上限 × 7 ＋ 1 日） | ★`plusDays` を0にすると「30日以上」が二度と出なくなる★。週1回より疎な施設が出ると係数7では足りず、★連続日数が静かに小さく出る★ | DFA の判断（「どの施設も7カレンダー日に1回は清掃予定がある」という前提。指示書AC 段階6 / D-131） |
| `FACILITY_STATUS_THRESHOLDS` | `:553-558` | good 90 / warning 75 | **稼働率**の 良好 / 注意 / 要改善（施設別パフォーマンスのステータス列と色） | ステータス列と絞り込み。★エラー率用ではない★ | DFA の判断（暫定値。D-117 で「DFA 側の UI 判断」と確認。モックは 80/90 だった。D-069 項3） |
| `FACILITY_TABLE_THRESHOLDS` | `:571-574` | errorRateWarning 8 | エラー率のセルを赤くする目安（8.0 は薄字のまま） | ★表の色だけ。判定には一切影響しない★ | モック（`d-watch-mock.jsx:807` の `> 8`）。指示書F |
| `LAUNCH_GRACE_MINUTES` | `:197` | 30 | （名目上）未実施と判定するまでの猶予 | ⚠️ ★**何も起きません**★（下記） | D-025（暫定）。★D-091 項1 で「予定＋猶予30分を当方で計算しない」と決まり、使われなくなった★。★D-173 の結果待ちも使わない（`RESULT_WAIT_MINUTES` は別の定数）★。`f2b91fc` 以降は `:195` |
| ★`RESULT_WAIT_MINUTES`★（★新・10/07★） | `:205`（`a961829`） | 120 | ★結果待ちの長さ（分）★。予定の終了（無ければ開始）からこの分数のあいだは、レポートが無くても「結果待ち」とし、出発失敗に数えない（清掃中も含む。[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)） | ★延ばすと確定した出発失敗が出るまでが遅れる・縮めるとレポートが遅れた清掃が一時的に出発失敗に見える★。詳細ログ・CSV・ダッシュボード・アラートの出発失敗・連続日数・要レスキュー・当期の率が一斉に変わる（消費者は `isResultPending()` だけ）。★影響は直近の清掃に限られる★（待つ時間を過ぎた清掃には効かない）。★`LAUNCH_GRACE_MINUTES` を流用しないこと★ | ★ユーザー判断 2026-10-07（「B」・待つ時間は一旦2時間・設定で変えられるように）。D-173★。★暫定★ — 第12回（★R-2★。`04f974a` で送付版）でレポートの最大の遅れを聞いて見直す。D-091 項1 をこの件に限り上書き |
| ★`DASHBOARD_LATEST_RUN`★（新・10/06） | `:213-219`（`a961829`） | lookbackDays 7 | ダッシュボードの数値カード・表に出す「直近の清掃」を、何日前の予定まで遡るか（★境界を含む★）。★候補は「予定の終了 ≤ 現在」★ | ★どの予定を見せるかだけに効く★。1行の判定（出発・◎○△×・L1〜L3）には効かない。★`LAUNCH_GRACE_MINUTES` を流用しないこと★（意味が違う） | 7日はユーザー指示 2026-10-06（D-159 項3 / 指示書AV D6）。★旧 `settleMinutes`（30。予定の終了＋30分たつまで候補にしない）は D-173 で削除★ — レポートは30分より遅れうる（ユーザー回答 2026-10-07。D-172）ため、終わってからレポートが届くまでは行の旗 `result_pending`（`RESULT_WAIT_MINUTES`）で「結果待ち」と出す |

#### ⚠️ `LAUNCH_GRACE_MINUTES` は★今も死に定数です★（2026-10-05 確認）

`lib/config/index.ts:197` に `30` と書かれていますが、★**この値を import しているコードは1つもありません**★。参照はすべて「ここから読まないこと」という趣旨のコメントだけです（`lib/db/clean-log-mapping.ts:157` / `:292`、`lib/db/index.ts:1172` / `:1674` / `:1723`）。出発したかどうかは「`clean_task_reports` の行があるか」で決め、未来の予定は `start_at` だけで外します（D-091 項1 / D-107）。★この値を変えても挙動は一切変わりません。★ [CLAUDE.md](../CLAUDE.md) §4.4 は猶予30分を「設定値として外出しする」と書いていますが、判定には使っていない、というのが実態です。★D-173（2026-10-07）で「予定の終了＋120分」は計算するようになりましたが、それは別の定数 `RESULT_WAIT_MINUTES`（`lib/config/index.ts:207`）です★（[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)。CLAUDE.md §4.4 にも `a961829` で注記が入った）。

#### ⚠️ 混同すると事故る2組

- ★`GRADE_THRESHOLDS`（判定）と `FACILITY_TABLE_THRESHOLDS`（表の色）は別物★（`lib/config/index.ts:571-574` のコメント）。混ぜると「判定基準を変えたつもりが表の色だけ変わった」が起きます。**エラーは出ません。**
- ★`RESCUE_THRESHOLDS.notExecutedDays`（3）と `ESCALATION_THRESHOLDS.level3Days`（3）は別物★（`:79-82`）。片方を変えたときにもう片方まで動くと、変えた人が意図しない条件まで変わります。

---

### 6-2. 画面・入力・クエリの上限 — `lib/config/index.ts`

| 定数 | 行 | 現在の値 | 何を決めるか | 変えると何が起きるか | 根拠 |
|---|---|---|---|---|---|
| `RESCUE_INITIAL_VISIBLE` | `:119` | 5 | 要レスキューを最初に何件見せるか | ★APIが返す件数の上限ではない★（「もっと表示」で全件）。★APIで切ると、切られたことに運用担当が気づけない★（実データで7件中2件が黙って消えた） | ユーザー指示 2026-09-15（「表示は5件、もっと表示…で全件」）。D-137 |
| `CLEAN_AREA_INITIAL_VISIBLE` | `:823` | 10 | 清掃面積の内訳を最初に何件見せるか | 同上の作法 | DFA の判断（指示書AN。D-137 と同じ作法） |
| `MAX_DATE_RANGE_DAYS` | `:444` | 731 | 清掃詳細ログで指定できる期間の上限（日）。両端を含む日数。★上限であって既定ではない（初期表示は期間を指定しない＝全期間。D-159 追記）★ | 広げるほど1回の問い合わせが重い | ユーザー指示 2026-09-15（「詳細ログは2年分見れるように」。指示書AI 段階7 / D-136）。730 ではなく 731 なのはうるう日のため |
| `MAX_CSV_ROWS` | `:469`（★`f2b91fc` では `:483`★） | 20,000 | CSVエクスポートの行数上限。★ちょうど 20,000 行は出せる★。★清掃詳細ログとレポート（施設別パフォーマンス。D-163）の2つの CSV で共有★ | ★日数ではなく行数で決めている★（同じ2年でも店舗数で桁が違う） | ユーザー指示 2026-09-15（「出力できない期間を分かるように」。指示書AI 段階8 / D-136）。★値そのものは暫定★ — 本番の清掃回数が分からないので据え置き（D-144 訂正。コメント `:439` `:451` の「約25万行」は施設200件前提の古い試算で、D-144 でロボット30台未満に置き直した） |
| `PAGINATION` | `:409-425` | 既定50 / 上限200 / ページボタン7 | 一覧APIの1ページ件数 | `maxVisiblePages` は★奇数かつ7以上★であること（テストで固定） | API契約（`d-watch-api.yaml:1828` の `default: 50` `maximum: 200`）。ページ送りの部品は指示書AA（D-120） |
| `REPORT_YEAR` | `:507-512` | min 2000 / max 2100 | レポートで受け付ける西暦の範囲 | 外すと `year=-1` や `year=1000000` が 500 を出し、★攻撃者が選んだ値がサーバログに載る★ | DFA の判断（2026-09-14 の実測で 500 を確認。理由はコメント `:471-503`） |
| `DB_IN_CLAUSE_MAX_ITEMS` | `:550` | 1,000 | `in` 句に一度に渡す件数（超えたら分割） | 小さいとクエリ回数が増え、大きいとSQL1本が長くなる。★本番で実測して調整してよい★ | DFA の判断（指示書AC §E-5。Prisma の自動分割 65,535 を十分下回る値） |
| `SUCCESS_RATE_TARGET` | `:577` | 95 | ダッシュボードの「目標: 95%」の表示 | ★表示文言だけ。計算には使われない★ | モック（`d-watch-mock.jsx:494` の「目標: 95%」） |
| `DEVICE_NOTE_MAX_LENGTH` | `:590` | 200 | D-S制御盤「備考」の最大文字数 | 入力欄（`DsLogScreen.tsx:507`）とAPI検証が同時に変わる | DFA の判断（暫定。保存先がファイルなので肥大を防ぐ） |
| `PRECHECK_NOTE_MAX_LENGTH` ★新（10/02）★ | `:601` | 400 | アラートの備考（`robot_error_reports.comment` に保存）の最大文字数。★トリム前で数える★ | ★`d-watch-api.yaml:789` の `maxLength: 400`（★`/errors` 側は `:918`★。`f2b91fc`） と一致させること★（片方だけ変えない）。入力欄（`AlertsScreen.tsx:610`）と `lib/prechecks/status-update.ts` の検証が同時に変わる | 指示書AT §3-6（D-156）。`comment` 列は `text` で長さの制約は無いが、画面は1行メモとして表示するため |
| ★`ERROR_LIST_NOTE_REQUIRED_IDS`★ ★新（10/27）★ | `:618` | `[131]` | エラー内容でこの id を選ぶと★備考が必須★（131 = 決定稿の「その他(該当なし)」）。`ErrorListItem.note_required` と PATCH の検証（手順4）が読む | ★id は基本固定だが追加・変更はありうる★ — 変わったらここだけ直す。外すと「その他」を備考なしで保存できる | ユーザー回答④・決定 2026-10-27（D-184 / 指示書BD §4） |
| ★`ERROR_LIST_UNSELECTABLE_ERROR_TYPES`★ ★新（10/27）★ | `:627` | `D-S_CONTROL_FAILURE` / `SECURITY_ON` | ★エラー内容を選べない行の `error_type`★（完全一致）。`ErrorReportItem.error_list_selectable` を決める（`isErrorListSelectable()`）。★清掃開始失敗（`NOT_STARTED`）は入れない — 選べる★ | 足すとその種別の行で選べなくなり（PATCH は422）、外すと制御盤・セキュリティの行でも選べる。★大小文字・空白を緩めない★ | ユーザー判断 2026-10-27（D-184 / 指示書BD §4） |
| `ADVICE_TITLE_MAX_LENGTH` | `:638` | 100 | アドバイス見出しの文字数 | 既定値の実測は最長14文字 | DFA の判断（`DEFAULT_ADVICE_RULES` の実物を数えて余裕を掛けた。2026-09-13。D-124） |
| `ADVICE_TEXT_MAX_LENGTH` | `:640` | 400 | 推定原因の文字数 | 既定値の実測は最長62文字 | 同上 |
| `ADVICE_ACTION_MAX_LENGTH` | `:642` | 100 | 推奨アクション1件の文字数 | 既定値の実測は最長19文字 | 同上 |
| `ADVICE_ACTIONS_MAX_ITEMS` | `:644` | 10 | 推奨アクションの件数 | 既定値の実測は3件 | 同上 |
| `PATH_ID_MAX` | `:674` | `Number.MAX_SAFE_INTEGER` | URL の `{id}` の上限 | ★上限の根拠はDBの列の幅ではなくJavaScript側の桁落ち★ | DFA の判断（JavaScript の数値の限界） |
| `PATH_SERIAL_NUMBER_MAX_LENGTH` | `:691` | 100 | `{serial_number}` の文字数 | ★文字種は絞らない★（形式は TeamWorks の収集処理が決める） | DB の列の型（`controls.serial_number` が `varchar(100)`） |
| `PATH_ERROR_ID_MAX_LENGTH` ★新（10/02）★ | `:703` | 50 | `PATCH /prechecks/{id}/status` の `{id}` の文字数。超えたら 422 | ★文字種は絞らない★ | DB の列の型（`robot_error_reports.error_id` が `varchar(50)`）。指示書AT 段階1 |

---

### 6-3. 対応表 — 数字と名前の翻訳（`lib/config/index.ts`）

| 定数 | 行 | 値 | 何を決めるか | 押さえどころ | 根拠 |
|---|---|---|---|---|---|
| `DB_DATETIME_OFFSET_MINUTES` | `:236` | `9 * 60`（540） | DB の時刻を「JST の壁時計の値」として読むための時差。消費者は `fromDbJst()` / `toDbJst()`（`lib/format/date.ts:72` / `:87`）だけ | —  | TeamWorks 回答 第8回 A2（「JSTです」）。D-115。★DB の時刻は列の型によらず JST の壁時計の値★（[CLAUDE.md](../CLAUDE.md) §3）。DB のドライバが文字列を UTC とみなして返すので、この値を引いて真の時刻に戻している（`lib/format/date.ts:53-63`）。★変えると全画面の時刻が9時間ずれ、深夜の清掃が別の日に動く★。★コメント（`:223-235`）の「`@@system_time_zone = JST`」は第8回の回答で、ステージングの実測は UTC だった★（2026-10-02 に先方が JST に変更。当方の再実測は未了。D-155 項4）。★アプリの値は変えていない★ |
| `PRECHECK_MINUTES_BEFORE` | `:232`（`f2b91fc` では `:244`） | `[60, 30]` | 事前チェックを何分前にやるか（システム管理画面の表示・シード・fixtures の既定値）。★清掃詳細ログの列の割り当てには使わない★（D-178。列は DB の `precheck` の分数の大きい順） | ★2回で固定と確定済み★。この配列を3要素にしても詳細ログの列は増えない（DB に3つ以上あれば開始に近い2つを残し `data.anomaly`。D-178）。★旧: ここに無い分数（10分前など）は詳細ログの列に出なかった（D-169）★。アラート・エラー一覧は生の分数で出す。★予定行が無い回は `null`（画面「-」）★（D-171） | ユーザー回答 2026-09-11（「2回だけ」）。`precheck` が分数そのものになったのは TeamWorks 回答 第7回 A5-2（D-112）。★表示する回を設定値で選べるようにするのは後で★（D-169。2026-10-07 ユーザー判断「このままで問題ない」） |
| `CLEAN_TIMETABLE_PRECHECK` | `:272-275` | main 0 | `clean_timetables.precheck` の `0` が本清掃。それ以外は「何分前か」の数字そのもの | 本清掃の絞り込みに全箇所で使う（`0` を直書きしない） | TeamWorks 回答 第7回 A5-2 / A5-5（D-112） |
| `CLEAN_ERROR`（★旧 `CLEAN_ERROR_LABELS` は 2026-10-19 削除。D-182★） | `:324-335` | 0 正常〜4 清掃ロボット異常 | `clean_status_histories.clean_error` のコード表。★2026-09-30 以降、未実施判定にも清掃結果表示にも使っていない★（取得元が `clean_task_reports` に変わり、そちらに `clean_error` 列が無い）。★アプリのコードで読んでいるのはローカル用シード（`scripts/seed-local-db.ts`）だけ★ | `clean_statuses` 側で今後使う可能性があるため削除していない（指示書AQ §1-2） | 定義書の原文（`docs/handoff/analysis/table-definitions-02-clean-error.md:213`）。D-146 |
| ~~`cleanErrorLabel()`（関数）~~ | — | — | ★2026-10-19 に削除★（本番で未使用だった。指示書BB A14・D-182） | — | D-089 項3 / D-104 項2 |
| `CLEAN_TASK_STATUS` / `CLEAN_TASK_STATUS_LABELS` ★新（10/02）★ | `:348-361` / `:371-378` | 0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了 | ★`clean_task_reports.status`（清掃結果）の表示名★。清掃詳細ログの「清掃結果」列と CSV に出る。★`CLEAN_ERROR` とは別のテーブルの別のコード表★なので互いに読み替えない。★判定には使わない★ | ★原文から1文字も変えないこと★。英語は `lib/i18n.ts` で訳す | TeamWorks 回答 第10回 N-2（`docs/handoff/TeamWorks回答_第10回_20261002.md:63`）。D-155 項2 |
| `cleanTaskStatusLabel()`（関数）★新★ | `:384-386` | — | 表に無い値（CSV 実測の `6` など。意味は未受領）は `String(status)` を返す | ★「その他」「不明」に潰さない★ | D-155 項2。`6` の意味は第11回で照会中（D-157） |
| `ROBOT_STATUS_STANDBY` | `:406` | `"Standby."` | 事前チェックの予定行があり、エラーが記録されていないときに「1h前ST / 30分前ST」へ入れる値。★末尾のピリオド込みが原文★。★予定行そのものが無いときは入れず `null`（D-171）★ | ★`robots.status` を読んで得た値ではない★（エラーが無いことの読み替え） | TeamWorks 回答 第2回（`robots.status` コード表の `'0' => 'Standby.'`）。D-098 / D-065 Q2 |
| `MAKER_NAMES` | `:715-718` | `{1: "PUDU", 2: "Keenon"}` | `devices.maker_id` → メーカー名。未知の番号は数字のまま出す | 番号を変えると全画面の機種名が変わる | ★DFA の暫定決定★（makers テーブルは提供されない＝D-057 A-1。D-085）。実データの `maker_id` を受領したら差し替える |
| `ROBOT_MODELS` | `:738-743` | CC1 のみ `supported: true`（MT1 / C30 / C40 は `false`） | 対応機種。★未対応の機種も画面から消さず「未対応」バッジで出す★ | ★対応機種の追加は `false` → `true` の1文字だけ★ | ユーザー指示 2026-09-09。D-087 |
| `CONTRACT_DIGITS` | `:751-754` | dSecurity 0 / dWatch 1 | `facilities.contract`（10桁の**文字列**）の桁位置。唯一の消費者は `lib/format/contract.ts` | ★数値として比較しないこと★（全施設が「契約なし」になる） | D-080（[CLAUDE.md](../CLAUDE.md) §4.6） |
| `GROUP_ID_NONE` | `:769` | 0 | `customers.group_id` の「グループなし」。★APIには0を漏らさず `null` に変換する★（`lib/format/group-id.ts`） | `=== 0` を他所に散らすと「グループ0」という存在しないホールディングスが出る | TeamWorks の設計（NOT NULL でセンチネル値方式）。D-011 |
| `ROBOT_STATUS_TONES` | `:794-801` | 日本語6値の色分け | ⚠️ 下記 | — | fixtures 由来の暫定値（D-077 項3） |
| `AREA_UNIT` | `:815` | `"㎡"` | 面積の表示単位。★画面やAPIに直書きしないこと★ | 回答が違っていたら直すのはここ1行 | ★ユーザー指示 2026-09-16（「単位は㎡でよいです」）。DFA 側の判断であって TeamWorks の確認ではない★（第9回5章で照会中。D-141） |

#### ⚠️ `ROBOT_STATUS_TONES` は現状ほとんど機能しません

この表は**日本語6文字列の完全一致**でしか色を付けず、載っていない値はすべてグレーになります（`lib/theme-status.ts`）。[CLAUDE.md](../CLAUDE.md) §4.8 で「エラー種別を変換せずDBの生値をそのまま表示する」と確定しているため、実データは `EmergencyStop` のような英語識別子で来ます。★**実データ接続後は色がほぼ全部グレーになりますが、それが期待どおりの挙動です**★。`Standby.` もあえて表に入れていません（`:770-782`）。

---

### 6-4. アドバイスの文言 — `lib/config/advice-rules.ts`

R1〜R4・B1〜B3 の7ルールの**文言**の既定値 `DEFAULT_ADVICE_RULES` がここにあります（`:34-127`）。★発火条件はここではなく `lib/judge/advice.ts` にあります★（章5）。このファイルの `condition` は**画面に説明として出す文字列**であって、動作には影響しません。編集できるのは `title` / `text` / `actions` の3つだけで（`:130-133`）、保存先は `data/settings/advice.json` です。
★ただし編集の窓口 `PATCH /api/settings/advice` を呼ぶ画面は、いまありません★（システム管理画面は表示だけ。[章4](#4-apiの入口--appapi-の23本)）。

根拠: R1〜R4 は TeamWorks 合意（[CLAUDE.md](../CLAUDE.md) §4.5 の Phase A）、B1〜B3 はモックにあったエラー別のアドバイス（`:12`）。

### 6-5. ロボット状態のマッチ値 — `lib/config/robot-status-match.ts`

| 定数 | 行 | 現在の値 | 意味 | 根拠 |
|---|---|---|---|---|
| `B2_HANDLE_UP_STATUSES` | `:35` | `["Handrail Lifted"]` | B2（ハンドルアップ検知）を出す `error_type` の値。⚠️ ★暫定値★。受領済みの `error_type` 24値のリストに含まれておらず、実データでの存在は未確認 | ユーザー指示 2026-09-08（「暫定で変更のおそれはあります」。D-078 項2） |
| `B3_OFFLINE_STATUSES` | `:46` | ★`[]`（空）★ | ⚠️ ★**空なので B3（オフライン検知）は絶対に出ません**★。オフラインを表す値が未判明のため意図的に空 | D-078 項2 / D-077 項3。★推測で値を入れない★（[CLAUDE.md](../CLAUDE.md) §7） |

比較は前後の空白を落とし、大小文字を無視します（`lib/judge/advice.ts`）。**ここに1行足すだけでアドバイスが出るようになります**（同時に `lib/judge/__tests__/advice.test.ts` の該当テストが落ちますが、それは「値を足した合図」です）。

### 6-6. アクセス制限 — `lib/config/allowed-ips.ts`

★**このファイルの値は環境変数ではなくコードです。`.env.local` を直しても変わらず、変更にはデプロイが必要です**★（`ALLOW_DIRECT_ACCESS` だけは例外で環境変数由来）。`index.ts` に入れていないのは、`proxy.ts` に判定ロジックの部品を引きずり込まないためです（`:4-8`）。

| 定数 | 行 | 現在の値（2026-10-05 確認） | 押さえどころ | 根拠 |
|---|---|---|---|---|
| `ALLOWED_IPS` | `:24-26` | ★空（例がコメントで1行あるだけ）★ | ★完全一致のみ★。CIDR・レンジ・ワイルドカードは効かず、`192.0.2.0/24` と書くと★常に拒否★になる。★プライベートIPを書かないこと★ | D-072 / D-077 項5 / D-079 項1 |
| `ALLOW_ALL_WHEN_EMPTY` | `:35` | `true` | 許可リストが空のとき「制限をかけない」。★いまは IP 制限が事実上かかっていない★ | 既定 `true` は開発を止めないため（D-101）。★`false` にするか許可リストを入れるかは、リリース直前のセキュリティ強化の対象として [CLAUDE.md](../CLAUDE.md) §3 に列挙済み★（ユーザー判断 2026-10-01。D-154） |
| `ALLOW_DIRECT_ACCESS` | `:52` | 環境変数由来（未設定＝`false`） | nginx を経由しない直接アクセスを通すか。★`=== "true"` なので、タイポ・`1`・`TRUE` はすべて拒否（安全側）★。`USE_FIXTURES`（`!== "false"`）と向きが逆なのは意図的 | D-101 |
| `TRUSTED_PROXY_HOPS` | `:63` | 1 | `X-Forwarded-For` の★末尾から★何番目を接続元とみなすか。2段構成になったら 2 にするだけで直る | ユーザー回答 2026-09-08（LB と nginx は同一＝1段。D-079 項1） |

### 6-7. 環境変数の見張り — `lib/config/env-check.ts`

★このファイルに `export const` はありません★（関数4本: `checkEnv()` `:43` / `databaseWriteCredentials()` `:162` / `hasError()` `:184` / `formatFinding()` `:189`）。`scripts/check-env.ts`（`npm run check:env`）から呼ばれ、`AUTH_TOKEN` が `changeme` のままでないか、`ALLOW_DIRECT_ACCESS=true` が残っていないか、書き込み用アカウント（`DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD`）が片方だけになっていないか等を調べます。

★`databaseWriteCredentials()`（10/02 に追加。指示書AT §3-2）は `lib/db/prisma.ts` の `getWritePrisma()` も使います★ — 起動前の検査と実際の接続で、判定の基準を1本にそろえてあります。2つとも未設定なら参照用の接続で書き、片方だけなら例外です（[章8](#8-データの出どころ) 8.10）。★エラーメッセージに値そのものを出さない★（`DATABASE_URL` にはパスワードが入り、出力はCIのログに残るため）のが設計上の要点です。


---

## 6-B. 画面ごとのデータの取り出し方 — クエリ層

画面から「施設名で絞って、日付の新しい順で、50件ずつ」と注文が来ます。その注文を**受け取って検品し、並べて、必要なぶんだけ切り出す**のがクエリ層（`lib/<画面>/query.ts`）です。DBそのものには触りません（それは `lib/db/`）。

★**2026-09-14（D-134 時点）からの増減**★

| | 2026-09-14 | 2026-10-01 | 2026-10-05 | 増えたもの |
|---|---|---|---|---|
| `lib/*/query.ts` | 7本 | ★9本★ | 9本 | ★`lib/clean-area/query.ts`（清掃面積。指示書AN）★ / ★`lib/masters/query.ts`（ホールディングス・顧客。指示書AK）★ |
| `lib/api/` | 3本 | ★4本★ | 4本 | ★`master-id.ts`★ |
| `lib/types/index.ts` | 896行 | 1,361行 | ★1,398行★ | 10/05: ★`PrecheckStatusUpdateRequest` / `PrecheckStatusPatch`★（指示書AT） |
| `lib/settings/` | 5本 | 5本 | 5本 | —（旧版が「3ファイル」と書いていたのは誤り） |
| ★`lib/prechecks/status-update.ts`★ | — | — | ★新★ | ★アラートの対応状況・備考の更新規則★（純粋関数。下の差分表と章8-10） |
| ★`lib/prechecks/edit-drafts.ts`★（改訂4） | — | — | ★新（10/06）★ | ★編集モードの下書きと一括保存★（純粋関数。D-165）。★`lib/prechecks/status-cycle.ts` は D-161 で削除★ |
| ★`lib/reports/facility-performance-load.ts`★（改訂4） | — | — | ★新（10/06）★ | ★施設別パフォーマンスの取得〜並べ替え（表と CSV で共有。D-163）★ |

---

### 6B-1. 9本のクエリ層は「同じ形」をしている

まず**全部に共通する形**を1回だけ説明します。各ファイルの説明は、そのあとの差分表だけです。

#### 共通その1 — 「検品の結果」の返し方（`lib/api/query-result.ts`）

9本すべての `parseXxxQuery()` は、同じ型 `QueryParseResult<T>`（`lib/api/query-result.ts:23-25`）を返します。中身は2つの状態のどちらかです。

| 状態 | 中身 | ルート側がすること |
|---|---|---|
| `{ ok: true, query }` | 検品を通った値 | そのままデータ取得へ渡す |
| `{ ok: false, error, status }` | 422 などのエラー本文一式 | そのまま HTTP 応答にする |

★**失敗を `null` で返さないこと**★（`query-result.ts:5-7`）。`null` だと「**どのパラメータが、なぜ**不正か」を呼び出し側に渡せず、ルートは `details: null` の 422 しか組み立てられません。失敗の組み立ては `fail()`（`:32-43`）1本に寄せてあり、`details` は `{ パラメータ名: [理由] }` の形で入ります。

#### 共通その2 — 処理の順番

```
① parseXxxQuery()   注文を検品する（形式・範囲・enum）。不正なら 422
② filterXxx()       条件に合う行だけ残す
③ sortXxx()         並べ替える（同値のときの安定化キーまで決める）
④ paginate / cap    そのページぶんを切り出す／表示上限をかける
```

★**①を飛ばして生の `searchParams` をデータ層に渡さないこと**★（`lib/clean-logs/query.ts:55-57`）。検品を通った値だけがDBまで届く、という1段を必ず挟みます。

#### 共通その3 — 「分類」の絞り込みは1本に寄せてある（`lib/api/master-id.ts`）

`group_id`（ホールディングス）と `customer_id`（顧客）は、9本のうち多くが受け取る共通パラメータです。検証は `parseMasterIdFilters()` / `parseMasterIdParam()`（`lib/api/master-id.ts:45` 以降）の1本だけが知っています。

★**`Number.parseInt()` で書き直さないこと**★（`master-id.ts:36-38`。D-123）。`parseInt("1abc")` は例外も出さずに `1` を返すので、★送った文字列とは違うIDで絞り込みが走ります★。判定は `^\d+$` の全体一致です。`"0"` も 422 にします（0 は「グループなし」を表す予約値であって、有効なIDではないため。D-011）。

**変えると何が起きるか**: この検証を各 `query.ts` に書き写すと、★どれか1本だけ緩い★状態ができても誰も気づけません（`master-id.ts:5-10`）。

#### 9本の差分表

| ファイル | 行数 | 対応する画面 | この1本だけの特徴 |
|---|---|---|---|
| `lib/clean-logs/query.ts` | 242 | 清掃詳細ログ | ★日付の上限（731日）を持つ唯一の本★（`:109-116`）。`parseDate()` / `toDbQueryDate()` は他の本にも貸し出している（`:62` / `:59`）。★`parseDate()` は存在しない日付（2026-02-31）も null（D-182）★。`capCleanLogs()`（`:230`）が連続日数に表示上限をかける |
| `lib/reports/query.ts` | 541 | レポート | ★最大★。週/月/★年★の3粒度と `year` の範囲検証（`:159`）。`select◯◯Rows()` 4本（`:496` `:503` `:510` `:526`）で確定期間のサマリ行を選ぶ |
| `lib/facilities/query.ts` | 201 | 施設ロボットマスタ | 自前でページ切り出しを持つ（`paginateFacilities()` `:190`）。検索は `filterFacilitiesByQ()`（横断）と `filterFacilitiesByFields()`（項目別）の2系統 |
| `lib/schedules/query.ts` | 198 | スケジュール | 同じく自前のページ切り出し（`:191`）。曜日の絞り込みを持つ |
| `lib/prechecks/query.ts` | 212 | アラート（事前チェック） | `summarizePrechecks()`（`:182`）が画面上部の件数バッジを作る。★`applyResolutionChange()`（`:198`）は、1行の対応状況を変えたときに件数バッジを差分だけ直す★（一覧を取り直さないため。指示書AT） |
| ★（参考）`lib/prechecks/status-update.ts`★ | 130 | アラートの対応状況の**更新** | ★クエリ層ではありませんが同じ置き場★。`parsePrecheckStatusUpdateBody()`（`:100`。本文の検品。`status` / `note` 以外のキーは捨てる）と `planPrecheckStatusUpdate()`（`:50`。★DBに書く列だけのパッチを作る唯一の関数★）。詳細は章8-10 |
| ★（参考）`lib/prechecks/edit-drafts.ts`★（★改訂4。D-165★） | 149 | アラート・エラー一覧の★編集モードの下書き★ | ★クエリ層ではありませんが同じ置き場★（純粋関数）。下書きは「行の値と違う項目」だけを持ち、同じ値に戻したら消える（`setDraftStatus()` `:68` / `setDraftNote()` `:80`。★入力の Map を書き換えず新しい Map を返す★）。備考は行の `null` と `""` を同じとみなす。送る本文は★対応状況は必ず・備考は変えたときだけ★（`patchRequestFromDraft()` `:93`）。★`saveDrafts()`（`:121`）は行の並び順に1行ずつ順に送り、1行の失敗で止めない★（成功・失敗・残った下書きを返す）。期待値表は `lib/prechecks/__tests__/edit-drafts.test.ts`（P1〜P8 / D1〜D5 / S1〜S3）。★旧 `lib/prechecks/status-cycle.ts`（巡回順）は D-161 で削除★ |
| ★（参考）`lib/reports/facility-performance-load.ts`★（★改訂4。D-163★） | 85 | レポートの施設別パフォーマンス（表と CSV） | ★DB を読むので純粋層ではありません★。`loadFacilityPerformance()`（`:38`）が「取得 → 絞り込み → 並べ替え → 連続日数の上限処理」まで済ませた★全件★を返し、★`limit` は見ない★。★表のルートと CSV のルートが同じ1本を呼ぶ★（片方だけ直すと表と CSV が食い違う） |
| `lib/ds-devices/query.ts` | 152 | D-Secログ | `summarizeDsDevices()`（`:147`） |
| `lib/ds-alerts/query.ts` | 150 | D-Secアラート | `summarizeDsAlerts()`（`:141`）。種別と対応状況の2軸で絞る |
| ★`lib/clean-area/query.ts`★ | 125 | 清掃面積（新） | ★日付の上限を持たない★（`:14-18`）。1本の集計クエリで返るのは粒度ごとの行数（最大でも施設数）なので、★期間を広げても応答は増えない★。`group_by` は `group` / `customer` / `facility` の3択（`:32-36`） |
| ★`lib/masters/query.ts`★ | 87 | セレクトリストの選択肢 | ★ページングしない唯一の本★（`:8-14`）。既定の50件で切ると★51件目以降の顧客がセレクトから黙って消える★。並びは必ず **id 昇順**（`:60-66`。日本語の名前順はDBとJSで一致せず、`USE_FIXTURES` の切替で順序が変わるため） |

**変えると何が起きるか**: `summarizeXxx()` の件数は「対応状況フィルタの**前**」に数えています。これを一覧の件数（`meta.total`）と取り違えると、ページ送りの総ページ数がずれます（`components/ui/Pagination.tsx` の `total` の注記。D-054）。

---

### 6B-2. 型定義 `lib/types/index.ts`（1,398行）

#### 「型」とは何か

**型とは、データの「形の約束」です。** 「清掃ログ1件には、施設名（文字）と完了率（数字か、未実施なら空）と判定（◎○△×のどれか）が入っている」という**取り決めだけ**を書いた表で、★**実際のデータは1件も入っていません**★。約束に反したコードは、動かす前に機械が止めてくれます。たとえば判定に `"◯"`（別の記号）を入れようとすると、画面を開くまでもなくエラーになります。

★このファイルの正は `d-watch-api.yaml`（API契約）★です（`lib/types/index.ts:4-5`）。YAML を変えたら必ずここも合わせます。

#### 主な型

| 型 | 行 | 何の形か |
|---|---|---|
| `Grade` / `EscalationLevel` | `:17` / `:18` | `"◎"\|"○"\|"△"\|"×"` と `"L1"\|"L2"\|"L3"` |
| `Meta` | `:20-24` | 一覧APIが必ず返す `{ total, page, per_page }` |
| `ApiError` / `ErrorCode` | `:47` / `:31-45` | エラー応答の形と、使ってよいエラー名の一覧 |
| `ListResponse<T>` / `SummaryListResponse<T,S>` | `:84` / `:89` | 一覧の外枠（件数バッジ付きの画面は後者） |
| `CleanLogItem` | `:202-274` | 清掃詳細ログ1行 |
| `PrecheckItem` | `:472-512` | 事前チェック1行 |
| ★`PrecheckStatusUpdateRequest` / `PrecheckStatusPatch`★ | `:521` / `:535` | ★アラートの対応状況の更新★ — 画面からの注文（`status` 必須・`note` 任意）と、★DBに書く4列だけのパッチ★（`status` / `note`→`comment` / `responded_at` / `resolved_at`）。★両者を分けてあるのが要点★（注文のキーがそのままDBに届く経路を作らない） |
| ★`ErrorStatusUpdateRequest` / `ErrorStatusPatch` / `ErrorListItem`★ ★新（10/27）★ | `:766` / `:776` / `:742`（2026-10-27） | ★エラー一覧の更新とエラー内容★ — 上の2つに `error_list_id` を足した注文とパッチ（★5列目を書けるのはこのパッチだけ★）と、選択肢1行（`note_required` 付き。`GET /error-lists`・`ErrorReportItem.error_list`）。D-184 |
| `ReportKpi` / `KpiValue` | `:713` / `:621` | レポートのKPI4指標と、前期比つきの1指標 |
| `FacilityPerformance` / `ContinuousProblem` / `ErrorBreakdownItem` | `:643` / `:668` / `:697` | レポートの3表 |
| `Facility` / `Schedule` / `RobotSummary` | `:981` / `:1021` / `:974` | マスタ系 |
| `DsDevice` / `DsAlert` | `:899` / `:916` | D-Security 系 |
| `RescueRequiredItem` | `:1104` | 要レスキュー1行 |
| `CleanAreaRow` / `CleanAreaTotal` | `:1294` / `:1348` | 清掃面積（新） |
| `XxxDbQuery` 各種 | `:117` ほか | ★画面からの注文ではなく、データ層へ渡す形★。両者を分けてあるのが要点 |

★**`RescueReason` だけは `lib/judge/rescue.ts` から借りています**★（`:11-13`）。理由の語彙を決めているのは判定層なので、★ここに文字列を書き写さない★（写すと片方だけ増えても誰も気づけません）。

**変えると何が起きるか**: 型を緩める（`string` にする等）と、機械の検査をすり抜けた値が画面まで届きます。[CLAUDE.md](../CLAUDE.md) §9.3 が `any` を禁止しているのはこのためです。

---

### 6B-3. API呼び出しの共通部品 `lib/api/`（4ファイル）

| ファイル | 行数 | 役割 |
|---|---|---|
| `client.ts` | 153 | 画面から自アプリのAPIを叩く★唯一の入口★ |
| `query-result.ts` | 43 | 検品結果の共通型（6B-1 参照） |
| `master-id.ts` ★新★ | 107 | `group_id` / `customer_id` の検証（6B-1 参照） |
| `path-params.ts` | 110 | URL の `{id}` / `{serial_number}` / ★`{id}`（文字列の `error_id`。`parsePathErrorId()` `:89`）★の検証 |

#### `apiFetch()`（`lib/api/client.ts:102-153`）

★**画面に `fetch` を直書きせず、必ずここを通します**★（`client.ts:4`）。1箇所に寄せる理由は、認証方式が変わったときに直す場所を1つにするため、そしてエラー形式と `meta` の解釈を共通化するためです。外部URLは受け付けません（`:106-110`）。ビューアーは自アプリのAPIルート経由でしかデータを読まない決まり（D-046）を、コードで守らせています。

失敗したときは `ApiFetchError`（`:28-63`）を投げます。この例外は `status`（HTTPの番号。ネットワーク到達不能は `0`）と `serverMessage`（サーバーが返した文言。読めなければ `null`）を**別々に**持っています。★混ぜない理由★は、HTMLのエラーページが返ったときに `"<!DOCTYPE html>..."` という文字列がそのまま画面に出てしまうからです（`:22-26`）。

**変えると何が起きるか**: `details` の型を「文字列の配列」に狭めると、`GET /reports/kpi` の 422 が返す ★数値の配列★（`{ customer_ids: [1, 2] }`）を扱うコードが★型検査を通ってしまいます★（`:38-43`）。

---

### 6B-4. 設定の保存 `lib/settings/`（5ファイル）

アドバイスの文言と表示上限は**画面から編集できます**。保存先は★メインDBではなく、DFA独自のJSONファイル★です（DBはTeamWorks管理で読み取り専用のため。[CLAUDE.md](../CLAUDE.md) §7）。

#### 二段構え — 既定値はコード、編集結果はファイル

```
既定値（コード）              編集結果（ファイル）            実際に使われる値
lib/config/advice-rules.ts  +  data/settings/advice.json     = loadAdviceRules()
lib/config/index.ts
  CONSECUTIVE_DAYS_DISPLAY  +  data/settings/display-limits.json = loadConsecutiveDaysMax()
```

★**ファイルに入るのは「編集された項目だけ」**★です（`lib/settings/advice.ts:33-35`、`lib/settings/display-limits.ts:26-28`）。未編集のルールはキーごと存在せず、コードの既定値がそのまま使われます。★これにより、既定値を後から改善したときに、編集していないルールへ自動で反映されます★。

| ファイル | 行数 | 役割 |
|---|---|---|
| `json-store.ts` | 234 | ファイルの読み書きの共通部品。★アトミック差し替え★と★壊れたJSONの `.bak` 退避★。★一時ファイルは書き込みごとに別の名前（`<filePath>.<pid>.<uuid>.tmp`。D-182）★。★書き込みはファイルパスごとにプロセス内で直列化（`writeQueues` / `enqueueWrite()`。`c5fe519`。D-182）★ |
| `advice.ts` | 166 | アドバイス文言。既定値に編集内容を重ねる合成だけを持つ |
| `advice-validation.ts` | 112 | 文字数・件数の検証（上限は `lib/config/index.ts:638-644`） |
| `display-limits.ts` | 385 | 連続日数の表示上限。`loadConsecutiveDaysMax()`（`:223`）/ `validateConsecutiveDaysMax()`（`:147`） |
| `ds-device-notes.ts` | 155 | D-S制御盤の備考メモ |

★**保存先 `data/` は git の管理外です**★（`json-store.ts:9-10`）。つまり★バックアップがありません★。だから壊れたファイルは消さずに `.bak` へ退避します。★書き込みはファイルパスごとにプロセス内で直列化します★（`writeQueues` `:106`・`enqueueWrite()` `:108`。`c5fe519`。指示書BB §10 Q6・表12 J1・D-182）— Windows では同じ宛先への rename が同時に走ると `EPERM` になるためです。呼んだ順に1件ずつ書くので★後勝ち★、失敗した書き込みは後続を止めず、★リトライはしません★。★別プロセス間のロックではありません★（本番は Node.js 1プロセス常駐。`:12-17`）。複数プロセスで同じファイルに書く構成にしたらここにファイルロックを入れる想定で、呼び出し側は変えずに済む作りです。★読み → 書きの間の上書き（read-modify-write の後勝ち）は今までどおり防ぎません★。

★**`json-store.ts` は `lib/logger` を import しません**★（`:22-25`）。警告は `onWarn` で外へ渡すだけにしてあります。ログを呼ぶのは呼び出し側（APIルート）の仕事です。

**変えると何が起きるか**: `loadConsecutiveDaysMax()` を使わず `lib/config` の既定値を直接読むと、★管理画面で上限を上げても反映されず、しかもエラーもテスト失敗も出ません★（`lib/db/consecutive-source.ts:395`）。


---

## 7. 見せ方 — 整形・部品・色・言葉

判定が終わった**生の数字**を、人が読める形にして画面に並べるまでの層です。★ここで計算をしないこと★が全体の約束で、丸め・言い換え・単位付けだけを行います。

---

### 7-1. 表示の整形 `lib/format/`（★12ファイル。改訂4 で13★）

★**2026-09-14（D-134 時点）は9ファイル、現在は12ファイル**★（実測。`git ls-tree 8f2f29e lib/format` と現在の比較）。増えた3本は ★`clean-result.ts`★（清掃結果の生値表示）/ ★`group-id.ts`★（グループなしの変換）/ ★`number.ts`★（3桁区切り）です。`consecutive-days.ts` と `search-text.ts` は9/14時点で既に存在していました。

| ファイル | 行 | 入力 → 出力の実例 | 押さえどころ |
|---|---|---|---|
| `date.ts` | `:8` `:20` | `2026-08-27T02:00:00+09:00` → `8/27` / `02:00` | ★タイムゾーン変換をせず文字列から切り出す★（`:2-3`）。`new Date()` を挟むと実行環境のTZで1日ずれる |
| `date.ts`（DB用） | `:42` `:72` `:101` | Prismaの `Date` → `2026-08-27T02:00:00+09:00` | DBの日時は★JSTの壁時計値★。時差は `DB_DATETIME_OFFSET_MINUTES` から読む |
| `date.ts`（DBへ書く用） | `:87` | 真の瞬間の `Date` → DBに書く壁時計値の `Date`（`toDbJst()`） | ★メインDBに日時を書くときは必ずこれを通す★。消費者はシードと ★`lib/db/robot-error-write.ts`（アラートの対応日時2列）★（`:77-80`）。付け忘れはガードテストが検出する |
| `schedule.ts` | `:17` | `"0111110"` → `平日のみ` / `"1010100"` → `月水金` / `"0000000"` → `なし` | ★曜日の起点は日曜★（7桁の1文字目が日曜）。空文字を返さず `なし` と書くのは、空欄だと不具合と区別できないため |
| `schedule.ts` | `:35` `:51` | `"0200"` → `02:00` / ★`"2830"` → `翌04:30`★ | ★終了時刻は24〜29時を取りうる★（日跨ぎ）。範囲外（`"3000"`）は★例外で止める★ |
| `number.ts` ★新★ | `:46` `:61` | `341635.4` → `341,635` / `341635` → `341,635㎡` | ★ロケールを `en-US` に固定★（環境で区切り文字が変わるのを防ぐ）。★`NaN` / `Infinity` は `-`★（`0` と書かない） |
| `contract.ts` | `:26` `:36` | `"1100000000"` → `{d_security_enabled: true, monitoring_enabled: true}` | ★下記 7-3★ |
| `group-id.ts` ★新★ | `:32` | `0` → `null` / `3` → `3` | ★DBの `0` は「ホールディングスなし」の予約値★。APIに `0` を漏らすと★「グループ0」という存在しないホールディングスが画面に出る★ |
| `clean-result.ts` ★新★ | `:61`（★`:98` `cleanLogRowView()` は D-173★） | `4` → `"清掃正常終了"` / `6` → `"6"` / ★`null` → `"出発失敗"`★ / ★結果待ちの行 → 出発 `pending`・`"結果待ち"`・完了率 null・判定なし・開けない★ | ★`clean_task_reports.status` の**表示名**を出す★（2026-10-02 から。第10回 N-2・D-155 項2。それまでは生値）。表は `CLEAN_TASK_STATUS_LABELS`、★表に無い値は数字のまま★。★日本語で返し、画面は `t()` に通す・CSV はそのまま★。★`null` の判定を `\|\|` で書くと `0`（清掃開始前）が「出発失敗」に化ける★（`:62-63`）。★`cleanLogRowView()` は詳細ログの1行の見え方（出発・清掃結果・完了率・判定を出すか・開けるか）を決め、表と CSV が共有する★（片方だけ直すと同じ行が画面では結果待ち・CSV では出発失敗になる。D-173） |
| `raw-label.ts` | `:21` | `null` / `""` / `"  "` → `(値なし)` / `" ABC "` → `ABC` | ★トリム以外の変換をしない★。「その他」「不明」と書かない（破棄済み D-067 と紛らわしいため） |
| `robot-model.ts` | `:26` | `("pudu", "cc1")` → `true` / `("PUDU","CC10")` → `false` | 前後空白と大小文字だけ揃えて★完全一致★。部分一致にすると `CC10` が `CC1` に当たる |
| `address.ts` | `:7` | `("東京都○○区1-2-3", "△△ビル")` → `東京都○○区1-2-3△△ビル` | ★区切り文字を入れない★（日本語住所の慣習） |
| `consecutive-days.ts` | `:32` | `(45, 30)` → `{days:30, capped:true}` / `(30, 30)` → `{days:30, capped:false}` | ★表示上限であって判定の閾値ではない★。★上限ちょうどは `capped:false`★ |
| `search-text.ts` | `:119` `:133` `:176` `:190` | `foldPlain("PUDU-CC1")` → `pudu-cc1` / `foldName("ドン・キホーテ")` → `ドンキホーテ` | ★強さの違う2種類がある★（下記） |
| `period.ts` | `:123` `:155` ほか | `weekOfMonth("2026-08-30")` → `{year:2026, month:8, week:6}` | ★下記 7-2★ |
| `precheck-status.ts` ★新（D-171）★ | `:22` | `null` → `{kind: "none", text: "-"}` / `"Standby."` → `{kind: "pill", text: "Standby."}` | ★清掃詳細ログの「1h前ST / 30分前ST」のセル★。★`-` にするのは `null`（事前チェックの予定が無い）だけ★ — 空文字や値そのものが `"-"` は Pill のまま（DB の値を変換しない。D-077）。★`statusColor()` に `null` を届けないための分岐★。`-` は JP/EN 共通の記号なので `t()` に通さない。CSV は `-` ではなく空のセル（`lib/clean-logs/csv.ts`）。期待値表 `lib/format/__tests__/precheck-status.test.ts`（S1〜S5） |

#### ★検索の照合に2種類ある理由（`search-text.ts`）★

| 強さ | 使うフィールド | すること |
|---|---|---|
| `foldName`（強） | 施設名 / 都道府県 | 大小文字を揃え、さらに★記号（中点・長音・空白・ハイフン類）を除く★ |
| `foldPlain`（弱） | 住所 / シリアル / メーカー / 機種 | ★大小文字だけ★揃える |

施設名だけ強いのは、店名の記号が表記ゆれであって桁位置の意味を持たないからです（`ドン・キホーテ戸塚` を `ドンキ` で探せる）。★逆に住所とシリアルに強い方を使ってはいけません★（`search-text.ts:22-40`）。住所の `東京都○○区1-2-3` と `東京都○○区2-3-4` が両方 `123` / `234` になり、`23` で両方ヒットします。シリアルも `PUDU-CC1-0001` → `PUDUCC10001` となって★桁の境界が消えます★。

**変えると何が起きるか**: 照合規則を画面ごとに書くと、同じシリアルを同じ社内画面で探しても入力欄によって当たったり当たらなかったりします（実際に起きた。`search-text.ts:5-8`）。

---

### 7-2. ★月跨ぎ週のルールを知っている唯一の場所 — `weekOfMonth()`★

週は日曜〜土曜ですが、★**月をまたぐ週は月境界で1日に切り、それぞれの月の別々の週として数えます**★（D-071。TeamWorks のサマリ集計に合わせたもの）。その結果 ★**週の長さは1〜7日で変わり、月の週番号は1〜6になります**★。

```mermaid
gantt
    title 2026年8〜9月 — 月境界で切った週（D-071。週の長さは1〜7日）
    dateFormat YYYY-MM-DD
    axisFormat %m/%d
    section 2026年8月
    第1週 1日（08-01 土のみ）  :crit, 2026-08-01, 1d
    第2週 7日                  :2026-08-02, 7d
    第3週 7日                  :2026-08-09, 7d
    第4週 7日                  :2026-08-16, 7d
    第5週 7日                  :2026-08-23, 7d
    第6週 2日（08-30〜08-31）  :crit, 2026-08-30, 2d
    section 2026年9月
    第1週 5日（09-01〜09-05）  :crit, 2026-09-01, 5d
    第2週 7日                  :2026-09-06, 7d
    第3週 7日                  :2026-09-13, 7d
    第4週 7日                  :2026-09-20, 7d
    第5週 4日（09-27〜09-30）  :crit, 2026-09-27, 4d
```

赤くした4本が★7日でない週★です。カレンダー上は 08-30(日)〜09-05(土) が1つの週ですが、★月末で切って「8月第6週（2日）」と「9月第1週（5日）」の2本になります★。各月の第1週は必ず1日から始まり、最終週は必ず末日で終わります。

| 押さえどころ | 場所 |
|---|---|
| ★帰属ルールを知っているのはこの関数だけ★ | `lib/format/period.ts:123-136` |
| `periodRange()` / `shiftPeriod()` も自前で計算せず、この関数を呼ぶ | `period.ts:27-29` |
| ★第6週は実在する★（2020〜2040 の252か月中55か月。2026年は5月と8月） | `period.ts:31-35` |
| ★7日を前提にした算術を書かないこと★ | `period.ts:17` |
| 年次（`yearly`）はこの関数に関与しない（暦年で締める） | `period.ts:37-42` |

**変えると何が起きるか**: 旧DFA案（D-067。1日を含む週をその月の第1週とし、同じ週を2つの月に分けない）に戻すと、TeamWorks の週次サマリと帰属がズレます。この関数以外の場所で週を計算すると、画面ごとに「第何週か」が食い違います。

---

### 7-3. ★契約フラグ `contract.ts` — いちばん静かに壊れる場所★

`facilities.contract` は ★**10桁の文字列**★であって★**数値ではありません**★（D-080）。1桁目が D-Security、2桁目が D-Watch で、契約済みなら `1` です。

| 値 | 意味 |
|---|---|
| `"0000000000"` | 未設定（既定値） |
| `"1000000000"` | D-Security のみ |
| `"0100000000"` | D-Watch のみ |
| `"1100000000"` | 両方 |

★**数値として比較してはいけません**★（`lib/format/contract.ts:4-6`）。`Number("0000000000")` は★例外を出さずに★ `0` になり、`contract === 3` のような比較は★絶対に真になりません★。結果は ★全施設が「契約なし」になってメニューが全部消える★ ですが、★例外もテスト失敗も出ません★。

判定は文字の完全一致（`contract[i] === "1"`）だけ（`:26-31`）。桁位置は `CONTRACT_DIGITS`（`lib/config/index.ts:751-754`）から読み、ここに直書きしません。桁が足りない文字列・空文字でも例外を投げず、自然に「契約なし」扱いになります（`:23-24`）。

#### ★★新しい注意: `contract` という列は2つあります★★

[CLAUDE.md](../CLAUDE.md) §4.6 に 2026-09-17 の実測として追記された項目です。

| 列 | 場所 | 扱い |
|---|---|---|
| `facilities.contract` | `prisma/schema.prisma:141` | ★これが正★ |
| `clean_schedules.contract` | `prisma/schema.prisma:571` | ★読まないこと★ |

★**型も既定値も完全に同一（`varchar(10)` / `"0000000000"`）なので、取り違えても型エラーになりません**★。用途は未確認で、TeamWorks の定義書にも説明がありません。★現在のアプリは `clean_schedules.contract` を読んでいません（正しい状態）★。

**変えると何が起きるか**: 読む列を取り違えると、メニューの表示制御が別のテーブルの値で決まります。型が同じなので、コンパイルもテストも通ります。

---

### 7-4. 共通UI部品 `components/ui/`（★6個。9/14から増減なし★。改訂4 で関数1本 `date-picker.ts` が増えた）

| 部品 | 行数 | props | 使われ方 |
|---|---|---|---|
| `Pill` | 17 | `text` `color` | 色付きの小さなラベル。背景は色コードに `22`（＝透明度）を足して薄くする（`Pill.tsx:11`） |
| `FilterButton` | 30 | `active` `color` `onClick` `children` | 絞り込みボタン。押されているときだけ枠と文字に色が付く |
| `GradeBadge` | 16 | `grade` | ◎○△× のバッジ。★色は `grade` から自分で決める★（◎○→`ok` / △→`wn` / ×→`ng`。`GradeBadge.tsx:9-10`） |
| `ProgressBar` | 28 | `percent` `tone?` | 進捗バー。★`NaN` / `Infinity` が来ても壊れないよう 0〜100 に丸める★（`:17-19`） |
| `StatCard` | 38（`a961829`） | `value` `label` `sub?` `tone`（`ok` `wn` `ng` `ac` ★`mt`（中立。D-173）★） `onClick?` | KPIカード。★`value` は文字列★（丸めは呼び出し側の責務）。★`color` ではなく `tone` にしてあるのは、生の色コードを直書きさせないため★（`:4-6`） |
| `Pagination` | 130 | `total` `page` `perPage` ほか | ページ送り。★何ページあるか・どの番号を描くかは自分で決めず `lib/pagination/page-items.ts` の純粋関数に任せる★（`:15-19`） |
| ★`date-picker.ts`（部品ではなく関数）★ ★新（10/06。D-162）★ | 19 | — | `openDatePicker(e)`（`:11`）。`<input type="date">` の `onClick` に渡すと、★枠のどこを押してもカレンダーが開く★（`showPicker()` を呼ぶ。無いブラウザでは何もせず、例外は握りつぶす）。期間の入力がある4画面（清掃詳細ログ・アラート・エラー一覧・清掃面積）が使う。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅) |

★`Pagination` だけが `useTheme()` を使います★（`:24-30`）。使うのは★文言の対訳のためだけ★で、色はCSS変数から取ります。★`total` に `summary.total` を渡さないこと★（`:40-46`。`summary` は対応状況フィルタの**前**に数えた件数なので、総ページ数がずれる。D-054）。

**変えると何が起きるか**: 判断（何ページ描くか等）を部品の中に書くと、テストが走りません（`vitest.config.ts` の対象は `lib/**` と `app/**` で、`components/**` は含まれていない）。

---

### 7-5. `.module.css` と `.tsx` の役割分担

6個の部品はすべて `Xxx.tsx` と `Xxx.module.css` の2枚組です（★`date-picker.ts` は見た目を持たない関数なので1枚だけ★）。分け方の規則はこうです。

| どこに書くか | 何を書くか | 例 |
|---|---|---|
| `.module.css` | ★状態によって変わらない見た目★ | 余白・角丸・文字サイズ・枠線の太さ |
| `.tsx` の `style={{...}}` | ★状態によって変わる色★ | 押されているボタンの枠色、判定に応じた背景色 |

★**インライン指定（`.tsx` 側）のほうが強く、CSSの指定を上書きします**★。たとえば `FilterButton.tsx:21-25` は `borderColor` / `background` / `color` の3つをインラインで与えるので、`FilterButton.module.css` に同じ項目を書いても**効きません**。

**変えると何が起きるか**: 「CSSを直したのに色が変わらない」の原因はほぼこれです。色を変えたいときは `.tsx` のインライン指定を見ます。

---

### 7-6. 画面の骨格 `components/layout/AppShell.tsx`（165行）

左のサイドバーと右の本体という2分割の器です。メニューは3つの配列で定義されています。

| 定数 | 行 | 中身 |
|---|---|---|
| `MENU_MONITORING` | `:8-26`（`f02eb1d`） | 清掃詳細ログ / アラート / ★エラー一覧（10/06）★ / レポート / ★清掃面積★ |
| `MENU_DS` | `:25-28` | D-Secログ / D-Secアラート |
| `MENU_SETTINGS` | `:30-34` | 施設ロボットマスタ / スケジュール / システム管理 |

ダッシュボードだけは配列に入れず、見出しの直下に単独で置いています（`:127-131`）。

★**点検記録はコメントアウトで隠してあります**★（`:17-22`）。写真ストアが未着手で画面が成立しないためで（D-079 項3）、★復活は1行のコメントを外すだけ★。画面ファイルと対訳はそのまま残してあります。★スコープから外したわけではありません★（D-031 は変えていない）。

サイドバー右上のボタン2つ（EN/JP と ☀/🌙）は専用の行に固定されています（`:103-124`）。言語を切り替えてもボタンの位置が動かないようにするためです。

---

### 7-7. 色 — ★同じ値が2箇所にある★

| 場所 | 形 | 誰が使うか |
|---|---|---|
| `app/globals.css:1-40` | CSS変数（`--ok` `--ng` など17個） | `.module.css` と、`.tsx` の `var(--ok)` |
| `lib/theme.ts:4-43` | JavaScriptのオブジェクト `THEME_TOKENS`（`dark` / `light`） | `.tsx` が色コードを文字列として必要とするとき |

★**この2つは同じ色を別々に書いてあり、同期は手作業です**★。`--ok: #22c55e`（`globals.css:9`）と `ok: "#22c55e"`（`theme.ts:11`）のように、片方だけ直すと画面の中で色が食い違います。★**どちらかを直したら必ずもう片方も直してください。**★ 名前の対応は `--sf-h` ↔ `sfH` のようにケバブケースとキャメルケースの違いだけです。

#### `lib/theme-status.ts`（29行）— ★完全一致でしか色が付かない★

ロボット状態の文字列から色を引く関数が1つだけあります（`statusColor()` `:24-29`）。対応表は `ROBOT_STATUS_TONES`（`lib/config/index.ts:794-801`）で、★日本語6文字列の完全一致★のみ。★表に無い値はすべてグレー★です。

[CLAUDE.md](../CLAUDE.md) §4.8 で「エラー種別を変換せず生値をそのまま表示する」と確定しているため、実データは `EmergencyStop` のような英語識別子で来ます。★**実データ接続後は色がほぼ全部グレーになりますが、それが現在の期待挙動です**★（`theme-status.ts:10-12`）。実際に出現した値を確認してから表に追記していく運用です。

★`Object.hasOwn()` で先に確かめてから引いています★（`:26`）。`toString` や `__proto__` のような名前が状態として渡ったとき、添字アクセスが★`undefined` ではなくJavaScript内蔵の値を拾ってしまい、グレーにすらならない★ためです。

#### フォント（`app/globals.css:54-58`）

★**日本語フォントを明示的に列挙しています**★。欧文フォント（Geist）は日本語の字を持たないため、指定しないと `sans-serif` に落ち、★閲覧環境に中国語フォントがあると漢字が中華字形で表示されます★。★この行から日本語フォントを外さないでください★（[CLAUDE.md](../CLAUDE.md) §0-3）。

---

### 7-8. 言葉 — ★日本語の辞書は存在しません★

`lib/i18n.ts`（496行。`50a3082`。★`a961829` では455行・表は `:7-450`★）には `EN` という表が1つだけあります（`:7-491`）。その形は次のとおりです。

```
キー（日本語の原文）  →  値（英訳）
"📋 清掃詳細ログ"     →  "Cleaning Logs"
"完了率"              →  "Completion"
```

★**つまり日本語はキー側であり、どこにも辞書がありません**★。各 `.tsx` に日本語がそのまま書かれていて、`t("完了率")` は ★日本語のときは引数をそのまま返す★ だけです（`translate()` `:392-395`。`a961829` では `:452-`）。

#### ★日本語の文言を変える手順★

1. ★`lib/i18n.ts` を直しても日本語は1文字も変わりません★。ここは英語しか持っていません
2. 変えたい日本語が**書かれている `.tsx` を探して直します**（画面固有なら `components/features/<画面>/`、メニューなら `components/layout/AppShell.tsx`）
3. ★同時に `lib/i18n.ts` のキーも同じ文言に直します★。直さないと、★その項目だけ英語に切り替わらなくなります★（キーが一致しなくなり、`translate()` が引数をそのまま返すため）
4. ★例外もテスト失敗も出ません★。英語表示にして目で確かめるしかありません

**変えると何が起きるか**: 手順3を忘れると、EN に切り替えたときその1行だけ日本語のまま残ります。`lib/i18n.ts:49-54` は、まさにこれを避けるために「見出し語をここ1箇所に集めてある」と書いています（TeamWorks の回答が来たら4行差し替えるだけで済むように）。

★**改訂4（10/06〜10/07）で足したキー**★（どれも ★EN は暫定★）: 編集モードの「編集」「キャンセル」「保存（{n}件）」「変更を破棄しますか？」「{n}件を保存できませんでした」「編集中は絞り込みとページ送りを使えません」（D-165。`:438-445` あたり）、アラートの「本清掃は成功」（D-171。`:449`）、「結果待ち」→ "Pending"・「レポートの到着待ち」→ "Waiting for report"（D-173。`:228-229`。詳細ログの清掃結果とダッシュボードのカードで共有）。
★辞書は全画面で共有★なので、D-160 で足した「備考」→ "Note" により ★D-Secログの備考列の見出しも EN で "NOTE"★ になっています（直していない）。清掃詳細ログの「-」（事前チェックの予定が無い）は記号なので辞書に入れていません。


---

## 8. データの出どころ

画面に出ている数字は、どこから来ているのか。D-Watch には**データの置き場所が4種類**あり、それぞれ持ち主が違います。持ち主が違うということは、**勝手に直してよい場所とそうでない場所がある**ということです。

この章は「どのテーブルのどの列を、何のために、なぜ読むか」を書きます。数字1つずつの式と根拠は[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)を見てください。

### 8.1 置き場所の地図

```mermaid
flowchart LR
    subgraph TW["TeamWorks の持ち物（DFA は読むだけ。例外は4列＋error_list_id の更新）"]
        STG[("ステージングDB<br/>MariaDB 10.11<br/>133.125.238.144 / clean_watch<br/>★実データあり★")]
        PRD[("本番DB<br/>★別にある・未接続★")]
    end

    subgraph LOCAL["開発者のパソコン"]
        LDB[("ローカルDB<br/>MySQL 8.4 / dwatch_local<br/>当方のシードで作る")]
    end

    subgraph DFA["DFA の持ち物（本リポジトリ）"]
        FX["lib/fixtures/<br/>サンプルデータ<br/>15ファイル"]
        ST["data/settings/*.json<br/>備考・アドバイス文言・表示上限<br/>★git に入らない★"]
    end

    subgraph GEN["自動生成（手で直さない）"]
        SC["prisma/schema.prisma<br/>952行・39モデル"]
        CL["lib/generated/prisma/<br/>Prisma クライアント"]
    end

    ENV{{"接続先は .env.local の<br/>DATABASE_* で決まる"}}
    SW{"USE_FIXTURES<br/>lib/db/use-fixtures.ts:17"}

    STG -->|"prisma db pull（構造だけ）"| SC
    SC -->|"prisma generate"| CL
    STG --- ENV
    LDB --- ENV
    PRD -.->|"まだつないでいない"| ENV
    ENV --> PS["prismaSource（実DB版）<br/>lib/db/index.ts:706"]
    CL --> PS
    FX --> FS["fixtureSource（サンプル版）<br/>lib/db/index.ts:459"]
    FX -->|"npm run seed:local<br/>★dwatch_local だけ★"| LDB

    PS --> SW
    FS --> SW
    SW -->|"db（22メソッド）"| API["app/api/**/route.ts<br/>23本"]
    ST <-->|"読み書き"| API
    PS -->|"★robot_error_reports の4列（エラー一覧は＋error_list_id）だけ書く★"| STG
    API --> UI["画面<br/>components/features/"]

    classDef tw fill:#fde7e7,stroke:#c0392b,color:#000
    classDef prd fill:#fff,stroke:#c0392b,stroke-dasharray: 5 5,color:#000
    classDef local fill:#eef7ee,stroke:#2e7d32,color:#000
    classDef dfa fill:#e7f0fd,stroke:#2471a3,color:#000
    classDef gen fill:#eee,stroke:#777,color:#000
    classDef sw fill:#fff4d6,stroke:#b9770e,color:#000
    class STG tw
    class PRD prd
    class LDB local
    class FX,ST,API,UI dfa
    class SC,CL gen
    class SW,ENV sw
```

| 置き場所 | 持ち主 | 中身 | 直してよいか |
|---|---|---|---|
| **ステージングDB**（赤） | TeamWorks | ドン・キホーテの実店舗10件＋本社の実データ（2026-09-30 23:25〜10-01 00:09 JST に先方が投入） | ★読むだけ★。例外は `robot_error_reports` の4列の UPDATE（ユーザー指示 2026-10-01。D-152）と、エラー一覧の道の `error_list_id`（D-184。★権限は未確認★）。★スキーマ変更・データ投入は禁止★（[CLAUDE.md](../CLAUDE.md) §2・§7） |
| **本番DB**（赤の点線） | TeamWorks | 未確認（★まだ接続していない★。構成・タイムゾーンも未確認。D-152 3-5） | — |
| **ローカルDB**（緑） | 開発者 | 当方のシード（`scripts/seed-local-db.ts`）が fixtures から作る `dwatch_local`。MySQL 8.4 なので、本番の MariaDB 10.11 と同一ではない（D-100） | 自由。★ただしシードはテーブルを消してから入れ直す★ |
| **サンプルデータ** `lib/fixtures/`（青） | DFA | 境界値・異常系を仕込んだ手書きのデータ | 自由（ただし [docs/fixtures.md](fixtures.md) が正） |
| **設定の保存ファイル** `data/settings/`（青） | DFA | D-S制御盤の備考・アドバイス文言・表示上限 | 画面から保存される。★git に入らないのでバックアップが無い★ |
| **自動生成**（グレー） | 機械 | DB の形を写したもの | ★手で直さない★（D-056） |

**変えると何が起きるか**: ★`scripts/seed-local-db.ts` をステージングに向けると、実データが消えます★（`:848-852` で `deleteMany` してから入れ直すため）。`lib/db/local-guard.ts` が「ローカルホストの `dwatch_local` 以外」を拒否して止めます（`:10-19`。呼び出しは `scripts/seed-local-db.ts:89`）。★このガードを外さないこと★（D-152 3-2）。

### 8.2 ★どのテーブルから何を取るか★

★**2026-09-30（D-151 / 指示書AQ）でデータの取得元が切り替わり、2026-10-02（D-155）で完了率の式が変わりました**★。この表が今の正です。「読んでいる場所」は `lib/db/` の中の `findMany` 等の位置です（2026-10-05、`a1d7e43`）。`include`（関連する表を一緒に読む機能）で読むテーブルは、呼び出し元の位置を書いています。

#### 清掃の予定・結果・エラー（毎日増えるテーブル）

| テーブル | 読む列 | 何に使うか | 読んでいる場所 | 根拠 |
|---|---|---|---|---|
| `clean_timetables`（清掃の**予定**） | `id` / `facility_id` / `section_id` / `parent_id` / `precheck` / `start_at` / ★`end_at`（★結果待ちの判定 `isResultPending()` 用。詳細ログ・アラート・連続日数・当期の率で読む。D-173★）★ | ① ★本清掃（`precheck = 0`）1件が、清掃詳細ログの1行・稼働率の分母1件★ ② 事前チェック行（`precheck` ＝ 60 / 30、`parent_id` ＝ 本清掃の id）→「1h前ST / 30分前ST」とアラート ③ `start_at` が期間・日付の基準（清掃面積を除く） ④ `start_at` が未来の予定は外す | ★2026-10-05 から、すべて `lib/db/timetable-source.ts` の `findTimetables()` / `findTimetablesByChunks()` / `findTimetableById()` を通す★（呼び出し元: `lib/db/index.ts` / `consecutive-source.ts` / `clean-area-source.ts`） | 1回の清掃＝3行（D-092 項1）。`precheck` は「何分前か」の数字そのもの、0 だけが本清掃（TeamWorks 第7回 A5。D-112）。★予定は `clean_timetables` と `clean_timetable_histories` の両方★（[CLAUDE.md](../CLAUDE.md) §4.7 の取得元の表。D-158 項4） |
| `clean_task_reports`（清掃の**結果**） | `clean_timetable_id` / `status` / `clean_area` / `task_area` / `started_at` / `ended_at` / `facility_id`（★`clean_ratio` は読み込むが計算に使わない★） | ① ★行が**あるか**で出発／出発失敗（未実施）★ ② `status` → 清掃結果の表示名（判定には使わない） ③ ★`clean_area` ÷ `task_area`（上限100・小数第1位切り捨て）が1回ごとの完了率★ ④ `clean_area` / `task_area` の合計が清掃面積 ⑤ `ended_at`（空なら `started_at`）が最新の1行を選ぶ ⑥ `started_at` が清掃面積画面の期間の基準 | `lib/db/index.ts:1059` `:1394` `:1572` `:1808` `:2540` `:2816` / `consecutive-source.ts:441` / `clean-area-source.ts:383`（面積の合計）`:389`（完了率・実施回数用の行）`:460`（重複の見張り） | 取得元を一本化（D-151。ユーザー指示 2026-09-30）。★未実施＝行が無いこと★（[CLAUDE.md](../CLAUDE.md) §4.7）。完了率の式は TeamWorks のサマリと同じ（第10回 N-3。D-155 項1。★第11回 P-2=A で確定。D-158★）。`status` の意味は第10回 N-2（D-155 項2）。★`status = 4` は稼働率の分子にも使う（第11回 P-6=A）★ |
| `robot_error_reports`（ロボットの**エラー**） | `error_id` / `clean_timetable_id` / `error_type` / `errored_at` / `created_at` / `serial_number` / `facility_id` / ★`status` / `comment` / `responded_at` / `resolved_at`（この4列は**書く**）★ / ★`error_list_id`（エラー内容。★5列目として**書く**。エラー一覧の PATCH だけ★。D-184）★ | ① 事前チェック行を指すエラー → 「1h前ST / 30分前ST」とアラートの行（`error_type` を変換せずに出す） ② ★本清掃行を指すエラー → エラー発生率の分子・エラー内訳（当期）★ ③ 4列 → アラートの対応状況・備考（読み書き） ④ ★全行（削除済みの施設・清掃エリアの行を除く。U8）→ エラー一覧（`/errors`。10/06〜。4列の読み書きも）★ ⑤ ★`error_list_id` → エラー一覧の「エラー内容」（10/27〜。`error_lists` で名前解決。読むのは `ROBOT_ERROR_LIST_ROW_SELECT` だけ）★ | `lib/db/index.ts:1066` `:1560` `:1755` `:1839` `:1852` `:1880` `:2080` `:2545` `:2821` `:3043` / ★読む列は `lib/db/robot-error-select.ts` の `select` で決める（`robot_id` / `robot` は読まない。指示書AU 段階0-4）★ / ★書き込みは `lib/db/robot-error-write.ts` の `updateMany` の1文（4列 `:195`・5列 `:212`）だけ★ | 値は変換しない（D-077 / [CLAUDE.md](../CLAUDE.md) §4.8）。エラー率の分子は「本清掃行を指すエラー」だけ（TeamWorks 第8回 A7。D-114 項2）。検知日時は `errored_at`、空なら `created_at`（D-105）。4列の書き込みは D-152 / D-156。★`error_list_id` は D-184★（★`prisma/schema.prisma:354` は定義書ベースの手書きで NULL 可（ユーザー回答③「初期は空欄」）。実DDLの NULL 可否と UPDATE 権限は未確認★） |

#### サマリ（確定した期間。TeamWorks のバッチが作る）

| テーブル | 読む列 | 何に使うか | 読んでいる場所 | 根拠 |
|---|---|---|---|---|
| `customer_summaries` | `customer_id` / `year` / `month` / `week` / ★`operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio`★ | 確定期間（先週・先月・昨年）の KPI 4つと、前期比の「前期」 | `lib/db/index.ts:2402` | 確定期間はサマリを読む、詳細から再計算しない（[CLAUDE.md](../CLAUDE.md) §4.7 / D-102）。年次も読む（D-151） |
| `facility_summaries` | `facility_id` / `year` / `month` / `week` / 率4列 | 確定期間の施設別パフォーマンス | `lib/db/index.ts:2657` | 同上 |
| `facility_error_summaries` | `facility_id` / `year` / `month` / `week` / `error_type` / `error_count` | 確定期間のエラー種別内訳（`error_type` ごとに `error_count` を合計） | `lib/db/index.ts:2985` | 同上。種別は変換しない（D-077 / D-088） |

★サマリの粒度は `month` と `week` で見分けます★ — `week = 0` が月次、`month = 0 かつ week = 0` が年次（TeamWorks 承認済み。D-074 項3）。判定は `periodOfSummaryRow()`（`lib/db/report-mapping.ts:100`）だけが知っています。★`week === 0` だけで判定すると年次行が月次に化けて混入し、重複検知で 500 になります★。

★当期（今週・今月・今年）はサマリにまだ行が無い★ので、上の3テーブル（予定・結果・エラー）からその場で集計します（[CLAUDE.md](../CLAUDE.md) §4.7）。

#### マスタ（めったに変わらないテーブル）

| テーブル | 読む列 | 何に使うか | 読んでいる場所 | 根拠 |
|---|---|---|---|---|
| `facilities`（施設） | `id` / `facility_name` / `customer_id` / `prefecture_name` / `addr1` / `addr2` / `phone` / `email` / `manager_name` / `charger_name` / ★`contract`★ / `deleted_at` | 施設名（全画面）、施設マスタ画面の連絡先、施設名・分類 → 施設ID の解決、契約フラグの表示 | `lib/db/index.ts:1712` `:2173` `:2439` / `facility-name-source.ts:67` / `facility-group-source.ts:91` / `clean-area-source.ts:470` ほか各所の `include` | ★`contract` は10桁の**文字列**。1桁目＝D-Security、2桁目＝D-Watch★（D-080。[CLAUDE.md](../CLAUDE.md) §4.6）。★施設名・分類 → 施設ID の解決と清掃面積の施設マスタは `deleted_at` で絞らない★（罠7。解決は行の可視性と別の責務）。★削除済みの施設・清掃エリアの★行★は全画面で出さない（`lib/db/visibility.ts`。D-182）★ |
| `sections`（清掃エリア） | `section_name`（＋ロボットへの橋渡し） | エリア別完了率・D-Secアラートのエリア名。★予定 → 清掃エリア → ロボット★の橋渡し | `include` のみ（`lib/db/index.ts:1001` `:1385` `:1526` `:2196`。D-Secアラートは `select`（`lib/db/robot-error-select.ts:112` の `ROBOT_ERROR_DS_ALERT_SELECT`）） | P1 は1清掃エリア＝1ロボット（[CLAUDE.md](../CLAUDE.md) §8） |
| `robots`（ロボット） | `serial_number` / `device_id` / `deleted_at` | シリアル番号（清掃詳細ログ・アラート・施設マスタ・スケジュール） | `include` のみ（`lib/db/index.ts:795` `:1248` `:1715` `:1768` `:2178`） | ★`robots[0]`（`id` 昇順の1台目）を使う★。★0台でも例外にせずシリアル・機種を `null`（画面は「-」）、1台でなければ `data.anomaly` を清掃エリアごとに1回（D-182。旧: 例外）★ |
| `devices`（機種） | `maker_id` / `model_name` | 機種名（`MAKER_NAMES` で番号→メーカー名） | `include` のみ（`lib/db/index.ts:1007` `:2146`） | makers テーブルは提供されない（D-057 A-1）。番号の割り当ては DFA の暫定（D-085） |
| `customers`（顧客） | `customer_name` / `group_id` / `deleted_at` | 分類の絞り込み、レポート KPI の対象顧客 | `lib/db/index.ts:2123` ほか `include` | `group_id = 0` は「グループなし」（D-011）。★KPI は対象顧客が1社に定まらないと 422★（サマリの率は合算できないため。`lib/reports/target-customer.ts`） |
| `groups`（ホールディングス） | `group_name` / `deleted_at` | 分類の絞り込み | `lib/db/index.ts:2096` | — |
| `clean_schedules`（清掃スケジュール） | `weeks` / `start_time` / `end_time` / `inactive` / `deleted_at` / `facility_id` / `section_id` | スケジュール画面、ダッシュボードのスケジュール進捗の「予定」件数 | `lib/db/index.ts:2191` | `end_time` は時が24〜29を取りうる（翌日。TeamWorks 回答 B-5）。★`inactive` は 0 が有効（論理が逆）★ |
| ★`error_lists`（エラー内容の選択肢）★ ★新（10/27）★ | `id` / `error_category_main_en` / `error_category_main_ja` / `error_category_sub_en` / `error_category_sub_ja` / `error_detail_en` / `error_detail_ja` / `inactive` / `deleted_at`（★`created_at` / `updated_at` は読まない★） | ① ★選択肢★（`GET /error-lists`。★`inactive = 0` かつ `deleted_at IS NULL` だけ・id の昇順★） ② ★名前解決★（エラー一覧の `error_list`。★絞らない★ — 無効化・削除済みでも解決する） ③ PATCH の検証（id が有効か） | ★すべて `lib/db/error-list-source.ts` を通す★（選択肢は `lib/db/index.ts:2158` の `errorLists()`、名前解決は `loadErrorListIndexByIds()` `error-list-source.ts:150`（`id IN` を1回。id が0件なら撃たない）） | ★先方が作成済み・管理側UIなし★（ユーザー回答① 2026-10-27）。★FK が無いのでリレーションは張らない★。★`prisma/schema.prisma:372` の `ErrorList` は定義書ベースの手書き★（ステージングに接続できず db pull できないため。★D-056 の例外★。接続回復後に db pull で置き換えて差分を確かめる）。★Pudu の `error_type` の列は無い★。★中身が決定稿 131行と一致するかは未確認★。★ローカルの `dwatch_local` には元々無い★（`scripts/local-db/` で足す）。D-184 / 指示書BD |

#### D-Security（制御盤）

| テーブル | 読む列 | 何に使うか | 読んでいる場所 | 根拠 |
|---|---|---|---|---|
| `controls`（D-S制御盤） | `serial_number` / `status` / `facility_id` / `deleted_at` | D-Secログの一覧と通信状態（0 正常 / 1 異常）、備考を保存する前の実在確認 | `lib/db/index.ts:1962` `:2058` | 通信状態は2状態（D-065） |
| `control_reports`（制御盤の報告） | `control_id` / `reported_at` | 最終報告日時（制御盤ごとの最大値） | `lib/db/index.ts:1969`（`groupBy`） | ★制御盤1台につき1行（最新の報告だけ）★（TeamWorks 第10回 Q8-3。D-155 項5） |
| `control_error_histories`（制御盤のエラー履歴） | — | ★アプリは読まない（D-180 項8）。シードは `deleteMany` だけ★ | — | 権限上は4列の UPDATE 可（[CLAUDE.md](../CLAUDE.md) §3）だが、D-Sec アラートは `robot_error_reports` に書く |

**変えると何が起きるか**: 取得元を `clean_status_histories` に戻すと、完了率の定義ごと過去に戻ります。清掃面積の完了率用の取り寄せ（`clean-area-source.ts:389-401`）から `clean_timetable_id: { not: null }` と本清掃の絞り込みを外すと、予定外のタスクが完了率・実施回数に混ざり、★他の画面と完了率が食い違います★（例外は出ません）。`periodOfSummaryRow()` から `month` の判定を外すと、レポート画面が年次と月次を混ぜて 500 を返します。

#### この切り替えで押さえるべき5点

1. ★**未実施 ＝ 本清掃に対応する `clean_task_reports` の行が無いこと**★です。`clean_task_reports` には「未実施」を表す列がありません。したがって★「出発失敗」と「未実施」は同じ扱い★です（ユーザー確認 2026-09-30）。判定は `notExecutedFromReport()`（`lib/db/clean-log-mapping.ts:165`）の1箇所だけが知っています。★`clean_task_reports.status`（清掃結果）は判定に使いません★（表示名を出すだけ。D-155 項2。[CLAUDE.md](../CLAUDE.md) §4.4 の「中断で返る＝失敗」は実データを見て見直す）
2. ★**`clean_task_reports.clean_timetable_id` が NULL の行は「予定外のタスク」**★です（ユーザー回答 2026-10-01。D-152 3-3）。★欠損ではなく、実在する清掃★です。予定と突き合わせられないので `in` の条件に一致せず、予定起点の画面には出ません（`lib/db/index.ts:1052-1054`）。★完了率・実施回数には入れず、総清掃面積の面積の合計にだけ入れます★（D-155 項3。TeamWorks のサマリも NULL の行を除く＝第10回 N-3）
3. ★**完了率は「1回ごとの完了率」の単純平均**★です（清掃1回＝1票。D-155。★第11回 P-2=A で確定。D-158★）。1回ごとの完了率は `clean_area` と `task_area` から `runCompletionRate()`（`lib/judge/kpi.ts:166`）が作り、★`clean_task_reports.clean_ratio` 列は使いません★。★`task_area = 0` の行は1回ごとの完了率を 0% にします★（第11回 P-5=B。D-158。旧: null で分子にも分母にも入れなかった）
4. ★**清掃面積の画面は、面積と完了率で読む行が違います**★。面積の合計は `groupBy`（予定外を含む全行）、完了率・実施回数は★行を取り寄せて数えます★（`loadCleanAreaFacilityStatsWithRowCount()` `lib/db/clean-area-source.ts:378`）。1回ごとの完了率（上限100・切り捨て）は集計クエリで書けず、生SQLも使わないためです。★期間を指定しないと全期間を運ぶ★ので、行数と所要時間を `judge.clean_area` ログに出しています（D-156 項2）
5. **期間の基準の列は画面で違いますが、結果は一致します**。清掃詳細ログ・アラート・レポート・ダッシュボードは予定の `clean_timetables.start_at`、清掃面積は `clean_task_reports.started_at` です（`lib/db/index.ts:2337`）。清掃面積が `started_at` を使うのは、予定外のタスクには予定の日時が無いからです。★予定のある清掃では `started_at` に予定の `start_at` と同じ値が入る★（ユーザー回答 2026-10-05「A」）ので、同じ清掃が画面によって別の日に入ることはありません。★旧: 「`started_at` は実際に清掃を始めた時刻」（2026-10-01）は、予定外のタスクについてだけ正しい★

### 8.3 ★読まないテーブル・列と、その理由★

DB には読まない列がたくさんあります。「読み忘れ」ではなく、**読まないと決めている**ものを書きます。

#### 読まない列（★うっかり読むと数字が変わる★）

| テーブル.列 | 理由 | 根拠 |
|---|---|---|
| ★`clean_task_reports.clean_ratio`★ | TeamWorks が「計算式が間違っている。訂正したら回答する」と回答。完了率は `clean_area ÷ task_area` から作る。★`select` には残している★（列に戻すとき `runCompletionRate()` の1関数で済むように。指示書AS D6） | D-155 項1。★第11回 P-1: 訂正後の列も今の式と同じ値・P-2=A: 今の式のまま（D-158）★。列に戻す予定は無い |
| ★`clean_schedules.contract`★ | ★契約フラグは `facilities.contract` だけが正★。型も既定値も `facilities.contract` と同じ（`varchar(10)` / `"0000000000"`）なので、★取り違えても型エラーにならない★。TeamWorks の回答では「事前確認の行を作るかの基準」 | [CLAUDE.md](../CLAUDE.md) §4.6 / D-155 項5（第10回 N-13）。`prisma/schema.prisma:571`（正のほうは `:141`） |
| ★`customers.contract`★ | 削除依頼済み。★現在の `prisma/schema.prisma` にこの列はありません★（`model Customer` `:96-121`。構造） | [CLAUDE.md](../CLAUDE.md) §4.6・§7 |
| `customer_summaries` / `facility_summaries` の `task_area` / `clean_area` | ★実データで `task_area` が全行 1000 固定で、期間内の合計になっていない★。清掃面積は常に `clean_task_reports` から集計する | `lib/db/clean-area-source.ts:35-38`。第9回 Q2-1 で照会中（指示書AN / D-141） |
| `customer_summaries` / `facility_summaries` の `task_count` / `launch_count` / `error_count` | 率4列だけを使う。下の「実施回数の列が無い」件のとおり、件数だけでは率を組み立て直せない | 構造（`prisma/schema.prisma:694-696` / `:728-730`） |
| `robots.status` / `robots.battery` / `robots.clean_ratio` | 「1h前ST / 30分前ST」は `robot_error_reports` から作り、エラーが無ければ `Standby.` を入れる（`robots.status` を読んでいるのではない。★事前チェックの予定行が無ければ `null`。D-171★）。バッテリー残量はいつも空（`battery_level: null`） | D-065 Q2 / D-098。`robots.status` の46値は動作フェーズだけで、ハンドルアップ・オフラインを表さない（D-077 項3） |
| `clean_timetables.inactive` | 無効化された予定でも絞らない（現状の挙動） | `lib/db/index.ts:962-963` のコメント（指示書R 禁止事項4/5） |
| `robot_error_reports.error_level` / `error_detail` | 画面に出していない | — |
| ★`facility_error_summaries.error_list_id`★ ★新（10/29）★ | ★エラー種別内訳は `error_type` で束ねる★ — 同じ `error_type` で `error_list_id` が違う行は足し合わせる（API の出力は変えない）。★`errorBreakdown()` は `select` で使う列（`year` / `month` / `week` / `error_type` / `error_count`）だけ読む★（全列読みだったため、TeamWorks が `error_detail` を消した 2026-10-29 に500になった） | 構造（ステージングの `information_schema`。2026-10-29 監督の実測）。`error_detail`（text）が消え `error_list_id`（bigint unsigned NOT NULL・error_lists への FK）が入った。`customer_error_summaries` も同じ。★`prisma/schema.prisma` は手書きで追従★（db pull 禁止。D-056 の例外） |

#### 読まないテーブル

| テーブル | 理由 | 根拠 |
|---|---|---|
| ★`clean_status_histories`★ | 清掃実績は `clean_task_reports` に一本化した。★アプリのコードに問い合わせは0件★（2026-10-05 確認）。テーブル自体は TeamWorks の管理下なので残る。ステージングでは0行（2026-10-01 実測・先方投入） | D-151（ユーザー指示 2026-09-30）/ [CLAUDE.md](../CLAUDE.md) §4.7 |
| `clean_statuses` | 「今の状態」の表。画面に出していない | — |
| `customer_error_summaries` | エラー内訳は施設別（`facility_error_summaries`）を合計して出す | — |
| `security_statuses` / `security_status_histories` | 「セキュリティ解除のまま」を表す列が無く、セキュリティバナーを作れない | D-090 項3-9（`lib/db/index.ts:2065-2076`） |
| `facility_contacts` / `customer_contacts` | 施設の連絡先は `facilities` の列から読む | — |
| `section_robot_statuses` / `robot_task_versions` / `robot_instruct_histories` / `link_schedules` | 画面に出していない | — |
| `clean_link_schedules` | 不要テーブル（`link_schedules` と同じ構造でデータ0件） | D-058 |
| Laravel の基盤10テーブル（`users` / `sessions` / `jobs` ほか） | TeamWorks 側のフレームワークの表 | — |

#### ★サマリに「実施回数」の列が無く、稼働率を検算できない★

[CLAUDE.md](../CLAUDE.md) §4.7 は「サマリには丸め前の小数と**分子・分母**が格納される（検算可能にするため）」と定めていますが、★実際の DDL と食い違っています★（2026-09-30 実測・構造）。

| KPI | 式（[CLAUDE.md](../CLAUDE.md) §4.2） | サマリにある列 | 検算できるか |
|---|---|---|---|
| 稼働率 | ★正常終了（status 4）の回数 ÷ 清掃予定回数★（第11回 P-6=A） | `operation_ratio`（率）、予定回数 `task_count` | ★できない★ — ★正常終了の回数の列が無い★ |
| 出発成功率 | ★開始できた回数 ÷ 清掃予定回数★（第11回 P-6=A） | `launch_ratio`、`launch_count` | ★できる★ — `launch_count ÷ task_count`（第11回 P-6=A で分母が `task_count` と確定） |
| 清掃完了率 | 1回ごとの完了率の平均 | `clean_ratio`（率） | ★できない★ — 1回ごとの完了率の合計も件数も無い |
| エラー発生率 | ★エラー発生回数 ÷ 清掃予定回数★（第11回 P-6=B） | `error_ratio`、`error_count` | ★できる★ — `error_count ÷ task_count` |

（構造: `prisma/schema.prisma:694-710` / `:728-744`）

★帰結★:

- ★確定期間の稼働率は TeamWorks が出した `operation_ratio` をそのまま信じるしかありません★。式は「正常終了（status 4）の回数 ÷ 予定回数」と回答がありました（第11回 P-6=A。D-158）が、分子の列がサマリに無いので検算はできません
- ★複数の顧客を合算した KPI は出せません★ — 率は加重平均できないためです。だから KPI は、対象顧客が1社に定まらないと 422 を返します（`lib/reports/target-customer.ts:1-15`）。★率の平均を取ってはいけません★（施設数の違いが消える）
- ★当期（アプリがその場で集計する分）は、2026-10-05 からサマリと同じ式です★（稼働率 ＝ 正常終了 ÷ 予定、出発成功率 ＝ 開始 ÷ 予定、エラー発生率 ＝ エラー ÷ 予定。指示書AU 段階3。D-158）。旧: 稼働率と出発成功率に同じ「出発した回数」を分子に使っていた

### 8.4 接続先について

★**接続先 `133.125.238.144` / `clean_watch` は★ステージング★です。本番DBは別にあります**★（D-152。2026-10-01 ユーザー回答「これはステージングです、本番は他にあります」）。
2026-09-17 の D-148（「本番DBそのもの」）は★破棄★されました。過去の文書の「ステージング」「テスト環境」という呼称のほうが正しかったことになります。

| 項目 | 現状 | 根拠 |
|---|---|---|
| 中身 | ★実データ★。施設はドン・キホーテの実店舗10件＋本社。★当方のダミーは消えた★ | 2026-10-01 実測・先方投入（[CLAUDE.md](../CLAUDE.md) §3） |
| 予定・実績の行数 | ★`clean_timetables` / `clean_schedules` / `clean_status_histories` は0行、`clean_task_reports` は2行（どちらも `clean_timetable_id` が NULL＝予定外、status 4）★。★予定起点の画面（清掃詳細ログ・ダッシュボード・アラート）は空★で、清掃面積だけ2回と出る | 2026-10-01 実測・先方投入。TeamWorks は「0行のテーブルには 10/5 中に入る」と回答（第10回 N-6。D-155 項5）。★入ったかは未確認★ |
| データの入れ替え | ありうる（時期未定） | 第10回 N-6（D-155 項5） |
| ★書き込み★ | ★`robot_error_reports` の4列（`status` / `comment` / `responded_at` / `resolved_at`）と、エラー一覧の PATCH だけが書く5列目の `error_list_id`（D-184）★。★実データ（ドン・キホーテのエラー行）の対応状況が書き換わることを承知のうえ★ | ユーザー指示 2026-10-01「書き込んでよい」（D-152）。★それ以外の列は引き続き禁止★（[CLAUDE.md](../CLAUDE.md) §7。権限も無い）。★`error_list_id` の UPDATE 権限は「あるはず」（ユーザー回答② 2026-10-27）だが★未確認★ — 接続回復後に `SHOW GRANTS` で確かめる（指示書BD §8）。★ステージングに接続できない（1045）ので、実DBへの書き込みは未検証★ |
| `control_error_histories` の4列 | 権限上は許されているが、★書くコードはまだ無い★。★D-Sec アラートの対応状況は `robot_error_reports` に書く（D-180）ので、今後も書く予定は無い★ | [CLAUDE.md](../CLAUDE.md) §3 |
| 再実測 | ★2026-10-02 以降できていない★（10-02 は認証エラー、10-05 時点もログインできない件を別途問い合わせ中） | D-155 項4 / D-157 |
| セキュリティ（接続元の制限・TLS・パスワード変更） | ★リリース直前にまとめて行う★（検証段階のため保留）。対象は [CLAUDE.md](../CLAUDE.md) §3 に列挙済み | ユーザー判断 2026-10-01（D-154） |
| 構築・保守 | TeamWorks（サーバーの契約と費用は DFA） | D-143 |

接続情報（ホスト名・パスワード）は `.env.local` にあり、**このドキュメントには書きません**（★この文書を書くときも `.env.local` は開いていません★）。`lib/db/prisma.ts:36-39` が「実在のDBユーザー名をコードに書かない。未設定なら黙って既定値で繋ぎにいかず、何が足りないかを言って落とす」と定めています。

★**MariaDB と MySQL は同一ではありません**★（D-100）。本番は MariaDB 10.11、ローカルは MySQL 8.4 なので、ローカルで通っても本番で通らない可能性があります。★アプリのコードに生SQL（`$queryRaw` / `$executeRaw`）は1件もありません★（すべて Prisma のクエリビルダ経由。[CLAUDE.md](../CLAUDE.md) §3）。MariaDB 特有の書き方を使いたいときは TeamWorks に知らせる約束です。

### 8.5 `USE_FIXTURES` — サンプルと実DBの切り替え

判定はたった1行です。

```ts
export const USE_FIXTURES = process.env.USE_FIXTURES !== "false";
```
（`lib/db/use-fixtures.ts:17`）

| 項目 | 現状 |
|---|---|
| 既定値 | ★`true`（サンプルデータ）★ — 「`"false"` **以外**はすべて true」なので、**未設定でもサンプルになります** |
| `.env.example` の記載 | `USE_FIXTURES=true`（`.env.example:2`。新しい開発者はDB無しで始められる） |
| `.env.local` の現在の設定 | ★未確認★（この文書を書くために `.env.local` は開いていません） |
| `false` のときの接続先 | `.env.local` の `DATABASE_HOST` / `DATABASE_USER` / `DATABASE_PASSWORD` / `DATABASE_NAME`（または `DATABASE_URL`）で決まる（`lib/db/prisma.ts:41-71`）。ステージングにもローカルの `dwatch_local` にも向けられる |

**★切り替わらない書き方の罠★**

- ★`USE_FIXTURES=FALSE`（大文字）は効きません★。比較は `!== "false"` の**完全一致**なので、大文字・前後の空白・`0`・`no` はすべて「true（サンプル）」になります。**例外もエラーも出ず、実DBに繋いだつもりでサンプルが出ます**
- ★`lib/db/index.ts` から直接 `USE_FIXTURES` を import しないでください★。この定数がわざわざ独立ファイルに切り出されているのは、`lib/logger` がこの値を読むために `@/lib/db` を import していた結果、**Edge Runtime のバンドルに Prisma クライアントまで引きずり込まれ、`USE_FIXTURES=false` で `/api/reports/*` が軒並み404になった**実測事故があるためです（`lib/db/use-fixtures.ts:4-15`）
- ★`db` の分岐はモジュール読み込み時に1回だけ決まります★（`lib/db/index.ts:3070`）。`.env.local` を書き換えたら **dev サーバを再起動**してください

**設計上の決めごと**: ★`USE_FIXTURES` の true / false で件数や数字が変わってはいけません★。これは指示書AC 禁止事項6 として、★サンプル版の絞り込み5本すべての冒頭★（`lib/db/fixture-clean-logs.ts` ほか）に書かれています。判定ロジック（`lib/judge/`）は切り替えの**外側**にあるので、サンプル開発中も本番と同じ判定が走ります（D-044 / [CLAUDE.md](../CLAUDE.md) §6-3）。
★書き込みも同じです★ — アラートの対応状況の更新は `USE_FIXTURES=true` でも動き、サンプル版は★書き込みをメモリ上の上書き表に持ちます★（`lib/db/fixture-precheck-overrides.ts`。★プロセスを再起動すると消える★）。

### 8.6 `lib/db/` の全ファイル（24本。★改訂3 時点 26本★）

2026-09-14 時点は13本、2026-10-01（第3版）は20本、第4版（2026-10-05 `a1d7e43`）は22本でした。★**現在は24本**（ほかに `__tests__/`）。指示書AU（2026-10-05）で ★`timetable-source.ts`★ と ★`robot-error-select.ts`★ が増えました（行数は 2026-10-06 の `wc -l`）★。第3版から増えたのは ★`robot-error-write.ts`★ と ★`fixture-precheck-overrides.ts`★（どちらも指示書AT。2026-10-02）。行数は 2026-10-05 の `wc -l` です。
★改訂3（指示書AW。2026-10-06）で ★`error-report-mapping.ts`★ と ★`fixture-error-report-overrides.ts`★ が増えて26本です★。
★改訂9（指示書BD。2026-10-27）で ★`error-list-source.ts`★ が増えました★（2026-10-27 の作業ツリーで `lib/db/*.ts` は29本。★表に無い `ds-alert-mapping.ts`・`visibility.ts` は改訂5・改訂7 で増えたもの★で、この改訂では足していません）。改訂9 で変わったファイルの行数は 2026-10-27 の `wc -l` です。

| ファイル | 行 | 役割 |
|---|---|---|
| `index.ts` | 2,591 | ★中心★。`DataSource` の型（`:192`）、サンプル版（`:459`）、Prisma版（`:706`）、`db` の切り替え（`:2591`） |
| `use-fixtures.ts` | 17 | `USE_FIXTURES` の判定だけ。Edge Runtime 対策で独立（8.5） |
| `prisma.ts` | 137 | Prisma クライアントのシングルトン。参照用 `getPrisma()`（`:75`）と★書き込み用 `getWritePrisma()`（`:100`）★。★`index.ts` の先頭で静的に import しない★（未生成の環境でテストが全滅する。`:4-8`） |
| **読み取りの切り出し** | | |
| `consecutive-source.ts` | 463 | ★連続日数の材料★を作る唯一の読み取り `loadConsecutiveInputs()`（`:388`）。`cleanLogs` / `facilityPerformance` / `continuousProblems` / `rescueRequired` の**4経路が共有**（D-109 の再発防止）。表示期間の前を「表示上限 × 7 ＋ 1 日」遡る。★`ConsecutiveDbRange.facility_ids`（必須。null=絞らない/[]=0行）で施設を絞る・削除済みの施設・清掃エリアの行は入れない（D-182）★ |
| `clean-area-source.ts` | 513 | 清掃面積の材料。★面積は `groupBy`、完了率・実施回数は行の取り寄せ★（`loadCleanAreaFacilityStatsWithRowCount()` `:378`、数える関数 `cleanAreaStatsFromRows()` `:287`）。重複の見張り `countDuplicatedTimetables()` も |
| `facility-name-source.ts` | 68 | ★施設名 → 施設ID の解決★用。`deleted_at` で**絞らない** |
| `facility-group-source.ts` | 98 | ★分類（ホールディングス／顧客）→ 施設ID の解決★用 |
| ★`timetable-source.ts`★ ★新（10/05）★ | 295 | ★予定を `clean_timetables` と `clean_timetable_histories` の両方から読む唯一の読み口★（`findTimetables()` / `findTimetablesByChunks()` / `findTimetableById()`）。同じ `where` を2表に渡し、同じ id は本表を採る。中身が違えば warn `db.timetable_conflict`。★`count` / `groupBy` を2表にまたがって書かない★（同じ id の重複を引けない）。指示書AU 段階3b / D-158 項4 |
| ★`robot-error-select.ts`★ ★新（10/05）★ | 126 | ★`robot_error_reports` を読むときの `select` の定数★（`ROBOT_ERROR_COUNT_SELECT` ほか）。★`robot_id` / `robot` を読まない★（先方の DDL では NULL 可・Prisma では NOT NULL のため。第11回 P-8 b）。指示書AU 段階0-4。★改訂3 でエラー一覧の ★`ROBOT_ERROR_LIST_KEY_SELECT`（`:72`。1段目のキーだけ）★ と ★`ROBOT_ERROR_LIST_ROW_SELECT`（`:90`。2段目・PATCH の前後）★ を足した（99行）★。★改訂9 で `ROBOT_ERROR_LIST_ROW_SELECT`（`:91`）に `error_list_id`（`:107`）を足した — ★`error_list_id` を読むのはこの定数だけ★（129行。D-184）★ |
| ★`error-list-source.ts`★ ★新（10/27）★ | 160 | ★エラー内容（`error_lists`）の読み取りと名前解決★。★用途で規則が違う★: 選択肢は ★有効な行だけ・id の昇順★（`activeErrorListItems()` `:87` が正。Prisma の `ACTIVE_ERROR_LIST_WHERE`（`:58`）は写し）、名前解決は ★絞らない★（`errorListIndex()` `:101` / `resolveErrorList()` `:118`。★`loadErrorListIndexByIds()`（`:150`）に `inactive` / `deleted_at` の条件を足さないこと★ — 足すと無効化した選択肢を選んだ行が「不明」に化ける）。★行の `error_list_id` を `errorListIdsOf()`（`:106`）で集めて `id IN` を1回だけ★（N+1 にしない）。★選べる行かの判定 `isErrorListSelectable()`（`:129`。`ERROR_LIST_UNSELECTABLE_ERROR_TYPES` との完全一致）★もここ。指示書BD §5-3 / D-184 |
| **★書き込み（メインDB）★** | | |
| ★`robot-error-write.ts`★ ★新（10/02）★ | ★227★ | ★`robot_error_reports` を UPDATE する唯一のファイル★。★経路ごとに許す列が違う★: 事前チェックは ★4列★ `writeRobotErrorColumns()`（`:189`。許す列は `ROBOT_ERROR_WRITABLE_COLUMNS` `:38`）、★エラー一覧は5列（4列＋`error_list_id`）★ `writeErrorReportColumns()`（`:206`。許す列は `ERROR_REPORT_WRITABLE_COLUMNS` `:52`。改訂9・D-184）。8.10 |
| **変換（DBに触らない関数）** | | |
| `clean-log-mapping.ts` | 489 | 清掃ログ1件を組み立てる変換。`notExecutedFromReport()`（`:165`）/ `completionRateFromReport()`（`:203`。★`runCompletionRate()` を呼ぶ★）/ `latestReportByTimetableId()`（`:241`）/ `isFutureSchedule()`（`:308`）/ 連続日数 `computeConsecutiveProblemDays()`（`:463`） |
| `report-mapping.ts` | 971 | レポート4メソッドの変換。★`periodOfSummaryRow()`（`:100`）★、サマリの率 → KPI `kpiFromSummaryRatios()`（`:231`）、当期の集計 `aggregateCleanStatsByFacility()`（`:460`） |
| `precheck-mapping.ts` | 238（★`f2b91fc` で287★） | アラートの変換。★`robot_error_reports.status` の 0/1/2 と 未解消/対応中/解消 の対応★（`:38` と逆向き `:58`。`f2b91fc`）、「1h前ST / 30分前ST」の割り当て `statusColumnsFromPrecheckRows()`（`:187`。★予定行が無い列は `null`。D-171★）、★親の本清掃が成功したか `mainRunSucceededFromReport()`（`:339`。D-171）★ |
| ★`error-report-mapping.ts`★ ★新（10/06）★ | ★234★ | ★エラー一覧の変換★。`errorReportKeyFromRow()`（★`:112`★。1段目のキー）/ `errorScheduleLink()`（★`:141`★。紐づく予定の4区分）/ `errorReportItemFromRow()`（★`:175`★。1行。★改訂9 で引数に `error_lists` の索引を足し、`error_list_id` / `error_list` / `error_list_selectable` を組み立てる★）/ `errorReportListWhere()`（★`:222`★。SQL の `where`。★正は `lib/error-reports/query.ts` の `filterSortErrorReportKeys()`★）。行番号は 2026-10-27 の作業ツリー |
| `rescue-mapping.ts` | 198 | 要レスキュー店舗の組み立て |
| `ds-status.ts` | ★35★ | D-Sec のコード値変換（`controls.status` 0/1 だけ。制御盤エラーの変換2関数は削除。D-180 項8）。想定外の値は例外（★一覧に出すかは `isListedControlStatus()` で先に外す。D-182★） |
| `date-range.ts` | 68 | JST の `YYYY-MM-DD` を「DB の壁時計値」の範囲に変換 `dbDateRange()`（`:50`）。★ここ1箇所に閉じ込める★（9時間ずれの事故対策） |
| `chunked.ts` | 33 | `in: [...]` に渡す配列の分割（`DB_IN_CLAUSE_MAX_ITEMS` ごと） |
| **サンプル版の絞り込み（5本）** | | |
| `fixture-clean-logs.ts` | 46 | サンプル版 `cleanLogs()` |
| `fixture-prechecks.ts` | 47 | サンプル版 `prechecks()` |
| `fixture-area-completions.ts` | 33 | サンプル版 `areaCompletions()` |
| `fixture-clean-area.ts` | 103 | サンプル版 `cleanArea()`（★完了率は `runCompletionRate()` を通す★） |
| `fixture-reports.ts` | 117 | サンプル版レポート4メソッド |
| **サンプル版の書き込み** | | |
| ★`fixture-precheck-overrides.ts`★ ★新（10/02）★ | 69 | ★`lib/fixtures/prechecks.ts` は書き換えず、id → 4項目の上書き表をメモリに持つ★（`:29`）。Prisma 版と同じ歯止め（`robotErrorColumnsFromPatch()`）を通す |
| ★`fixture-error-report-overrides.ts`★ ★新（10/06）★ | ★62★ | ★エラー一覧のサンプル版の上書き表★。★改訂9 から `robotErrorColumnsFromErrorPatch()`（5列）★で DB の列に写してから重ねる（Prisma 版と同じ歯止め・同じ値。★`error_list_id` も写す — 写し忘れると PATCH が 200 なのに GET に出ない★）。★`fixture-precheck-overrides.ts` とは状態を共有しない★（id が重ならない） |
| **その他** | | |
| `local-guard.ts` | 31 | シードがローカルの `dwatch_local` 以外に書き込むのを止める（8.1） |

**変えると何が起きるか**: `consecutive-source.ts` を経由せずに連続日数の入力を自前で引くと、4経路の遡り幅が食い違い、「同じ施設・同じ日なのに清掃詳細ログとレポートで連続日数が違う」という不具合（D-109 / D-103）が復活します。★L3 が L1 に化けますが、例外もテスト失敗も出ません★。

★**古いコメントが2箇所残っています**★（2026-10-05 確認。どちらも動作には影響しません）:

| 場所 | 古い記述 | 今の正 |
|---|---|---|
| `lib/db/clean-area-source.ts:440` | 「`clean_timetable_id` は TeamWorks がまだ埋めていない（今後埋める前提）」 | ★NULL は予定外のタスクで、後で埋まるものではない★（2026-10-01 ユーザー回答。D-152 3-3 / [CLAUDE.md](../CLAUDE.md) §4.7）。NULL を除く条件そのものは正しい |
| `lib/db/index.ts:744-745` | 「`precheck = 1` … 1回目の事前チェック（60分前）/ `precheck = 2` … 2回目（30分前）」 | ★`precheck` は「何分前か」の数字そのもの（60 / 30）★（TeamWorks 第7回 A5。D-112）。コードは生の分数として正しく読んでいる（詳細ログの列は D-178 から分数の大きい順に割り当て、`PRECHECK_MINUTES_BEFORE` は使わない） |

### 8.7 `DataSource` の22メソッド（★改訂2 時点 23・改訂3 時点 26・改訂9 時点 27★）

★**22本です**★（2026-09-14 時点は15本、2026-10-01 時点は20本）。インターフェース（`lib/db/index.ts:231-533`）・サンプル版（`:579-857`）・Prisma版（`:944-3068`）の3箇所すべてで 22 本と数えました。★10/02 に増えた2本（`precheckItemById` / `updatePrecheckErrorColumns`）はアラートの対応状況の更新用★です。

| # | メソッド | 戻り値 | 主に読む（書く）テーブル | サンプル版 | Prisma版 |
|---|---|---|---|---|---|
| 1 | `cleanLogs(query)` | 清掃詳細ログ一覧 | `clean_timetables` `clean_task_reports` `robot_error_reports` | `:585` | `:990` |
| 2 | `facilityNameIndex()` | 施設名→IDの解決用 | `facilities` | `:609` | `:1296` |
| 3 | `facilityGroupIndex()` | 分類→IDの解決用 | `facilities` `customers` `groups` | `:626` | `:1309` |
| 4 | `areaCompletions(query)` | エリア別完了率 | `clean_timetables` `sections` `clean_task_reports` | `:639` | `:1358` |
| 5 | `prechecks(query)` | アラート一覧 | `clean_timetables` `robot_error_reports` `clean_task_reports` | `:646` | `:1491` |
| 6 | ★`precheckItemById(id)`★ ★新★ | アラート1行（★書き込みの前後の確認用。事前チェック異常の行だけ。出発失敗の id は `null`★） | `robot_error_reports` `clean_timetables` | `:654` | `:1750` |
| 7 | ★`updatePrecheckErrorColumns(id, patch)`★ ★新★ | ★`robot_error_reports` の4列を更新★（`{ matched }`） | ★`robot_error_reports`（書く）★ | `:658` | `:1815`（★書き込み用の接続★） |
| 8 | `dsDevices()` | D-S制御盤一覧 | `controls` `control_reports` | `:691` | `:1956` |
| 9 | `dsDeviceExists(serial)` | 備考保存前の実在確認 | `controls` | `:700` | `:2055` |
| 10 | `dsAlerts()` | D-Secアラート | `robot_error_reports`（`D-S_CONTROL_FAILURE`。宣言 `:386`） | ★`:703`★ | ★`:2075`★ |
| 11 | `groups()` | ホールディングス | `groups` | `:713` | `:2093` |
| 12 | `customers()` | 顧客 | `customers` | `:724` | `:2120` |
| 13 | `facilities()` | 施設マスタ | `facilities` `robots` `devices` `customers` `groups` | `:727` | `:2135` |
| 14 | `schedules()` | スケジュール | `clean_schedules` | `:730` | `:2188` |
| 15 | `inspections()` | 点検記録一覧 | （メインDBには置かない） | `:733` | ★`:2227` 未実装★ |
| 16 | `inspectionDetail(id)` | 点検記録の詳細 | （同上） | `:736` | ★`:2230` 未実装★ |
| 17 | `rescueRequired(query)` | 要レスキュー店舗 | `clean_timetables` `clean_task_reports` `facilities` | `:757` | `:2254` |
| 18 | `cleanArea(query)` | 清掃面積の集計材料 | `clean_task_reports` `clean_timetables` `facilities` | `:793` | `:2324` |
| 19 | `reportKpis(query)` | KPI4指標 | `customer_summaries` ＋（当期）詳細3テーブル | `:821` | `:2385` |
| 20 | `facilityPerformance(query)` | 施設別パフォーマンス | `facility_summaries` ＋（当期）詳細3テーブル | `:825` | `:2608` |
| 21 | `continuousProblems(query)` | 連続問題施設 | `clean_timetables` `clean_task_reports` `facilities` | `:829` | `:2885` |
| 22 | `errorBreakdown(query)` | エラー種別内訳 | `facility_error_summaries` ＋（当期）`robot_error_reports` | `:852` | `:2965` |
| 23 | ★`cleanLogRunMeta(query)`★ ★新（10/06）★ | ★清掃ログの id → 清掃エリア・予定の終了★（ダッシュボードの直近モード専用。D-159）。★`where` は `cleanLogs()` と同じ非公開の `mainTimetableWhere()`（`:905`）を共有★。サンプル版は★終了＝開始・清掃エリア＝ロボット★ | `clean_timetables` `clean_timetable_histories`（`findTimetables()` 経由） | ★`:596`（`137a94b`）★ | ★`:1274`（`137a94b`）★ |
| 24 | ★`errorReports(query)`★ ★新（10/06）★ | ★エラー一覧★（`{ items, total }`）。★2段階で読む★: キーだけを SQL で `status IN`・`facility_id IN`・期間・★施設と清掃エリアの可視性（`ERROR_ROW_VISIBLE_WHERE`。U8）★に絞って全件読み → `filterSortErrorReportKeys()` で絞り込み・並べ替え → ページの `error_id` だけ列を足して読む。★0件なら2段目と予定のクエリを撃たない★。2段目で消えていた行は飛ばす（一覧全体を500にしない） | `robot_error_reports` ＋ 予定2表（`findTimetablesByChunks()`）＋ ★`error_lists`（改訂9。ページの行の `error_list_id` で `id IN` を1回・絞らない。予定と並べて読む）★ | ★`:666`（`f02eb1d`）★ | ★`:1833`（`f02eb1d`）★ |
| 25 | ★`errorReportById(id)`★ ★新（10/06）★ | エラー一覧の1行（★PATCH の前後の確認用。事前チェックかどうかを問わない★。大小文字は JS の `===` で決める） | `robot_error_reports` ＋ 予定2表（`findTimetableById()`）＋ ★`error_lists`（改訂9。`error_list_id` が null なら撃たない）★ | ★`:680`★ | ★`:1875`★ |
| 26 | ★`updateErrorReportColumns(id, patch)`★ ★新（10/06）★ | ★`robot_error_reports` の5列（4列＋`error_list_id`。改訂9・D-184）を更新★（`{ matched }`）。★`error_list_id` を書けるのはこのメソッドだけ★（7 は4列のまま）。★形は `updatePrecheckErrorColumns()` と同じで、違いは写しの関数だけ（こちらは `writeErrorReportColumns()`）。共通化せずに並べた★（`/alerts` 側の挙動を変えないため）。パッチの型は `ErrorStatusPatch` | ★`robot_error_reports`（書く）★ | ★`:723`（2026-10-27）★ | ★`:1950`（★書き込み用の接続★。2026-10-27）★ |
| 27 | ★`errorLists()`★ ★新（10/27）★ | ★エラー内容の選択肢★（`GET /error-lists`。★有効な行だけ・id の昇順★。規則の正は `activeErrorListItems()`）。★名前解決には使わない★（無効化した id の行が「不明」に化ける。名前解決は 24・25 の中で `error_lists` を絞らずに引く）。PATCH の検証でも呼ぶ | `error_lists` | ★`:753`（2026-10-27）★ | ★`:2158`（2026-10-27）★ |

★改訂3 の3本（24〜26）★のページの切り出しと行の組み立ては、`lib/db/index.ts` の非公開2関数 `errorReportPageIds()`（`:541`）・`errorReportItemsInPageOrder()`（`:558`）を fixtures 版と Prisma 版が共有しています（規則を2箇所に書かない）。

★**`notImplemented()` の呼び出しは2件です**★（`grep -n "notImplemented(" lib/db/index.ts` は3行当たり、1行は `:868` の関数の定義そのもの）。`inspections()`（`lib/db/index.ts:2228`）と `inspectionDetail()`（`:2231`）の Prisma 版が未実装で、呼ばれると例外を投げます。

```ts
async inspections() {
  return notImplemented("inspections");
},
```

これは意図した状態です。点検記録は**メインDBに保存しない**（DFA 独自ストアに分離する）と決まっており（[CLAUDE.md](../CLAUDE.md) §7 / D-004）、画面も `PlaceholderScreen`（準備中の札）のままです。黙って空配列を返さないのは、「データが無い」のか「未実装」なのかを切り分けられるようにするためです（`lib/db/index.ts:862-867`）。

**それ以外の20メソッドは、サンプル版・Prisma版の両方が実装済みです。**

★**サンプル版を `notImplemented` にしないこと**★ — この警告が `lib/db/index.ts` の3箇所（`:709` `:744` `:775`）に書かれています。ダッシュボードも清掃面積の画面も `USE_FIXTURES=true` で開けなければならないからです。テストでも固定されています（`lib/fixtures/__tests__/masters.test.ts:22` `:91`）。

### 8.8 `lib/fixtures/` — サンプルデータ（15ファイル。★改訂3 時点 16★）

DB 接続前の開発はすべてここで行いました（D-044）。**DB 接続後も削除しません**（テスト用・デモ用・障害切り分け用。D-045）。行数は 2026-10-05 の `wc -l` です。

| ファイル | 行 | 持っているデータ |
|---|---|---|
| `index.ts` | 23 | まとめ役（14ファイルを再輸出） |
| `_helpers.ts` | 35 | 日時ヘルパー。「昨夜の清掃」という運用イメージが崩れないよう日付を相対で作る |
| `clean-logs.ts` | 363 | ★最重要★ 清掃詳細ログ。判定の境界値、L1〜L3、31日分、該当0件、長い施設名、1施設2台 |
| `clean-task-reports.ts` | 366 | 清掃結果（`clean_task_reports` 相当）。★`clean_area` を清掃ログの完了率から逆算して作る★（完了率が面積から `runCompletionRate()` で決まるようになったため。面積を独立に作ると `USE_FIXTURES=false` 経由で境界値 90.0 / 70.0 / 10.0 が壊れる。指示書AS 段階6）。`clean_ratio` 列には清掃ログと同じ値を入れてあるが、★完了率には使っていない★ |
| `task-status-rule.ts` | 76 | ★`clean_task_reports.status` の決め方の唯一の定義★。`clean-logs.ts` と `clean-task-reports.ts` が共有 |
| `areas.ts` | 215 | エリア別完了率。★面積は完了率から作り、`runCompletionRate()` で元の完了率に戻ることをテストで確かめている★ |
| `prechecks.ts` | 215 | アラート（事前チェック） |
| ★`error-reports.ts`★ ★新（10/06）★ | 354 | ★エラー一覧★（`FX-ERR-01`〜`20`。★DB の行の形★で持つ。表1 E1〜E9 と制御盤・清掃開始失敗・D-Sec・`clean_timetable_id` 0（予定外。FX-ERR-20））。★D-Sec アラートの元（FX-ERR-10・12〜17・19）★。既定15行・「解消済みも表示」18行。`error_level` 等はダミー（[docs/fixtures.md](fixtures.md) §13） |
| `reports.ts` | 559 | レポート（KPI・施設別・連続問題・エラー内訳）。★今年・前年の期間を含む★ |
| `facilities.ts` | 285 | 施設マスタ。ロボット **0台 / 1台 / 3台** |
| `groups.ts` | 26 | ホールディングス |
| `customers.ts` | 33 | 顧客（★4件。だからレポートの KPI は開発中いつも 422 になる★） |
| `schedules.ts` | 239 | スケジュール |
| `ds-devices.ts` | 135 | D-S制御盤 |
| `inspections.ts` | 154 | 点検記録（画面は未着手） |

★**サンプルデータにはわざと異常なケースが仕込んであります。**★ 「きれいなデータ」だけでは、境界値や表示崩れのバグが見つからないからです。実例:

- ロボット **0台**の施設（`facilities.ts:262`）— 「1清掃エリア＝1ロボット」の前提が崩れたときに**静かに空文字を返さず例外で気づける**ことを確認するため
- ★3日前のデータを意図的に入れていない★（`prechecks.ts:212-215`）— 該当0件の見え方を確認するため
- 連続問題施設のうち ★L1 を1件だけ意図的に残してある★（`reports.ts:392` `:436`）
- `ds-devices.ts` は「正常→異常→正常→異常」の順で定義してあり、★画面で「異常→正常」に並び替わること★を確認する用途です（`:15-16`）

**用意すべきケースの一覧は [docs/fixtures.md](fixtures.md) が正です。** 新しい境界値を足すときは、先にそちらへ書いてください。

もう1つの決めごと: ★サンプルデータであっても判定は本番と同じ関数を通します★（`clean-logs.ts:24-27`）。`grade` と `escalation_level` は `lib/judge/` が埋めています。

★**実在の店舗名・住所・電話番号・氏名は絶対に入れないこと**★（`lib/fixtures/index.ts:9`。[CLAUDE.md](../CLAUDE.md) §7）。★ステージングには実店舗のデータが入ったので、なおさら混ぜないでください★。

**変えると何が起きるか**: サンプルから境界値のケースを消すと、`lib/judge/__tests__/` は通ったまま画面だけが壊れます。全テストが通っていても、fixtures は画面で叩いて確認してください。

### 8.9 `prisma/schema.prisma` と `lib/generated/prisma/`

| 項目 | 内容 |
|---|---|
| `prisma/schema.prisma` | 952行。★39モデル★ — TeamWorks の定義書の28テーブル ＋ 不要テーブル `clean_link_schedules` ＋ Laravel の基盤10テーブル（`:37-41`）。最後の `db pull` は2026-09-11（DDL `clean_watch_20260911.sql`。`:15-27`） |
| `lib/generated/prisma/` | `schema.prisma` から自動生成される読み書き部品（Prisma クライアント）。★`.gitignore` 済み（`.gitignore:47`）★ |

流れは一方通行です。

**TeamWorks がDBを変える → `prisma db pull` → `schema.prisma` が書き換わる → `prisma generate` → `lib/generated/prisma/` ができる**

★**どちらも手で直さないでください**★（D-056 / [CLAUDE.md](../CLAUDE.md) §9.1）。`schema.prisma` は `db pull` の生成物なので、手で直しても次の `db pull` で消えます。消えた瞬間、「直したはずの型」が静かに元へ戻ります。

★**例外が2つあります**★（★2つ目は改訂9 で増えた★）。

1. （D-156 項3 / 指示書AT）`db pull` の後に `updated_at` 列へ `@updatedAt` を手で付け直す運用（D-059）がありますが、★`RobotErrorReport.updated_at` にだけは付けません★（`prisma/schema.prisma:341-347`。ファイル冒頭 `:33-34` にも注記）。アプリがこの表の列（4列。エラー一覧の経路では `error_list_id` を足した5列）を書くので、付けると Prisma が `updated_at` を勝手に書き込もうとするためです（8.10）。★付け忘れ・付け直しはガードテスト（`lib/db/__tests__/robot-error-write-sql.test.ts`）が見張っています★。
2. ★（D-184 / 指示書BD §5-1。2026-10-27）`ErrorList` モデル（`error_lists`。`prisma/schema.prisma:372-386`）と `RobotErrorReport.error_list_id`（`:354`）は、db pull ではなく★テーブル定義書を見て手で書きました★。ステージングに接続できず（1045）db pull できないためです（★D-056 の例外★。ファイル冒頭 `:35-37` と各所にも注記）。★`error_list_id` は定義書では必須だが、ユーザー回答③「初期は空欄」で NULL 可にしてある★。★接続回復後に db pull で置き換え、手書きとの差分（型・NULL 可否）を確かめること★（指示書BD §8）。★`@updatedAt` はどちらにも付けていません★。

★**今後の DDL 変更待ち**★: `robot_error_reports` をエラー全般の表にし、`robot_id` / `serial_number` を NULL 可にする（制御盤・セキュリティのエラーも入れる）と TeamWorks が回答しています（第10回 N-5）。★DDL の変更後に `db pull` します★（D-155 項5。記号と本文の食い違いを第11回 P-8〜P-10 で照会中。D-157）。

`lib/generated/prisma/` が git に入っていないということは、**clone 直後の環境にはこのフォルダが存在しない**ということです。だから `lib/db/prisma.ts` は `lib/db/index.ts` の先頭で静的に読み込まれません（`prisma.ts:4-8`）。読み込むようにすると、Prisma クライアント未生成の環境（`USE_FIXTURES=true` の通常の開発者を含む）で `npm test` が全滅します。

**メインDBへのマイグレーション作成・スキーマ変更は禁止です**（[CLAUDE.md](../CLAUDE.md) §7）。

**変えると何が起きるか**: `schema.prisma` を手書きで直して型を合わせると、ローカルでは通って本番で落ちます。ローカル検証環境は MySQL 8.4、本番は MariaDB 10.11 で、同一ではありません（D-100）。

### 8.10 時刻の扱い — ★9時間を足さない★

★**DB に入っている時刻の値は、列の型（TIMESTAMP / DATETIME）によらず JST の壁時計の値です**★（[CLAUDE.md](../CLAUDE.md) §3。ユーザー指摘 2026-10-01「データベース上の時間は全てJSTに変換済みです」）。★**生の値に9時間を足さないこと**★。

| 段 | 何をするか | 場所 |
|---|---|---|
| 読む | Prisma のアダプタは DB の文字列を「UTC」とみなして日時にする。そのままだと真の時刻より9時間先を指すので、`fromDbJst()` で9時間引いて真の時刻に戻す | `lib/format/date.ts:72`（理由は `:53-71`） |
| 画面に出す | `jstIsoFromDb()` が `"2026-09-11T02:00:00+09:00"` の形にする。★DB の生値と画面の表示が一致すれば正しい★ | `lib/format/date.ts:101` |
| 期間で絞る | JST の `YYYY-MM-DD` を、DB の壁時計値の範囲に直す | `dbDateRange()` `lib/db/date-range.ts:50` |
| 書く | アラートの対応開始日時・解消日時は `toDbJst()` を1回だけ通して書く | `lib/format/date.ts:87` / `lib/db/robot-error-write.ts:78`（`dateOrNull()`。2026-10-27） |
| 時差の値 | 540分（9時間） | `DB_DATETIME_OFFSET_MINUTES`（`lib/config/index.ts:236`） |

**経緯（混乱しやすいので）**:

- 以前は一部の列が UTC で書かれていました。★`control_reports.reported_at` は UTC で書かれていて9時間早く見え、`robot_error_reports` は `errored_at` が正しい JST で、ずれていたのは `created_at`（UTC）の側でした★（TeamWorks 第10回 N-14a / N-14b）
- ★TeamWorks は 2026-10-02 に DB の既定のタイムゾーンを 'Asia/Tokyo' にしてデータを再登録した、以降はずれない★と回答しています（先方の申告。★当方の再実測は未了★）
- ★アプリのコードは変えていません★（D-155 項4）。サーバーが JST になったことで、TIMESTAMP 列も JST の文字列で返り、`fromDbJst()` の前提どおりになるためです。★残る危険は、再登録されなかった古い TIMESTAMP 行だけ★（そこだけ +9時間に見える）。再実測で確かめます
- ★一度「TIMESTAMP は UTC で返るので +9時間」と誤って書き（`9c175a2`）、取り消した（`d540335`）経緯があります★。★推測で9時間ずらさないこと★
- `lib/config/index.ts:225-235` のコメントは「`@@system_time_zone = JST`」（TeamWorks 第8回 A2）と書いていますが、ステージングの実測は UTC でした（2026-10-02 に JST に変更。D-155 項4）。値（540分）はどちらの場合も正しく働きます
- ★本番DBのタイムゾーンは未確認★です（第11回 P-11 で照会中。D-157）

**変えると何が起きるか**: `fromDbJst()` を通さずに `toJstIso()` へ直接渡すと、表示が9時間進むうえ、`isFutureSchedule()` の比較が狂い、★深夜2時の清掃が翌朝の確認時刻でも「まだ未来」と判定されて画面から消えます★（`lib/format/date.ts:65-67`）。

### 8.11 `data/settings/` — メインDBに書けないものの置き場

メインDBは読み取り専用で、書いてよいのは4カラムと、エラー一覧の経路の `error_list_id`（D-184）だけです（[CLAUDE.md](../CLAUDE.md) §3。その書き込みは 8.12）。それ以外に画面で設定した値は、**`data/settings/` の JSON ファイル**に行きます。

| ファイル | 中身 | 書き込む窓口 | 既定パスを決めている場所 |
|---|---|---|---|
| `ds-device-notes.json` | D-S制御盤の備考 | `PATCH /api/ds-devices/{serial_number}/note` | `lib/settings/ds-device-notes.ts:54` |
| `advice.json` | アドバイス文言の編集内容 | `PATCH /api/settings/advice`（★呼ぶ画面は0件★） | `lib/settings/advice.ts:45` |
| `display-limits.json` | 表示上限（連続日数の頭打ち） | `PATCH /api/settings/display-limits` | `lib/settings/display-limits.ts:50` |

この作業環境では `ds-device-notes.json` だけが存在します（3バイト＝実質空。2026-10-05 確認）。残り2つは**初回保存時に作られます**。保存されていない間は、コードに書かれた既定値が使われます。

読み書きの共通部品は `lib/settings/json-store.ts`（234行）です。押さえるべき点が3つあります（`:6-24`）。

1. ★`/data` は `.gitignore` 済み（`.gitignore:50`）★ ＝ **git のバックアップがありません**。だから壊れたファイルは `.bak` に退避してから書き直します
2. ★別プロセス間のロックは入れていません★ — 実質1人・1プロセスの運用を前提に、rename によるアトミック差し替えで「壊れたファイルが残る」ことを防いでいます。★同じファイルへの書き込みはプロセス内で1件ずつ順に行います★（`writeQueues`。`c5fe519`。Windows で同時の rename が `EPERM` になるため）。**同時保存は後勝ち**です（呼んだ順の最後の値が残る）
3. ★`lib/logger` を import していません★ — すると `@/lib/db` 経由でサンプルデータ全体を引き込んでしまうためです

**変えると何が起きるか**: これらをメインDBに保存しようとすると、権限エラーになります（更新用アカウントは4カラムと `robot_error_reports.error_list_id` しか UPDATE できません。★`error_list_id` の権限は未確認★）。サーバーを入れ替えるときに `data/` を引き継ぎ忘れると、備考・アドバイス文言・表示上限が**全部既定値に戻ります**。git には入っていないので、復元できません。★サーバーの構築・保守は TeamWorks です（D-143）★ので、移行手順にこのフォルダを明記しておく必要があります。

### 8.12 ★メインDBへの書き込み — アラートの対応状況（2026-10-02 から）★

★**D-Watch がメインDBに書き込むのは、ここで説明する1本の道だけです**★（指示書AT。D-152 / D-156）。
★**改訂3（2026-10-06）: エラー一覧の `PATCH /api/errors/{error_id}/status`（`app/api/errors/[error_id]/status/route.ts`）が加わり、道は2本です**★（D-160 / 指示書AW）。
★順序と部品は下の図と同じ★で、違うのは ③⑥ が `db.errorReportById()`（★`robot_error_reports` に行があれば書ける。事前チェックかどうかを問わない★）、⑤ が `db.updateErrorReportColumns()`（★改訂3 では同じ `writeRobotErrorColumns()`。改訂9 から5列の `writeErrorReportColumns()`★）、404 の文言が「エラーが見つかりません」であること★と、下の改訂9 の差分★だけです。
★`/prechecks` のルートとは共通化していません★（`/alerts` の挙動を変えないため。指示書AW D12）。★ステージングでは 2026-10-06 16:11 JST に `/errors` から1行を書いて戻し、4列が操作前と完全一致・権限エラーなし★（先方投入データ。`docs/handoff.md` 追記62）。
★**改訂4（2026-10-06。D-165）: 画面から送るのは「編集モード」の「保存（N件）」を押したときだけです**★。それまでの「プルダウンで選んだら即保存」（D-161）・「備考はフォーカスが外れたら保存」（指示書AT §3-8）は廃止しました。
画面側の規則は `lib/prechecks/edit-drafts.ts`（純粋関数。アラートとエラー一覧が共有）にあります — ★変えた行だけを、行の並び順に1行ずつ順に送る（並列にしない）★／★1行の失敗で止めない★（残りも送り、失敗した行は下書きを残す）／★送る本文は対応状況が必ず・備考は変えたときだけ★（`note` を省略すると備考は変えない。備考を消すときは `note: ""`）。★サーバー側（下の図と表）は変えていません★ — API・書き込む列・日時の規則は指示書AT のままです。
書くのは `robot_error_reports` の4列 — ★`status`（対応状況）/ `comment`（備考）/ `responded_at`（対応開始日時）/ `resolved_at`（解消日時）★ — と、★エラー一覧の道だけが書く5列目の `error_list_id`（エラー内容。改訂9・D-184）★だけで、[CLAUDE.md](../CLAUDE.md) §3 が更新用アカウントに許している列と同じです（★`error_list_id` の権限は「あるはず」だが未確認★）。★それ以外の列・それ以外のテーブルに書くコードはありません★（`control_error_histories` にも書かない）。★書き込んでよいのはステージングだけ★（D-152）で、本番DBにはまだ接続していません。

★**改訂9（2026-10-27。D-184 / 指示書BD §3-3・§5-4）: エラー一覧の道は `error_list_id` も書きます**★（画面は指示書BE。エラー一覧の「保存」だけが `error_list_id` を載せる。アラートと D-Sec は載せない）。下の図・表は事前チェックの道のままで、エラー一覧の道は次のところが違います。

| 段 | エラー一覧の道（`app/api/errors/[error_id]/status/route.ts`） |
|---|---|
| ② 本文 | `parseErrorStatusUpdateBody()`（`lib/errors/status-update.ts:52`。呼び出しは `route.ts:122`）— `status` / `note` は事前チェックと同じ関数で検査し、加えて ★`error_list_id` は省略なら変えない・1以上の整数でなければ 422★（null・0・負数・小数・文字列。★解除は作らない★） |
| ④ 計画 | `planErrorStatusUpdate()`（`lib/errors/status-update.ts:87`。呼び出しは `route.ts:158`）— 状態・日時・備考は `planPrecheckStatusUpdate()` をそのまま使い、`error_list_id` は ★1. 現在値と同じなら検証せず書かない → 2. 選べない行（`error_list_selectable` が false）なら 422 → 3. 有効な選択肢でなければ 422 → 4. エラー内容か備考を変える要求で、適用後が備考必須（131）なのに備考が空なら 422★。★状態だけを変える要求には 4 を掛けない★（アラート画面からは同じ行の備考を消せるので、触っていない備考を理由に止めないため。監督判断 2026-10-27）。★有効な選択肢は id が変わり・選べる行のときだけ `db.errorLists()` で引く★（`errorListIdToLookUp()` `:71`。`route.ts:151`）。★空パッチの判定 `isEmptyErrorPatch()`（`:133`）は `error_list_id` も含めて見る★ |
| ⑤ 書き込み | `db.updateErrorReportColumns()` → ★`writeErrorReportColumns()`（`lib/db/robot-error-write.ts:206`）★。★5つ以外のキー・5列以外の列・空のパッチ・1以上の整数でない `error_list_id` は例外★（`robotErrorColumnsFromErrorPatch()` `:159`）。`error_list_id` は `bigint` にして書く |
| 監査ログ | `error_list_id_old` / `error_list_id_new`（★id の数値だけ。分類名・詳細の文字列は載せない★。`lib/logger/events.ts`） |

★事前チェックの道（`PATCH /prechecks/{id}/status`）は変えていません★ — 本文に `error_list_id` があっても捨て（`parsePrecheckStatusUpdateBody()`）、写しの `robotErrorColumnsFromPatch()` は `error_list_id` を例外にします。★D-Sec アラートの保存（`lib/ds-alerts/edit.ts`）は `error_list_id` を送りません★（送っても `D-S_CONTROL_FAILURE` の行は選べないので 422）。この3点は `app/api/prechecks/[id]/status/__tests__/route.test.ts`（捨てる）・`lib/db/__tests__/robot-error-write-sql.test.ts`（4列版は例外）・`lib/errors/__tests__/write-paths.test.ts`（D-Sec の本文に入らない）が固定しています。

```mermaid
flowchart LR
  S["アラート画面<br/>AlertsScreen.tsx<br/>（編集モードの「保存（N件）」で<br/>変えた行だけ1行ずつ）"] -->|"PATCH /api/prechecks/{id}/status<br/>{ status, note? }"| R["ルート<br/>app/api/prechecks/[id]/status/route.ts"]
  R -->|"① 形式"| P1["parsePathErrorId()<br/>lib/api/path-params.ts"]
  R -->|"② 本文"| P2["parsePrecheckStatusUpdateBody()<br/>lib/prechecks/status-update.ts"]
  R -->|"③ 実在（参照用の接続）"| G["db.precheckItemById()"]
  R -->|"④ 計画（変える列だけ）"| PL["planPrecheckStatusUpdate()"]
  R -->|"⑤ 書き込み（書き込み用の接続）"| W["db.updatePrecheckErrorColumns()<br/>→ writeRobotErrorColumns()<br/>lib/db/robot-error-write.ts"]
  W --> DB[("robot_error_reports<br/>4列だけ UPDATE")]
  R -->|"⑥ 読み直し"| G
  R -->|"監査ログ"| L["db.write<br/>lib/logger/db-write.ts"]
```

| 段 | 何をするか | 場所 |
|---|---|---|
| ① 形式 | URL の `{id}`（`error_id`。`varchar(50)`）を検査。50文字を超えたら 422 | `parsePathErrorId()`（`lib/api/path-params.ts:89`）。上限は `PATH_ERROR_ID_MAX_LENGTH` |
| ② 本文 | `status`（未解消 / 対応中 / 解消）は必須、`note` は任意。★それ以外のキーは捨てる★（`updated_at` など）。備考は400字まで（トリム前） | `parsePrecheckStatusUpdateBody()`（`lib/prechecks/status-update.ts:100`） |
| ③ 実在 | ★参照用の接続★で1行読む。★書けるのは `/alerts` に「事前チェック異常」として出る行だけ★。出発失敗・予定に紐づかないエラー・大小文字だけ違う id は 404 | `db.precheckItemById()`（Prisma 版 `lib/db/index.ts:1750-1814`。★`a961829` では `:1652-`★。★D-171 から親の本清掃の `clean_task_reports` も読む（`main_run_succeeded` のため。参照用の接続）★） |
| ④ 計画 | 今の値と要求から、★変える列だけのパッチ★を作る。何も変わらなければ書かずに今の値を返す | ★`planPrecheckStatusUpdate()`（`lib/prechecks/status-update.ts:50`）★ — パッチを作る唯一の関数 |
| ⑤ 書き込み | ★書き込み用の接続★で `updateMany` の1文。★4列以外のキーが来たら例外★ | `writeRobotErrorColumns()`（`lib/db/robot-error-write.ts:189-200`。2026-10-27）。呼び出しは `lib/db/index.ts:1855-1861`（2026-10-27） |
| ⑥ 読み直し | 書いた後の1行を返す。画面はこの値でその行だけ差し替える | `db.precheckItemById()` |

ルート本体は `app/api/prechecks/[id]/status/route.ts:88-173`（順序の宣言は `:11-13`）です。

#### 日時と備考の規則（`planPrecheckStatusUpdate()`）

- ★日時は画面から送りません。サーバーが状態に合わせて決めます★。未解消 → 日時なし / 対応中 → 対応開始日時だけ / 解消 → 両方（`lib/prechecks/status-update.ts:10-11`）
- ★足りない日時は「今」で埋める★。★既にある日時は上書きしない★（解消済みをもう一度「解消」にしても解消日時は動かない）
- ★状態を戻すと、その状態で空であるべき日時を消す★（例: 解消 → 未解消 で両方消える）。★同じ状態のままなら消さない★（備考だけ変えるときも画面は今の状態を送るので、先方が入れた日時を消さないため。`:44-45`）
- 備考: 送らなければ変えない / 空文字・空白だけは削除（`comment = NULL`）/ 前後の空白は落とす / 同じなら書かない（`:47-48`）
- 期待値表は `lib/prechecks/__tests__/status-update.test.ts`（#1〜#12）

根拠: D-156 項4（すべて推奨どおり。「戻すと消す・解消済みの再解消は上書きしない」）

#### ★事故を防いでいる仕掛け★

| 仕掛け | 何を防ぐか | 場所 |
|---|---|---|
| ★書き込みはこの1ファイルだけ★ | 許された列以外を書くコードが散らばること | `lib/db/robot-error-write.ts:1-28`（2026-10-27）。書いてよい列の一覧は ★経路ごとに2つ★ — 事前チェックの4列 `ROBOT_ERROR_WRITABLE_COLUMNS`（`:38-43`）と、エラー一覧の5列 `ERROR_REPORT_WRITABLE_COLUMNS`（`:52`。4列＋`error_list_id`。D-184）。★どちらにも列を足さないこと★（足すなら先に決定記録と [CLAUDE.md](../CLAUDE.md) §7 を直す）。★4列版に `error_list_id` を入れないこと★（事前チェック・D-Sec からエラー内容を書ける道ができる） |
| ★実行時の歯止め★ | 型をすり抜けたキーが DB に届くこと | `robotErrorColumnsFromPatch()`（`:144-149`）— 4つ以外のキー（★`error_list_id` も★）・4列以外の列・空のパッチは例外。★5列版 `robotErrorColumnsFromErrorPatch()`（`:159-171`）にも同じ歯止め★（5つ以外のキー・5列以外の列・空・1以上の整数でない `error_list_id` は例外）。検査の本体は共有の `assertPatchKeys()`（`:88`）/ `assertDataColumns()`（`:97`）、4列の写しは共有の `fourColumnsFromPatch()`（`:110`）。★例外のメッセージに備考の中身を出さない★ |
| ★`update` ではなく `updateMany`★ | `update` は SELECT → UPDATE → 全列 SELECT をまとめて走らせる。更新用アカウントに SELECT 権限があるか分からない（第11回で照会中。D-156 項5） | `:18-21` / `:195-198`・`:212-215`（2026-10-27） |
| ★`RobotErrorReport.updated_at` に `@updatedAt` を付けない★ | 付けると Prisma が `updated_at = <アプリの時刻(UTC)>` を勝手に SET に足す → ①5列目の書き込みで権限エラー ②JST の DB に UTC の値が入る。DB は `ON UPDATE current_timestamp()` で自分で更新する | `prisma/schema.prisma:338-344`（D-156 項3） |
| ★ガードテスト★ | 誰かが db pull の後で `@updatedAt` を機械的に付け直すこと | `lib/db/__tests__/robot-error-write-sql.test.ts`（★本物の生成クライアント＋SQLを記録するだけの偽アダプタ★で、SET 句が渡した列だけ・`updated_at` を含まないことを確かめる。DBには接続しない） |
| ★日時は `toDbJst()` を1回だけ通す★ | 9時間ずれた日時を書くこと | `dateOrNull()`（`:78-85`。2026-10-27）。`lib/format/date.ts:87` |
| ★大小文字の照合をアプリで確かめる★ | DB の照合順序は大小文字を区別しないので、`rer-1` で `RER-1` の行を書き換えられること | `lib/db/index.ts:1764` |
| ★件数0なら 404★ | 確認と書き込みの間に行が消えた場合（ステージングのデータ入れ替え等） | `route.ts:153-156` |

#### 接続 — 書き込み用アカウント

`getWritePrisma()`（`lib/db/prisma.ts:100-137`）が書き込み用の接続を返します。判定は `databaseWriteCredentials()`（`lib/config/env-check.ts:162-170`。`npm run check:env` と同じ1本）です。

| `DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD` | どうなるか |
|---|---|
| ★2つとも未設定★ | ★参照用の接続（`getPrisma()`）で書く★（いまはこれで動く見込み） |
| ★片方だけ★ | ★例外★（黙って参照用に倒さない） |
| 2つとも設定 | 別の接続を作る。★接続先は参照用と同じDB★なので `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` も要る（★URL 形式は設けない★ — `%40` が壊れる問題を持ち込まないため） |

#### 監査ログ `db.write`

★成功・失敗の両方で1件ずつ★出します（`lib/logger/db-write.ts:53`。型は `lib/logger/events.ts:250`（2026-10-27））。何も変わらない要求では書いていないので出しません。
載せるのは表名・`error_id`（★文字列のまま★）・SET した列・状態と日時の前後の値・`comment_changed`（★備考が変わったかどうかだけ。中身は載せない★）・成否です。★エラー一覧の道は `error_list_id_old` / `error_list_id_new`（id の数値だけ。改訂9・D-184）も載せます★。
★失敗時もエラーメッセージは載せません★（DB のユーザー名が入るため）。載せるのは `dbErrorCode()`（`db-write.ts:22`）が取り出したコードだけです。

#### サンプルデータ（`USE_FIXTURES=true`）のとき

`lib/db/fixture-precheck-overrides.ts` が ★id → 4項目の上書き表をメモリに持ちます★（`lib/fixtures/prechecks.ts` は書き換えない）。★dev サーバを再起動すると消えます★。Prisma 版と同じ歯止め（`robotErrorColumnsFromPatch()`）を通し、★事前チェック異常の行だけ★書けるのも同じです（`:46-76`）。
★エラー一覧は別の上書き表 `lib/db/fixture-error-report-overrides.ts`★（状態を共有しない。値は `toDbJst()` 済みの DB の列の形）。★改訂9 から5列（`error_list_id` も）を持ち、写しは Prisma 版と同じ `robotErrorColumnsFromErrorPatch()`★。★エラー内容の選択肢は `lib/fixtures/error-lists.ts`（決定稿131行）★から引きます。

**変えると何が起きるか**: ★`robot-error-write.ts` を通さずに Prisma の `update` を別の場所に書くと、4列・5列の歯止めも `@updatedAt` の見張りも効かなくなります★。`schema.prisma` の `RobotErrorReport.updated_at` に `@updatedAt` を付け直すと、ステージングでは権限エラー（または UTC の値の混入）になりますが、`npx prisma generate` → `npm test` でガードテストが赤くなるので気づけます。★D-Sec アラートも `robot-error-write.ts` の同じ1本で書く（`PATCH /errors/{error_id}/status`。D-180）★。`control_error_histories` を書く予定は無い。


---

## 9. 裏方 — 入口の関門・ログ・起動

画面には出ないが、**すべてのリクエストが必ず通る**部分です。ここを触ると全画面・全APIに一斉に効きます。

---

### 9-1. `proxy.ts` — すべての入口（115行）

`proxy.ts`（リポジトリ直下）は Next.js が**全リクエストの最初に呼ぶ関数**です。旧称 `middleware.ts` は Next.js 16 で非推奨になり `proxy` に改名されました（`proxy.ts:4-7`）。★**1プロジェクトに1ファイルしか置けない**★規約なので、ここには順序制御だけを書き、中身は `lib/proxy/` に分けてあります（`proxy.ts:9-11`）。

```mermaid
flowchart LR
    A["リクエスト到着"] --> B["① 相関IDの発行<br/>lib/proxy/request-id.ts"]
    B --> C{"② IP制限<br/>lib/config/allowed-ips.ts"}
    C -- "拒否" --> D["403 JSON<br/>code: forbidden<br/>ログ auth.ip_denied"]
    C -- "通過" --> E{"③ 認証<br/>共有トークン"}
    E -- "APIが未認証" --> F["401 JSON<br/>code: unauthorized"]
    E -- "画面が未認証" --> G["/login?next=元のパス へ転送"]
    E -- "通過・公開パス" --> H["本来の画面・APIへ"]
```

| # | 関門 | 実装 | 拒否時に返るもの |
|---|---|---|---|
| ① | 相関ID（リクエストごとの通し番号）の発行 | `lib/proxy/request-id.ts:27-35`（`crypto.randomUUID()`） | 拒否しない（必ず発行） |
| ② | IP制限 | `lib/proxy/authenticate.ts:78-83` | **403** `{error:{code:"forbidden",message:"このIPアドレスからは利用できません"}}`（`proxy.ts:66-74`） |
| ③ | 認証 | `lib/proxy/authenticate.ts:85-91` | APIは **401** `code:"unauthorized"`（`proxy.ts:82-86`）／画面は **`/login?next=<元のパス>`** へ転送（`proxy.ts:90-92`） |

#### ★順序に意味があります★

★**①が先である理由**★: 弾いたリクエストのログにもIDを付けるためです（`proxy.ts:13-17`、`docs/logging-design.md` §9）。逆にすると、★いちばん追跡したい403・401のログだけ `request_id` が `"-"` になります★。発行したIDは `runWithRequestId()` で包んでから使うので（`proxy.ts:45`）、この中で出るログにも必ず載ります。

★**②が③より先である理由**★: IPで弾くべき相手に、トークンの当たり外れを試させないためです（`authenticate.ts:8-9`）。

★クライアントが送ってきた `x-request-id` は信頼せず常に上書きします★（`request-id.ts:19-21`）。外から与えられたIDを通すと、別リクエストのログに同じIDを混ぜ込まれて相関が壊せます。

★認証失敗そのものはログを出しません★（`proxy.ts:78-80`）。未認証は「まだログインしていない」だけで、画面を開くたびに出るとノイズになるためです。追跡したいログイン試行は `/api/auth/login` 側が `auth.login_success` / `auth.login_failed` として記録します。

**変えると何が起きるか**: `proxy.ts:112-114` の `matcher` を消すとCSS・画像にも関門がかかり画面が崩れます。`matcher` から `/login` を除外すると保護漏れが見つけにくくなるので、除外せず関数の中の `isPublicPath()`（`authenticate.ts:35-39`）で通します。`runtime` を書くとエラーになります（proxy は Node.js 固定）。

---

### 9-2. 認証 `lib/auth/`（4ファイル）

P1は**社内利用のみ**のため、利用者ごとのアカウントは無く、★**共有トークン1本**★の簡易認証です。

| ファイル | 行数 | 役割 | 主な定数・関数 | 変えると何が起きるか |
|---|---|---|---|---|
| `token.ts` | 49 | トークン照合 | `verifyToken()`（`:22`）/ `readAuthToken()`（`:47`） | ★`===` に書き換えると応答時間の差から1文字ずつ推測されます★（タイミング攻撃）。必ず `timingSafeEqual` を通す（`:4-8`）。★長さが違う場合も同じ時間を使ってから false を返す★（`:34-38`） |
| `cookie.ts` | 33 | ログイン状態を保つcookie | `SESSION_COOKIE_NAME = "dwatch_session"`（`:12`）/ `httpOnly` `secure` `sameSite:lax` `path:/`（`:28-33`） | 名前を変えたら `lib/logger/redact.ts:45` の伏せ字対象も同時に直す。直さないと★cookie値がログに出ます★ |
| `client-ip.ts` | 78 | 接続元IPの取り出しと許可判定 | `clientIp()`（`:36`）/ `isAllowedIp()`（`:66`） | ★下記★ |
| `next-path.ts` | 31 | ログイン後の戻り先を安全な値に丸める | `safeNextPath()`（`:14`）/ `DEFAULT_NEXT_PATH="/dashboard"`（`:12`） | 検査を外すと `?next=//evil.example` で外部サイトへ飛ばされます（フィッシングの踏み台） |

★`AUTH_TOKEN` が未設定・空なら `verifyToken()` は**常に false**★（誰も入れない。`token.ts:26`）。★設定漏れが「認証を無効化して全部通す」にならない★安全側の設計です。**トークンの実値はこの文書に書きません。**

★**cookie に有効期限を付けません**★（`cookie.ts:24-26`）。[CLAUDE.md](../CLAUDE.md) §7 が「トークン期限/残日数の実装」を禁止しているためで、結果としてブラウザを閉じると消えるセッションcookieになります。

#### ★`X-Forwarded-For` は末尾を見る★

`X-Forwarded-For`（XFF）は「どこから来た通信か」を中継サーバが書き足すヘッダです。★**先頭はブラウザ側が勝手に書ける**★ため、先頭を信じると `X-Forwarded-For: <許可IP>` と自分で書くだけでIP制限を素通りできます。手前の nginx は★自分が見た接続元を末尾に追記★するので、信頼できるのは末尾側だけです（`client-ip.ts:12-16`。D-077 項5）。

`TRUSTED_PROXY_HOPS` は「自分の前に中継サーバが何段いるか」で、`clientIp()` は★末尾からこの数だけ遡った要素★を採用します（`client-ip.ts:36-49`）。現在 ★**1**★（LBと nginx が同一なので1段。`allowed-ips.ts:63`）。要素数が足りなければ ★`null`（取れない扱い）★ とし、★足りない分を先頭で埋めません★（`client-ip.ts:18-19`）。

★表記の正規化もしません★（`:21-25`）。IPv6の短縮形・`::ffff:` 付き・ポート付きはそのまま扱うので、★`ALLOWED_IPS` には nginx が実際に付ける表記をログで確認してから書きます★。

★要素数で「直接アクセスかどうか」を判断してはいけません★（`:27-29`）。クライアントがXFFを送らない普通のリクエストも、nginx が1個足して要素1個になるためです。

**変えると何が起きるか**: 実際1段なのに `2` にすると全員 `null` 扱い＝★全員403★（すぐ気づけます）。実際2段なのに `1` のままだと★末尾がLBの内部IPになります★（こちらは気づきにくい）。

---

### 9-3. IP制限の現状 ★重要★（2026-10-01 実測）

| 定数 | 現在の値 | 意味 | 場所 |
|---|---|---|---|
| `ALLOWED_IPS` | ★**空の配列**★（例のコメント1行のみ） | 許可IP一覧。★完全一致のみ。CIDR・レンジ・ワイルドカード非対応★ | `lib/config/allowed-ips.ts:24-26` |
| `ALLOW_ALL_WHEN_EMPTY` | ★**`true`**★ | 一覧が空のとき制限を「かけない」 | `:35` |
| `ALLOW_DIRECT_ACCESS` | `process.env.ALLOW_DIRECT_ACCESS === "true"` | nginx を通らない直接アクセスを通すか（★4つの中で唯一の環境変数★） | `:52` |
| `TRUSTED_PROXY_HOPS` | **`1`** | 信頼する前段プロキシの段数 | `:63` |

★**`ALLOWED_IPS` / `ALLOW_ALL_WHEN_EMPTY` / `TRUSTED_PROXY_HOPS` の3つは環境変数ではなくコードに直接書かれています。`.env.local` を直しても変わらず、変更にはコード修正とデプロイが必要です。**★

**現状の判定順**（`isAllowedIp()` `client-ip.ts:66-78`）:

1. `ip === null`（XFFから取れない＝直接アクセス）なら ★`ALLOW_DIRECT_ACCESS` だけで決める★。`ALLOW_ALL_WHEN_EMPTY` は一切関与しない
2. 一覧が空なら `ALLOW_ALL_WHEN_EMPTY` を返す
3. 一覧との完全一致

★**この2つを1つの設定値にまとめない理由**★（`:54-57`）: まとめると、一覧が空の初期状態のまま本番へ出た瞬間に★直接アクセスが素通り★します。

★★**結論: 一覧が空で `ALLOW_ALL_WHEN_EMPTY` が `true` の現在、nginx 経由で接続元IPが取れるリクエストはどのIPからでも全部通ります。IP制限は実質かかっていません。**★★ 本番前に `ALLOWED_IPS` を埋め、`ALLOW_ALL_WHEN_EMPTY` を `false` にする必要があります（`:31`。D-101）。★これは検証段階のあいだは保留で、リリース直前にまとめて行う方針です★（D-154。[CLAUDE.md](../CLAUDE.md) §3 の対象一覧に入っています）。

**変えると何が起きるか**: `ALLOWED_IPS` に1件でも書くと `ALLOW_ALL_WHEN_EMPTY` は無視され、★書いたIP以外は全部403★。★プライベートIP（`10.x` / `172.16-31.x` / `192.168.x`）は書かないこと★（`:20-22`）。万一プロキシが2段だった場合に末尾がLBの内部IPになり、そこに一致すると★「全員素通り」という逆方向の事故★になります（1段確定なら全員403で即座に気づけます）。

---

### 9-4. ログ `lib/logger/`（7ファイル）

| ファイル | 行数 | 役割 | 変えると何が起きるか |
|---|---|---|---|
| `index.ts` | 134 | ロガー本体。`logEvent()`（`:113`）が1行JSONを出す。★`mixin()`（`:46-51`）が全ログに `request_id` と `data_source`（`fixtures` / `prisma`）を自動付与★ | `mixin` を外すと全ログから相関IDが消える |
| `events.ts` | 487 | ★イベント名と項目の型定義★。ここに無いイベントはコンパイルが通らない | 新しいログを出すには、まずここに型を足す |
| `redact.ts` | 234 | ★出してはいけない項目の検出・除去★ | 下記 |
| `context.ts` | 73 | 相関IDの保管庫（`AsyncLocalStorage`）。`REQUEST_ID_HEADER = "x-request-id"`（`:34`） | 壊すとリクエストの串刺し追跡ができなくなる |
| `timestamp.ts` | 30 | ISO 8601 の JST（`+09:00`）ミリ秒付き（`:24-30`） | 秒精度にすると同一秒内の順序が追えない |
| `api.ts` | 50 | APIの `catch` から呼ぶヘルパー。★画面へ返すエラー本文をそのまま受け取る★のでログと表示がズレない（`:3-7`） | 文言を組み直すとログと応答が食い違う |
| ★`db-write.ts`★ ★新（10/05）★ | 60 | ★メインDB書き込みの監査ログ `db.write`★ を出す `logDbWrite()`（`:53`）と、例外から DB のエラーコードだけを取り出す `dbErrorCode()`（`:22`）。★成功・失敗の両方で1件ずつ★。★備考の中身もエラーメッセージも載せない★（§8.10） | `err` やメッセージを載せると、DB のユーザー名（列の権限エラーの文言に入る）がログに残る |

**イベント名の一覧**（`events.ts` の行番号。★2026-10-05 時点★）

| イベント | 行 | いつ出るか |
|---|---|---|
| `app.start` | `:44` | 起動時に1回 |
| `app.uncaught` | `:59` | `catch` で拾えなかったエラー |
| `api.error` | `:77` | APIが意図的に返したエラー |
| `judge.grade` / `judge.escalation` / `judge.advice` / `judge.areas` | `:90` / `:110` / `:134` / `:182` | 判定ロジックの記録。★`judge.areas` の分子・分母は `overall_completion_rate_sum` / `overall_completion_rate_count`★（`:194-197`。1回ごとの完了率の合計と件数。指示書AR で面積の合計から差し替え、AS で `overall_clean_ratio_*` から改名。★項目名に式の名前を入れない★ — 式を変えるたびに名前と中身がずれるため） |
| ★`judge.clean_area`★ ★新（10/05）★ | `:221` | 総清掃面積（F-08）の集計直後。★完了率用に取り寄せた行数 `rate_rows` と所要時間 `elapsed_ms`★、全体の実施回数と完了率（丸める前）。★期間を指定しないと全期間を運ぶので、重さを実データで見るため★（D-156 項2）。出すのは `app/api/clean-area/route.ts:99-107` |
| `db.write` | ★`:250`（2026-10-27）★ | ★許可された列（4カラム。`PATCH /errors/{error_id}/status` だけは `error_list_id` を足した5列。D-184）への書き込み★。★2026-10-02 から実際に出るようになった★（最初の利用者は `PATCH /prechecks/{id}/status`。§8.10） |
| `inspection.read` | `:272` | 点検記録の読み出し |
| `data.anomaly` | `:283` | データの異常を検知したとき |
| `settings.store_corrupt` / `settings.value_invalid` | `:315` / `:347` | 設定ファイルが壊れていた / 値が範囲外だった |
| `auth.login_success` / `auth.login_failed` / `auth.ip_denied` | `:387`（3つで1つの型） | ログイン試行とIP拒否 |
| `log.forbidden_fields_stripped` | `:408` | ★伏せ字の仕組みが実際に項目を取り除いたとき★ |

★`client_ip` を出すのは `auth.ip_denied` のときだけ★（`events.ts:431-436`）。拒否された接続元の生の表記が分からないと `ALLOWED_IPS` に登録できないためで、★ログイン成功・失敗には出しません★。

#### ★`redact.ts` — 伏せ字の仕組み★

値の中身は予測できないので、★**キー名で**判定して危ないキーごと取り除きます★（`redact.ts:1-7`）。守っているのは2種類です。

- ★**個人情報・自由記述（13個）**★（`:12-26`）: `facility_name` `address` `phone` `email` `manager_name` `charger_name` `inspector_name` `customer_name` `notes` `comment` `memo` `note` `detail`
- ★**認証情報（9個）**★（`:36-46`）: `cookie` `set-cookie` `authorization` `token` `session` `secret` `password` `api_key` `dwatch_session` — ★漏れると「読まれる」だけでなく「なりすまされる」ので別枠にしてあります★

大文字小文字は無視します（`Authorization` も拾う。`:57-64`）。検出したときの動きは環境で変わります（`enforceForbiddenFieldsPolicy()` `:163-175`）。

| 環境 | 動き | 理由 |
|---|---|---|
| 開発中 | ★例外を投げて止める★ | 書いた人がその場で気づけるように |
| 本番 | ★例外を投げず、キーを除去して warn を1行出す★ | ★ログの失敗でアプリを落とさない★ |

**変えると何が起きるか**: リストからキーを消すと、その項目が本番ログに素通りします。追加すると、そのキー名を使う既存ログが開発中に例外で落ちます。★`dwatch_session` は `lib/auth/cookie.ts` の cookie 名と同じ文字列でなければなりません★（cookie 名を変えるときは両方直す）。

**相関IDと出力先**: `proxy.ts` が発行したIDを `runWithRequestId()` が保管し、`mixin()` が全ログ行に `request_id` として載せるので、1リクエストのログを串刺しで拾えます。出力先は ★**stdout のみ**★（ファイル化・ローテーションはOS側。`index.ts:2`）。量は `LOG_LEVEL` で変え、未設定なら本番 `info` / それ以外 `debug`（`index.ts:22-25`）。詳細は [docs/logging-design.md](logging-design.md)。

---

### 9-5. 起動時 `instrumentation.ts`（80行）

★起動時に1回だけ `app.start` を出し、実際に効いているログレベル / `NODE_ENV` / ランタイム種別を記録します★（`:22-30`）。これが無いと「ログが出ないのは `LOG_LEVEL` のせいか、そもそも起動していないのか」を切り分けられません。

もう1つ `onRequestError()`（`:39-80`）があり、`catch` で拾えなかったサーバーエラーを ★`api.error` ではなく `app.uncaught`★ として記録します（`:34-37`）。★意図的に処理したエラーと捕捉漏れが同じ名前で混ざると、障害調査で区別できなくなるため★です。URLからクエリ文字列を落とすのは、施設名などの値が載るためです（`:51-53`）。

**変えると何が起きるか**: logger をファイル冒頭で `import` するとビルドが壊れます。★必ず関数の中で動的 import すること★（`:8-11`。pino は Node.js のAPIに依存するため）。

---

### 9-6. 開発・運用のコマンド（`package.json:5-14`）

| コマンド | 中身 | いつ使うか |
|---|---|---|
| `npm run dev` | `next dev` | 開発サーバー起動（日常） |
| `npm run build` | `next build` | 本番用に固める |
| `npm start` | `next start` | 固めたものを本番として動かす |
| `npm run lint` | `eslint` | 書き方チェック |
| `npm test` | `vitest run` | テスト1回実行（CIもこれ） |
| `npm run test:watch` | `vitest` | 変更を監視して自動再実行 |
| `npm run seed:local` | `tsx scripts/seed-local-db.ts` | ローカルDB `dwatch_local` にサンプルデータを入れ直す |
| `npm run check:env` | `tsx scripts/check-env.ts` | ★デプロイ前に必ず実行★ |

★**スクリプトは8本で、9/14から増減なし**★（実測）。

#### `npm run check:env` の検査項目

`.env.local` → `.env` の順に読み（`scripts/check-env.ts:16-18`）、★エラー1件でも終了コード 1★、警告だけなら 0（`:46-53`）。検査の中身は `lib/config/env-check.ts` の純粋関数で、★7項目（判定は10通り）★です（2026-10-02 に7番目が増えた）。

| # | 対象 | 条件 | 重さ | 行 |
|---|---|---|---|---|
| 1 | `AUTH_TOKEN` | 未設定・空（誰もログインできない） | error | `:52-58` |
| 1' | `AUTH_TOKEN` | ★`changeme`（`.env.example` の初期値）のまま★ | error | `:59-66` |
| 2 | `ALLOW_DIRECT_ACCESS` | `true` になっている | ★本番 error / 開発 warn★ | `:68-79` |
| 3 | `USE_FIXTURES` | 本番で `false` 以外（サンプルデータのまま動く） | warn | `:80-90` |
| 4 | `DATABASE_URL` | 未設定 | error | `:91-99` |
| 5 | `DATABASE_URL` | 本番でホストが `localhost` / `127.0.0.1` | warn | `:100-109` |
| 6 | `LOG_LEVEL` | pino のレベル名（`trace`/`debug`/`info`/`warn`/`error`/`fatal`）以外 | warn | `:110-119` |
| ★7★ | ★`DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD`★ | 2つとも未設定（★書き込みは参照用アカウントで行う★と知らせるだけ） | warn | `:122-131` |
| ★7'★ | 同上 | ★片方だけ設定★（書き込みが失敗する） | ★error★ | `:132-138` |
| ★7''★ | 同上 | 2つとも設定したが `DATABASE_HOST` / `DATABASE_NAME` が無い（書き込み用の接続は部品変数でしか組まない） | ★error★ | `:139-147` |

★**出力に値そのものを出しません**★（`DATABASE_URL` にはパスワードが入り、出力はCIのログに残るため。`env-check.ts:8-12` / `:189-192`）。★`debug` を弾かないこと★（`:110-112`。アプリの `LogLevel` は3つですが、開発時のしきい値として `debug` を返すため）。

**変えると何が起きるか**: この検査を飛ばして本番に出ると、`AUTH_TOKEN` が `changeme` のまま（＝共有トークンが公開情報）でも起動してしまいます。

---

### 9-7. 環境変数の一覧

`.env.example` と `.env.production.example` に書かれている変数です。★**実値はこの文書に書きません。`.env.local` も開いていません。**★

| 変数名 | 何を切り替えるか | 既定値 | 設定しないとどうなるか |
|---|---|---|---|
| `USE_FIXTURES` | サンプルデータか実DBか | 未設定は ★`true` 扱い★（`lib/db/use-fixtures.ts:17` が `!== "false"`） | サンプルデータのまま動く |
| `DATABASE_URL` | メインDBの接続先（1行で書く形式） | なし | `USE_FIXTURES=false` のとき起動時に落ちる（`lib/db/prisma.ts:67-71`） |
| ★`DATABASE_HOST`★ | 接続先を部品に分けて書く形式（★`DATABASE_URL` より優先される★） | なし | 未設定なら `DATABASE_URL` が使われる |
| ★`DATABASE_PASSWORD`★ | 同上（★`HOST` と両方揃ったときだけ部品形式になる★） | なし | 同上 |
| ★`DATABASE_USER` / `DATABASE_NAME`★ | 部品形式のとき★必須★ | なし | ★何が足りないかを言って起動時に落ちる★（`prisma.ts:47-54`） |
| ★`DATABASE_PORT`★ | 部品形式のポート | `3306`（`prisma.ts:59`） | 既定で動く |
| ★`DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD`★ ★新（10/05）★ | ★書き込み用アカウント★（`robot_error_reports` の4列（エラー一覧の経路は `error_list_id` を足した5列）の UPDATE だけに使う。`getWritePrisma()` `prisma.ts:100`）。`.env.example` にだけ記載（`.env.production.example` には未記載） | なし | ★2つとも未設定なら参照用アカウントで書く★。★片方だけだと書き込みが例外で失敗する★。2つとも設定するときは `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` も要る（§8.10） |
| `AUTH_TOKEN` | 共有トークンの期待値 | なし | ★誰もログインできない★（`lib/auth/token.ts:26`） |
| `LOG_LEVEL` | ログの量 | 本番 `info` / それ以外 `debug`（`lib/logger/index.ts:22-25`） | 既定で動く |
| `ALLOW_DIRECT_ACCESS` | nginx を通らない直接アクセスを通すか | 未設定＝`false`＝拒否（`allowed-ips.ts:52`） | ★ローカル開発では全ページ403★。本番は設定しないのが正解 |
| `NODE_ENV` | 本番判定（Node.js 標準。アプリでは設定しない） | 実行方法で決まる | `LOG_LEVEL` の既定と `redact.ts` の挙動が開発側に寄る |
| `NEXT_RUNTIME` | Next.js が自動で入れる。`app.start` ログに載せるだけ | Next.js が設定 | ログに `unknown` と出る（`instrumentation.ts:28`） |

★**部品変数（`DATABASE_HOST` ほか5本）が用意されている理由**★（`.env.example` / `lib/db/prisma.ts:32-33`）: ★パスワードに `@` が含まれると、`DATABASE_URL` に書いた `%40` が dotenv の展開で壊れることがある★ためです（`p%40ss` → `p`）。部品変数ならURLエンコードが要りません。★ユーザー名に既定値は置いていません★（実在のDBユーザー名をコードに書かない。[CLAUDE.md](../CLAUDE.md) §9.3）。

★`ALLOW_DIRECT_ACCESS` は `=== "true"` で判定します。`!== "false"` にしないこと★（`allowed-ips.ts:45-50`）。未設定・空文字・タイポ・`1`・`TRUE` はすべて拒否側に倒れ、本番で設定し忘れても安全です。★`USE_FIXTURES` が逆向き（`!== "false"`）なのは「開発しやすさ優先」で、意図的に違えてあります★。

#### ★未実装★

★`INSPECTION_DB_URL`★ は `.env.example` に点検記録用SQLiteの接続先として書かれていますが、★`lib/` `app/` `scripts/` `prisma/` のどこからも読まれていません★（`process.env.` の全件検索で0件。2026-10-01 実測）。点検記録の画面自体がサイドバーから隠されている状態（章7-6）と対応しています。

★**コード内で実際に読まれる環境変数は上表の14個です**★（`DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD` を数えて。`process.env.` と `env.` の全件検索。ワークツリー配下と `node_modules` を除く。2026-10-05）。


---

## 10. 人が書いていないファイル（開かなくてよいフォルダ）

リポジトリをエディタで開くと、ファイルは数万あるように見えます。**そのほとんどは機械が作ったもので、人が読む必要はありません。** この章は「どこを開かなくてよいか」の一覧です。

数字はすべて 2026-10-05 に `develop`（`ceeb736`）で実測しました（出所: `git ls-files` と `wc -l`。生成物は `find` で数えたディスク上の実ファイル）。

### 10.1 開かなくてよいもの

| 場所 | 規模 | 正体 | git に入っているか |
|---|---|---|---|
| `lib/generated/prisma/` | **47ファイル / 73,780行** | `prisma/schema.prisma` から `prisma generate` が作るDB読み書き部品。2026-09-14・10-01 時点と**同じ数字**（`@updatedAt` を1つ外しても行数は変わらない） | ★入っていない★（`.gitignore:47`） |
| `node_modules/` | 直下に405項目 | `npm install` で取ってくる外部部品（Next.js・React・Prisma 等） | 入っていない（`.gitignore:4`） |
| `.next/` | — | `next dev` / `next build` が作る作業場所 | 入っていない（`.gitignore:17`） |
| `tsconfig.tsbuildinfo` | 約190KB | 型チェックを速くするためのキャッシュ | 入っていない（`.gitignore:44`） |
| `next-env.d.ts` | 数行 | Next.js が自動で書く型の参照。`.next/dev/types/` を指す | 入っていない（`.gitignore:45`） |
| `package-lock.json` | 9,573行 | `npm install` が書く、外部部品のバージョン固定表 | ★入っている★（手で直さない） |
| `public/` | svg 5ファイル | ★初期セットアップ（`28f808d`、2026-08-26）の雛形の残り★。`app/` `components/` `lib/` のどこからも参照されていません | 入っている |

**変えると何が起きるか**: `lib/generated/prisma/` を手で直しても、次の `prisma generate` で跡形もなく消えます（[章8](#8-データの出どころ) §8.8）。`package-lock.json` を手で直すと、他の人の環境と本番で外部部品のバージョンがずれます。`public/` の svg は消しても画面は変わらないはずですが、**未確認**です（ビルド時の参照までは調べていません）。

### 10.2 人が書いたコードの内訳

| 区分 | ファイル数 | 行数 | 備考 |
|---|---:|---:|---|
| `lib/`（`.ts`、テスト除く） | 113 | 20,981 | 判定・整形・データ取得の本体 |
| `components/`（`.tsx`） | 26 | 5,866 | 画面と部品 |
| `app/`（`.ts`/`.tsx`、テスト除く） | 38 | 2,867 | URL の札（`page.tsx`）とAPIの入口（`route.ts`） |
| **アプリ本体の小計** | **177** | **29,722** | 2026-09-14 時点は 140ファイル / 20,471行、2026-10-01 時点は 172ファイル / 27,977行 |
| `components/`（`.module.css`） | 24 | 3,784 | 同名 `.tsx` 専用の見た目 |
| `app/globals.css` | 1 | 94 | 全画面の色・フォント（★日本語フォントを外さない★。[CLAUDE.md](../CLAUDE.md) §0-3） |
| ルート直下のコード（`proxy.ts` `instrumentation.ts` `next.config.ts` `prisma7.config.ts` `vitest.config.ts` `eslint.config.mjs`） | 6 | 313 | 設定と横断処理 |
| `scripts/` | 2 | 2,199 | ローカルDBのシード（2,145行）と環境変数チェック |
| `prisma/schema.prisma` | 1 | 952 | ★`prisma db pull` の生成物★。人が書いたものではないが、人が読む |
| **テスト**（`lib/` 88本 ＋ `app/` 16本） | 104 | 29,236 | [章11](#11-テストは何を守っているか) |
| 契約と仕様（`d-watch-api.yaml` / `d-watch-mock.jsx` / `CLAUDE.md`） | 3 | 4,939 | API契約の正 / 画面の正 / 作業ルール |

★**アプリ本体（177ファイル / 29,722行）は、生成物 `lib/generated/prisma/`（73,780行）の約4割です。**★ そしてテスト（29,236行）は**アプリ本体とほぼ同じ量**あります。

つまりこのリポジトリで「人が読むべきコード」は、エディタに見えている量の**ごく一部**です。さらにその中でも、非エンジニアの方が最初に読むべきなのは [章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで) で追った5ファイルだけです。**身構える必要はありません。**

**変えると何が起きるか**: この表の数字は、次に地図を直すときの基準線です。アプリ本体の行数は 9/14 → 10/01 の17日間で約7,500行（＋37%）、10/01 → 10/05 の4日間で約1,700行（＋6%）増えました。増え方が急になったら、どこかで同じ処理が二重に書かれ始めていないかを疑ってください。


---

## 11. テストは何を守っているか

### 11.1 テストとは何か

テストとは、**「この入力を入れたら、この答えが返るはずだ」をコードで書いて、機械に毎回確かめさせる仕組み**です。

たとえば「完了率 90.0% は ◎」を一度テストに書いておけば、誰かが判定の式を書き換えて 90.0% が ○ になった瞬間に、テストが赤く失敗して知らせてくれます。人が画面を目で見て気づくのを待つ必要がありません。

D-Watch では [CLAUDE.md](../CLAUDE.md) §6-2 が「判定ロジック（◎○△×・L1〜L3・KPI計算・丸め）には必ずテストを書く。期待値表を先に作り、それをテストで固定する。目視確認だけで済ませない」と定めています。

**命名の決まり**: テストは、対象のファイルと同じフォルダの `__tests__/` に、同じ名前＋`.test.ts` で置きます。

```
lib/judge/grade.ts              ← 本体
lib/judge/__tests__/grade.test.ts  ← そのテスト
```

例外は2種類だけです。複数のルートをまたいで確かめるもの（`app/api/__tests__/master-filter.test.ts` など）と、同じルートを**サンプルデータで通す版**を分けたもの（`route.fixtures.test.ts`。`app/api/clean-area/__tests__/route.fixtures.test.ts` の冒頭に分けた理由が書かれています）です。

**動かし方**: `npm test`（中身は `vitest run`。`package.json:10`）。GitHub に push すると自動でも走ります（`.github/workflows/ci.yml:45-46`。Lint → テスト → ビルドの順）。

### 11.2 いまの規模（実測）

`npx vitest run` を 2026-10-05 に `develop`（`ceeb736`）で実行した結果です。

```
 Test Files  104 passed (104)
      Tests  2469 passed (2469)
   Duration  8.95s
```

**104ファイル / 2,469件、すべて成功。所要 約9秒です。**（2026-10-01 は 98ファイル / 2,297件。★改訂4 時点（`04f974a`）は 125ファイル / 2,946件★。下の内訳表は 10/05 のまま）

| 場所 | ファイル | 件数 |
|---|---:|---:|
| `lib/` | 88 | 2,164 |
| `app/`（すべて `app/api/` の下） | 16 | 305 |
| `components/` | ★**0**★ | ★**0**★ |

実行中に `"level":"error"` のログが数行流れますが、**失敗ではありません**。「DBが落ちたら 500 を返すか」「同じ期間のサマリが2行あったら止まるか」といった**わざと失敗させる経路**を確かめているテストの出力です。

★**テストは接続先のDBに接続しません。**★ 実DB側の経路を確かめるテストは、Prisma クライアントを偽物に差し替えて動きます（例: `app/api/clean-logs/__tests__/route.test.ts:183-185` の `vi.mock("@/lib/db/use-fixtures", …)` と `vi.mock("@/lib/db/prisma", …)`）。★書き込みのガードテスト（`lib/db/__tests__/robot-error-write-sql.test.ts`）だけは本物の生成クライアントを使いますが、つなぐ先は「送られたSQLを記録するだけの偽アダプタ」で、やはりDBには接続しません★（§8.10）。

**10/01 から増えた6ファイル**（すべて指示書AS・AT）

| ファイル | 件数 | 守っているもの |
|---|---:|---|
| `lib/judge/__tests__/run-completion-rate.test.ts` | 22 | ★1回ごとの完了率の式と平均★（章5-4。指示書AS §4 の表1・表2） |
| `lib/prechecks/__tests__/status-update.test.ts` | 33 | ★対応状況・備考の更新規則★（日時を埋める・消す。#1〜#12） |
| `lib/db/__tests__/robot-error-write-sql.test.ts` | 10（★改訂9 時点 26★） | ★書き込みのSET句が渡した列だけで `updated_at` を含まないこと★（ガードテスト）。★改訂9 から、事前チェックの道は4列以内・エラー一覧の道は5列（4列＋`error_list_id`）以内、★4列版に `error_list_id` を渡すと例外★も確かめる（D-184）★ |
| `lib/logger/__tests__/db-write.test.ts` | 3 | 監査ログからDBのエラーコードだけを取り出すこと |
| `app/api/prechecks/[id]/status/__tests__/route.test.ts` | 27 | 書き込みAPIの入口（422・404・500・監査ログ） |
| `app/api/prechecks/[id]/status/__tests__/route.fixtures.test.ts` | 7 | 同じAPIをサンプルデータで通す版 |

★**改訂4（`f02eb1d` → `04f974a`）で増減したテストファイル**★（`git diff --name-status` で数えた。★全体は 125ファイル / 2,946件・全件成功（`04f974a`。監督の実測）★。ファイルごとの件数は数えていません）

| ファイル | 増減 | 守っているもの |
|---|---|---|
| `lib/prechecks/__tests__/edit-drafts.test.ts` | ★新★ | ★編集モードの下書きと一括保存★（P1〜P8 / D1〜D5 / S1〜S3。D-165） |
| `lib/__tests__/csv.test.ts` | ★新★ | CSV の共通部品（数式インジェクション対策・`csvRate()` の空欄。D-163・D-164） |
| `lib/reports/__tests__/facility-performance-csv.test.ts` | ★新★ | 施設別パフォーマンスの CSV の列・値・ファイル名（D-163・D-164） |
| `lib/reports/__tests__/facility-performance-labels.test.ts` | ★新★ | 「変化」とステータスの表示名（画面と CSV で共有。D-163） |
| `app/api/reports/facility-performance/export/__tests__/route.test.ts` | ★新★ | CSV の口（422・上限・ファイル名・一覧と同じ絞り込み。D-163） |
| `lib/format/__tests__/precheck-status.test.ts` | ★新★ | 1h前ST / 30分前ST のセル（S1〜S5。`null` だけ「-」。D-171） |
| `lib/prechecks/__tests__/status-cycle.test.ts` | ★削除★ | （巡回順の関数ごと削除。D-161） |
| `lib/dashboard/__tests__/aggregate.test.ts` | 追加 | ★事前チェックの異常・有無で本清掃の成功を失敗にしない★（D-170。5.9） |
| `lib/db/__tests__/precheck-mapping.test.ts` ほか | 追加 | ★予定行が無い列は `null`（N1〜N5）・`main_run_succeeded`（M1〜M6）★（D-171） |
| `lib/clean-logs/__tests__/result-pending.test.ts` | ★新（D-173）★ | ★結果待ちの判定 `isResultPending()`★（表1。境界ちょうどは確定＝false・未来の予定は対象外・終了が無ければ開始から） |
| `lib/format/__tests__/clean-result.test.ts`・`lib/clean-logs/__tests__/csv.test.ts`・`query.test.ts`・`lib/dashboard/__tests__/aggregate.test.ts`・`latest-run.test.ts`・`lib/db/__tests__/consecutive-source.test.ts`・`report-mapping.test.ts`・`rescue-mapping.test.ts`・`app/api/dashboard/__tests__/route.test.ts` ほか | 追加 | ★結果待ちの見え方・絞り込み・数え方★（表2 C1〜C11。D-173）。`lib/config/__tests__/dashboard-latest-run.test.ts` は `settleMinutes` の削除に合わせて変更 |
| `lib/dashboard/__tests__/aggregate.test.ts` | 変更（`8a657a2`） | D-170 のテストを「予定はあり異常なし」（`Standby.`）と「予定が無い」（`null`）に分けた（D-171 後の挙動に合わせた） |

★**改訂9（指示書BD。2026-10-27・未コミットの作業ツリー）で増えた・変えたテストファイル**★（件数は 2026-10-27 に `npx vitest run <ファイル>` で数えた。★全体の件数は数えていません★）

| ファイル | 件数 | 守っているもの |
|---|---:|---|
| `lib/errors/__tests__/status-update.test.ts` | ★新★ 37 | ★エラー一覧の更新規則★（`error_list_id` の型・手順1〜4・状態だけの要求には備考必須を掛けない） |
| `lib/errors/__tests__/write-paths.test.ts` | ★新★ 2 | ★D-Sec アラートの保存の本文に `error_list_id` が入らない★ |
| `lib/errors/__tests__/edit-drafts.test.ts` | ★新（指示書BE）★ | 期待値表 E と、その他は備考必須（送らない／ほかの行は送る） |
| `lib/errors/__tests__/error-content-label.test.ts` | ★新（指示書BE）★ | 詳細の列（JP / EN） |
| `lib/errors/__tests__/error-content-picker.test.ts` | ★新（指示書BE）★ | キーワードと分類の AND・大文字小文字・0件・クリア |
| `lib/db/__tests__/error-list-source.test.ts` | ★新★ 25 | ★選択肢は有効な行だけ・id の昇順／名前解決は絞らない（`id IN` だけ・0件なら撃たない）／選べる行の完全一致★ |
| `app/api/error-lists/__tests__/route.test.ts` | ★新★ 5 | `GET /error-lists` の口（Prisma 版の `where` / `orderBy`・500） |
| `app/api/error-lists/__tests__/route.fixtures.test.ts` | ★新★ 2 | 同じ口をサンプルデータで通す版（131件） |
| `app/api/errors/[error_id]/status/__tests__/route.fixtures.test.ts` | ★新★ 5 | `PATCH /errors/{error_id}/status` をサンプルデータで通す版（`error_list_id` が GET に反映される等） |
| `lib/db/__tests__/robot-error-write-sql.test.ts` | 変更（26） | 上の表のとおり（5列版のガード） |
| `app/api/errors/[error_id]/status/__tests__/route.test.ts`・`app/api/errors/__tests__/route.test.ts`・`route.fixtures.test.ts`・`app/api/prechecks/[id]/status/__tests__/route.test.ts`・`lib/db/__tests__/error-report-mapping.test.ts`・`fixture-error-reports.test.ts`・`robot-error-select.test.ts`・`lib/ds-alerts/__tests__/edit.test.ts` | 追加 | 新3項目・422 の各場合・★事前チェックの道は `error_list_id` を捨てる★ |

**`lib/` の内訳**（多い順）

| フォルダ | ファイル | 件数 |
|---|---:|---:|
| `lib/db/` | 19 | 515 |
| `lib/format/` | 12 | 287 |
| `lib/judge/` | 6 | 181 |
| `lib/settings/` | 4 | 122 |
| `lib/clean-logs/` | 3 | 117 |
| `lib/reports/` | 3 | 114 |
| `lib/logger/` | 6 | 93 |
| `lib/fixtures/` | 6 | 91 |
| `lib/prechecks/` | 3 | 88 |
| `lib/facilities/` | 4 | 84 |
| `lib/auth/` | 4 | 62 |
| `lib/masters/` | 2 | 57 |
| `lib/pagination/` | 1 | 56 |
| その他9か所（`config` `api` `clean-area` `schedules` `ds-devices` `ds-alerts` `proxy` `dashboard` と `lib/__tests__/`） | 15 | 297 |

### 11.3 ★`components/` のテストは今も0件★

これは**設定上そうなっています**。

```ts
include: ["lib/**/*.test.ts", "app/**/*.test.ts"],
```
（`vitest.config.ts:14`）

`components/**` が含まれていないので、仮に `components/` にテストを置いても**走りません**。

その代わり、このリポジトリは「**画面の判断を `lib/` に外出しして、そこでテストする**」設計を徹底しています。画面のコンポーネントは `lib/` が返したものを描くだけにする、という分担です。実例が5つあり、どれも冒頭コメントに同じ理由を書いています。

| 判断 | 置き場所 | 描く側 |
|---|---|---|
| ページ送りで何番を描くか | `lib/pagination/page-items.ts`（`:6-8`） | `components/ui/Pagination.tsx` |
| 要レスキューを何件見せるか | `lib/dashboard/rescue-visibility.ts`（`:6`） | `RescueRequiredCard.tsx` |
| 清掃面積の内訳を何件見せるか | `lib/clean-area/visibility.ts`（`:10`） | `CleanAreaScreen.tsx` |
| レポートの4ブロックを1本ずつ出す判定 | `lib/reports/block-visibility.ts`（`:8`） | `ReportsScreen.tsx` |
| 清掃面積の集計 | `lib/clean-area/aggregate.ts`（`:14`） | （API経由で画面へ） |

`lib/pagination/page-items.ts:6-8` の言い方がいちばん直接的です — ★「判断をコンポーネントに置くとテストで固定できない」★。このファイルのテストは、総ページ数 1〜300 を総当たりして「描く番号が1つ残らず実在するページに入っている」ことを機械的に確かめています（`:14-16`）。ユーザーの要件「3ページ目を押したら有りません、みたいなのはだめです」（`:11-13`）をそのまま固定したものです。

**変えると何が起きるか**: 画面の判断を `components/` の中に書くと、テストは1件も増えず、全部通ったまま画面だけが壊れる状態が生まれます。`vitest.config.ts` に `components/**` を足す場合は、React を描画するテスト環境（`environment: "node"` では足りない）の追加が要ります（未着手）。

### 11.4 ★主役は `lib/judge/__tests__/`★

判定ロジックのテストは6本・181件です（10/01 は5本・153件）。

| ファイル | 件数 | 守っているもの |
|---|---:|---|
| `rescue.test.ts` | 60 | 要レスキュー店舗の判定（★R-1 を `departed` で数える「表5（指示書AS）」が増えた★） |
| `grade.test.ts` | 34 | ★◎○△× と表示の丸め★ |
| `advice.test.ts` | 32 | 対応アドバイス（R1〜R4 / B1〜B3）の発火 |
| ★`run-completion-rate.test.ts`★ | 22 | ★1回ごとの完了率（`runCompletionRate()`）と、その平均の判定・表示★（D-155） |
| `kpi.test.ts` | 19 | KPI4指標と、平均の畳み方 `meanCleanRatio()`（★1回ごとの完了率の Σ と件数から平均を出す★） |
| `escalation.test.ts` | 14 | L1〜L3 と連続日数の数え方 |

これらのテストは**期待値表そのものが仕様書**になっています。非エンジニアの方も、このファイルを開けば「何%なら何判定か」を表で読めます。

**`grade.test.ts` の境界値表**（`lib/judge/__tests__/grade.test.ts:19-37`）

★**D-139（2026-09-16）で境界が変わりました。**★ 「境界はすべて上のランクに入る」（90.0 は ◎、70.0 は ○）。ただし ★× の境界だけは「10%未満」のまま★なので、10.0 は △ です。ファイル冒頭（`:12-15`）にその経緯が書かれています。

```ts
const table = [
  { name: "◎の下限すぐ上", rate: 90.1, grade: "◎", display: "90%" },
  { name: "◎/○の境界（90%以上が◎なので90.0は◎。D-139）", rate: 90.0, grade: "◎", display: "90%" },
  { name: "◎の境界のすぐ下", rate: 89.9, grade: "○", display: "90%" },
  { name: "丸めると90だが○", rate: 89.5, grade: "○", display: "90%" },
  { name: "○の下限すぐ上", rate: 70.1, grade: "○", display: "70%" },
  { name: "○/△の境界（70%以上が○なので70.0は○。D-139）", rate: 70.0, grade: "○", display: "70%" },
  { name: "○の境界のすぐ下", rate: 69.9, grade: "△", display: "70%" },
  { name: "△の下限すぐ上", rate: 10.1, grade: "△", display: "10%" },
  { name: "△/×の境界（10%未満が×なので10.0は△。D-139でも変わらない）", rate: 10.0, grade: "△", display: "10%" },
  { name: "×の代表値", rate: 9.9, grade: "×", display: "10%" },
  { name: "ゼロ", rate: 0, grade: "×", display: "0%" },
  { name: "未実施", rate: null, grade: "×", display: "-" },
];
```

表を読むコツは、**`grade`（判定）と `display`（画面の表示）が別の列になっている**ことです。89.5 は画面に「90%」と出ますが、判定は ○ です。判定は丸める前の生値で行うからです（[CLAUDE.md](../CLAUDE.md) §4.3）。

この表は `judgeGrade()` と `displayRate()` の**両方**に流し込まれます（`:40` と `:61`）。表の1行が、判定と表示の2つを同時に守っています。また ★`docs/fixtures.md` §2.1 と `lib/fixtures/clean-logs.ts` の境界値ケースと同じ値★です（`:15-17`）。画面で見える判定とテストで通している判定が一致していることを、目でも確かめられます。

**変えると何が起きるか**: この表を書き換えると、判定の仕様そのものが変わります。表を変えたくなったら、先に [CLAUDE.md](../CLAUDE.md) §4.1 を確認してください（`:17`）。境界値は TeamWorks との合意事項で、[CLAUDE.md](../CLAUDE.md) §0-2 が「期待値表の確定は分担しない」と定めています。

### 11.5 ★「間違ったやり方」を、通るテストとして記録している★

このリポジトリには、**わざと間違ったやり方をして、その間違った結果を「正しく」確かめる**テストがあります。名前に「アンチパターンの記録」と付いていて、2本あります。

**① 丸めてから判定すると判定が変わる**（`lib/judge/__tests__/grade.test.ts:44-57`）

```ts
it("丸めてから判定すると判定が変わることを確認（アンチパターンの記録）", () => {
  expect(judgeGrade(89.5)).toBe("○");             // ✅ 生値で判定
  expect(judgeGrade(Math.round(89.5))).toBe("◎"); // ❌ 丸めると90になり ◎ に化ける

  expect(judgeGrade(69.5)).toBe("△");             // ✅ 生値で判定
  expect(judgeGrade(Math.round(69.5))).toBe("○"); // ❌ 丸めると70になり ○ に化ける
});
```

89.5% の清掃は本当は ○ です。でも先に「90」に丸めてから判定すると ◎ になってしまう。テストはその**間違った結果（◎）が出ることを確認して通ります**。

なぜこんなテストを書くのか。「やってはいけないこと」を文章で書いても読み飛ばされますが、**実際に動くコードで「こうすると、こう壊れる」を見せると誰でも納得できる**からです。後から来た人が「表示も判定も同じ数字でいいのでは」と丸めを1箇所にまとめたくなったとき、このテストが理由を示します。

★D-139 で事故の向きが逆になった★こともコメントに残っています（`:45-48`）。旧規則では 90.4 を丸めると ◎ が「消えた」のに対し、新規則では 90 も ◎ なので消えません。いまは ★「無い ◎ が丸めで生まれる」側が事故★です。それでも「表示用に丸めた値で判定しない」という原則は変わりません。

> ★注記★: [CLAUDE.md](../CLAUDE.md) §4.3 のコード例のコメントは、第3版（10/01）の時点では「90.4%が丸めで90%になり、◎判定が消える」（D-139 以前の規則での説明）のままでしたが、★指示書AR で「89.5%が丸めで90%になり、本当は○なのに◎になる」に直りました★（2026-10-05 確認）。`grade.test.ts:49-50` は `judgeGrade(Math.round(90.4))` を `"◎"` と固定しています。

**② カレンダー日で数えると L3 が L1 に化ける**（`lib/judge/__tests__/escalation.test.ts:72-83`）

月・水・金に清掃がある施設で、3回続けて失敗したとします。連続日数は「スケジュールがある日」で数えるので **3日＝L3** です（`:55-70`）。ところが、清掃の予定が無い火・木を「問題なし」として混ぜてしまうと、連続が途切れて **L1** になります。テストはその間違った結果（L1）を確かめて通ります。

```ts
const wrong = [
  { date: "2026-08-03", isProblem: true },
  { date: "2026-08-04", isProblem: false }, // ← 本来含めてはいけない
  { date: "2026-08-05", isProblem: true },
  { date: "2026-08-06", isProblem: false }, // ← 本来含めてはいけない
  { date: "2026-08-07", isProblem: true },
];
expect(judgeEscalation(wrong).level).toBe("L1");
```

L3（オンサイト訪問を検討）が L1（現場へ連絡）に化けるのは、運用上もっとも危ない間違いの1つです。しかも**例外もエラーも出ません**。だから「こうすると壊れる」を動く形で残してあります。

### 11.6 件数の多いテスト（上位15本）

| # | ファイル | 件数 | 守っているもの |
|---|---|---:|---|
| 1 | `lib/db/__tests__/report-mapping.test.ts` | 167 | レポートの組み立て。年次・月次・週次の判別、重複検知 |
| 2 | `lib/db/__tests__/clean-log-mapping.test.ts` | 120 | 清掃ログ1件の組み立て。未実施＝レポート行なし、★1回ごとの完了率・`task_area = 0`★、連続日数 |
| 3 | `lib/format/__tests__/period.test.ts` | 108 | ★月跨ぎ週の帰属★（週の長さ1〜7日、第6週） |
| 4 | `lib/reports/__tests__/query.test.ts` | 86 | レポート4本のパラメータ検査 |
| 5 | `app/api/reports/__tests__/route.test.ts` | 73 | レポートAPIの入口（422・500・形） |
| 6 | `lib/format/__tests__/search-text.test.ts` | 65 | 施設名検索の記号ゆれ |
| 7 | `lib/judge/__tests__/rescue.test.ts` | 60 | 要レスキュー店舗の判定 |
| 8 | `lib/pagination/__tests__/page-items.test.ts` | 56 | ページ送りの番号（1〜300ページ総当たり） |
| 9 | `lib/settings/__tests__/display-limits.test.ts` | 56 | 表示上限の保存と既定値への倒れ方 |
| 10 | `lib/facilities/__tests__/query.test.ts` | 55 | 施設マスタのパラメータ検査 |
| 11 | `lib/logger/__tests__/redact.test.ts` | 53 | ★ログに出してはいけない項目の除去★ |
| 12 | `lib/prechecks/__tests__/query.test.ts` | 48 | アラートのパラメータ検査・並べ替え・件数バッジ（★対応状況を変えたときの差分反映 `applyResolutionChange()` を含む★） |
| 13 | `lib/schedules/__tests__/query.test.ts` | 42 | スケジュールのパラメータ検査 |
| 14 | `app/api/__tests__/master-filter.test.ts` | 40 | 分類（ホールディングス／顧客）の絞り込みを全ルート横断で確認 |
| 15 | `lib/clean-logs/__tests__/areas.test.ts` | 39 | エリア別完了率の絞り込みと全体率（★15位は39件で4本が並ぶ★ — ほかに `lib/clean-logs/__tests__/csv.test.ts` / `lib/clean-logs/__tests__/query.test.ts` / `lib/settings/__tests__/advice.test.ts`） |
| — | その他 89本 | 1,401 | — |

`lib/judge/__tests__/grade.test.ts`（34件）は件数では20位ですが、**最初に読むべきテスト**です（§11.4）。

**変えると何が起きるか**: テストが通っているからといって、画面が正しいとは限りません。テストが守っているのは `lib/` と `app/api/` だけで、`components/` は0件です（§11.3）。**全テストが通っていても、サンプルデータ（`USE_FIXTURES=true`）で画面を開いて確かめてください。** 境界値のケースは [docs/fixtures.md](fixtures.md) が一覧にしています。


---

## 12. よくある変更と、触る場所

> 「この数字はどこから来ているか」を知りたいだけなら、変更の前に[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)を見てください。

「やりたいこと」から引く索引です。**★印は事故が起きやすい変更**なので、該当章を読んでから手を付けてください。

> 行番号は 2026-10-05 時点（`ceeb736`）のものです。ズレていたら定数名で検索してください。

### コードを触らずにできること

> 環境変数・保存ファイルの詳細は[章14](#14-設定ファイルと保存ファイル--アプリの外側の決めごと)を参照。

| やりたいこと | やり方 |
|---|---|
| 連続日数の表示上限を変える | **システム管理画面から編集。**`data/settings/display-limits.json` に保存されます。★画面から編集できる設定はこれ1つだけです★ |
| D-S制御盤の備考を書く | **D-Secログ画面で「編集」を押し、備考を入力して「保存（N件）」を押す。**★押すまでは送られません★（D-183）。200文字まで。`data/settings/ds-device-notes.json` に保存 |
| ★アラート・エラーの対応状況・備考を記録する★ | **アラート画面（`/alerts`）またはエラー一覧（`/errors`）で「編集」を押し、対応状況をプルダウンで選ぶ／備考を入力して、「保存（N件）」を押す。**★押すまでは送られません★（D-165）。備考は400文字まで。★保存先はメインDB（`robot_error_reports` の4列）★です（章3-5・3-5b・章8-12）。アラートの出発失敗の行には記録できません |
| 表示をサンプルデータ⇄DBで切り替える | `.env.local` の `USE_FIXTURES`（★`"false"` という綴りに完全一致しないとDB側に切り替わりません★。章8） |
| ログの量を増やす／減らす | `.env.local` の `LOG_LEVEL`（章9） |
| 日本語⇄英語を切り替える | 画面右上の切り替え。★ブラウザに保存され、再読み込み・再ログインのあとも保たれます★（localStorage。D-183。ストレージが使えないブラウザでは保たれません） |
| ダーク⇄ライトを切り替える | 同上（★保存されます★） |

アドバイスの文言は、9/14 版では「システム管理画面から編集」と書いていましたが、**画面の編集機能はまだありません**（API だけ存在します）。いまは表示のみです。

### 1行だけ直せばよいこと

| やりたいこと | 触る場所 |
|---|---|
| ◎の基準を90%→95%にする | `lib/config/index.ts:20` の `excellent`。★**「以上」で比べる**ので、95.0 が ◎ になります★（D-139） |
| L2 を2日→3日連続にする | `lib/config/index.ts:46` の `level2Days` |
| 清掃詳細ログの期間上限を変える | `lib/config/index.ts:444` の `MAX_DATE_RANGE_DAYS`。★**現在は 731日（約2年）**★。以前は31日でした |
| 一覧の1ページ件数を増やす | `lib/config/index.ts:410-411`（既定50件・上限200件） |
| PUDU MT1 に対応する | `lib/config/index.ts:729` の `supported` を `false` → `true` |
| 「点検記録」をサイドバーに戻す | `components/layout/AppShell.tsx:22` のコメントアウトを外す。★ただし画面の中身はまだ「準備中」です★（章3。D-150 で出すことは決定済み、実装は未着手） |
| B3（オフライン検知）を出るようにする | `lib/config/robot-status-match.ts:46` に該当する状態文字列を足す。★現在は空なので B3 は出ません★ |
| 空欄の表示を「(値なし)」から変える | `lib/format/raw-label.ts:19` |
| ★結果待ちの長さ（予定の終了から何分は失敗に数えないか）を変える★ | `lib/config/index.ts:207` の `RESULT_WAIT_MINUTES`（★現在 120★。D-173）。★詳細ログ・CSV・ダッシュボード・アラート・連続日数・要レスキュー・当期の率が一斉に変わる★（消費者は `isResultPending()` だけ）。★`LAUNCH_GRACE_MINUTES` を代わりに変えても何も起きない★ |

### 2箇所以上を揃えないといけないこと ★

| やりたいこと | 触る場所 | 片方だけ直すとどうなるか |
|---|---|---|
| ★**色を変える** | `lib/theme.ts:4-43` **と** `app/globals.css:1-40` の**両方** | 画面の一部だけ色が変わる。エラーは出ない（章7） |
| ★**画面の日本語を変える** | ①その画面の `.tsx` の文字列を直す ②`lib/i18n.ts` の辞書の**キー側**も同じ文字列に直す | ②を忘れると英語表示のときだけ日本語のまま出る（章7） |
| ★**レポートの既定表示件数を変える** | `lib/reports/query.ts:85` **と** `components/features/reports/ReportsScreen.tsx:46`（どちらも `DEFAULT_LIMIT = 10`） | サーバーと画面で件数がズレる |
| 事前チェックのタイミングを変える | `lib/config/index.ts:257`（`a961829`）の `PRECHECK_MINUTES_BEFORE` | — （★清掃詳細ログの列には影響しない★。列は DB の分数の順で決まる。D-178） |
| ★**アラートの備考の文字数上限を変える** | `lib/config/index.ts:601` の `PRECHECK_NOTE_MAX_LENGTH` **と** `d-watch-api.yaml:789` の `maxLength: 400`（★`/errors` 側は `:918`★。`f2b91fc`） | 画面・サーバーと API 契約が食い違う |
| 清掃結果の表示名を変える | `lib/config/index.ts:373-380` の `CLEAN_TASK_STATUS_LABELS` **と** `lib/i18n.ts:221-226` のキー | ★表示名は TeamWorks の回答（第10回 N-2）の原文ママ★なので、原則として変えない。変えるなら英語に切り替わらなくなるのを防ぐため両方直す |
| ★**完了率の式（上限・桁）を変える** | `lib/config/index.ts:36-41` の `RUN_COMPLETION_RATE`（式そのものは `lib/judge/kpi.ts:166-175`）**と** 期待値表 `lib/judge/__tests__/run-completion-rate.test.ts` | ★TeamWorks のサマリの式に合わせた値（D-155）★なので、アプリ側だけ変えると前期比が「別の式どうしの差」になる。★先に期待値表、それから実装★（章5-11） |
| ★**CSV の数式対策・空欄の出し方を変える** | `lib/csv.ts`（`csvCell()` / `csvRate()` / `csvDocument()`）の1か所 | ★清掃詳細ログとレポートの2つの CSV が同じ関門を通る★（D-163）。CSV ごとに書き直すと片方だけ直して抜ける。★`csvCell()` を通さないセルを作らない★（D-125） |
| ★**レポートの施設別パフォーマンスの絞り込み・並びを変える** | `lib/reports/facility-performance-load.ts` の `loadFacilityPerformance()`（★表と CSV の両方が呼ぶ1本★）、画面のクエリは `ReportsScreen.tsx` の `facilityFilterQuery` | ★表と CSV で施設が食い違う★（例外もテスト失敗も出ない。D-163） |
| ★**「変化」・ステータスの表示名を変える** | `lib/reports/facility-performance-labels.ts`（画面と CSV で共有）**と** `lib/i18n.ts` のキー | 片方だけだと英語表示か CSV のどちらかがずれる |
| ★**分類プルダウンの幅（180px）を変える** | 7画面の `.module.css` の `.masterSelect`（[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅) ①） | その画面だけ並びがずれる |
| ★**表の列を足す・幅を変える** | その表の `<colgroup>` の `<col>` **と** `.module.css` の列幅・`.table` の `min-width` | 別の列に幅が付く・横スクロールの境目がずれる（[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅) ③） |
| ★**「本清掃は成功」の定義を変える** | `lib/dashboard/aggregate.ts` の `SUCCESS_GRADES`（★ダッシュボードの「成功」とアラートの `main_run_succeeded` が共有★） | ダッシュボードとアラートで「成功」の意味が食い違う（D-171） |
| API の入出力を変える | **先に `d-watch-api.yaml` を直してから**実装（[CLAUDE.md](../CLAUDE.md) §6-1） | 契約と実装が乖離する |

**★`lib/i18n.ts` を直しても日本語は変わりません。**あの辞書は「日本語→英語」の対訳表で、
日本語表示のときは辞書を見ずに書かれた文字列をそのまま出すためです（章7）。

### デプロイが必要なこと ★

`.env.local` を直しても変わりません。**コードを直してデプロイし直す必要があります。**

| やりたいこと | 触る場所 |
|---|---|
| ★アクセスできるIPアドレスを限定する | `lib/config/allowed-ips.ts:24` の `ALLOWED_IPS` に足す **と** `:35` の `ALLOW_ALL_WHEN_EMPTY` を `false` に。★**現在は両方とも未設定なので、IP制限は事実上効いていません**★ |
| ★プロキシの段数が変わった | `lib/config/allowed-ips.ts:63` の `TRUSTED_PROXY_HOPS` |

### 新しく作るとき — 真似すべき見本

| 作りたいもの | 見本にするファイル |
|---|---|
| 新しい一覧API | `app/api/clean-logs/route.ts`（コード内に「これが動く見本です」と明記されています） |
| 新しい画面 | `app/(dashboard)/detail-log/page.tsx` ＋ `components/features/detail-log/DetailLogScreen.tsx` |
| 新しいクエリ層 | `lib/clean-logs/query.ts`（クエリ層はすべて同じ形。章6-B） |
| 新しい共通UI部品 | `components/ui/GradeBadge.tsx`（`.tsx` ＋ 同名 `.module.css` の2点セット） |
| 新しい判定ロジック | `lib/judge/grade.ts` ＋ **先に** `lib/judge/__tests__/grade.test.ts` の期待値表 |

### やってはいけないこと

[CLAUDE.md](../CLAUDE.md) §7 が正ですが、非エンジニアが踏みやすいものを抜き出します。

| ❌ やってはいけない | なぜ |
|---|---|
| ★**許された列以外をメインDBに書く／`robot-error-write.ts` を通さずに書く**★ | 書いてよいのは `robot_error_reports` / `control_error_histories` の `status` / `comment` / `responded_at` / `resolved_at` と、★`robot_error_reports.error_list_id`（エラー一覧の PATCH だけ。D-184）★だけです（[CLAUDE.md](../CLAUDE.md) §3・§7）。★事前チェック（`/prechecks`）と D-Sec の経路からは `error_list_id` も書かない★（4列のまま）。それ以外の列は権限エラーになります。★書き込みは `lib/db/robot-error-write.ts` の1ファイルに閉じ込めてあります★（章8-10）。現在の接続先はステージングで（D-152）、★本番DBへの接続・書き込みはまだありません★ |
| ★**`RobotErrorReport.updated_at` に `@updatedAt` を付け直す**★ | db pull の後に `@updatedAt` を付け直す運用（D-059）の★唯一の例外★です。付けると Prisma が `updated_at` を勝手に SET し、権限エラー（または UTC の値の混入）になります。ガードテストが赤くなります（章8-8・章8-10） |
| ★`scripts/seed-local-db.ts` をステージングに向ける★ | テーブルを消してから入れ直すので、★実データが消えます★。`lib/db/local-guard.ts` が `dwatch_local` 以外を拒否します |
| `prisma/schema.prisma` を手で書き換える | TeamWorks が管理するDBの設計図。`prisma db pull` で取り込むだけ。手で直すと次の取り込みで消える。★例外は `ErrorList` と `error_list_id` の手書き（db pull できないため。D-184。接続回復後に db pull で置き換える。章8-8）★ |
| `lib/generated/prisma/` の中身を直す | 自動生成物。次の生成で消える |
| 閾値を画面や判定ロジックに直接書く | `lib/config/` に外出しする決まり（章6） |
| ★**表示用に丸めた数字で判定する**★ | `89.5` は画面に `90%` と出ますが判定は `○` です。★丸めた値で判定すると、**存在しない ◎ が生まれます**★（章5）。9/14 版は「◎が消える」と逆向きに書いていましたが、境界が「以上」に変わったため（D-139）事故の向きも逆になりました |
| ★**完了率を期間の合計どうしの面積で割る／画面ごとに完了率を計算する**★ | 清掃完了率は ★1回ごとの完了率（`runCompletionRate()`）の単純平均★です（D-155。★D-158 で確定★）。★Σ清掃面積 ÷ Σ対象面積（期間の合計どうし）は D-151 で廃止したまま★です。★1回ごとの値は `runCompletionRate()`、平均は `meanCleanRatio()` だけが知っています★（`lib/judge/kpi.ts`）。`clean_task_reports.clean_ratio` 列を完了率に使うのも、いまは誤りです（章5-4） |
| ★**未実施を `completion_rate === null` で数える**★ | 2026-10-05（P-5=B）からは `task_area = 0` も 0% になり、`null` は未実施だけになりました。それでも★未実施は `departed`（レポート行の有無）で数えます★（D-156 項1 / 指示書AU D3。章5-4）。完了率の扱いがまた変わったときに、出発失敗の数え方まで一緒に変わらないようにするためです |
| ★**事前チェックの結果・有無で成功／失敗を変える**★ | ★事前チェックは成功・失敗・未実施の判定に使いません★（D-170 / D-048）。判定は本清掃のレポート行の有無（`departed`）と完了率（`grade`）だけです。1h前ST / 30分前ST・アラートの事前チェック異常・`main_run_succeeded` は★表示だけ★（`main_run_succeeded` で対応状況や件数を変えない。D-171）。歯止めのテストは `lib/dashboard/__tests__/aggregate.test.ts`（章5-9） |
| ★**結果待ちの旗を見ずに「失敗」「未実施」を数える**★ | 結果待ちの行は `departed: false`・`grade: "×"` のままです（D-173）。★新しい集計・絞り込みで `result_pending`（または `isResultPending()`）を見ないと、レポートがまだ届いていないだけの清掃が失敗に混ざります★（例外もテスト失敗も出ない。章5-9b） |
| `facilities.contract` を数値として比較する | 10桁の**文字列**です。数値にすると全施設が「契約なし」になりますが、**エラーもテスト失敗も出ません**（章7） |
| ★`clean_schedules.contract` を読む★ | ★`contract` という列は2つあります★。正は `facilities.contract` だけ。型も既定値も同じなので、**取り違えても型エラーになりません**（章7） |
| `clean_status_histories` を読む | 清掃実績は `clean_task_reports` に一本化されました（D-151。章8） |
| `git add -A` を使う | 別作業の途中ファイルを巻き込む事故が過去に起きています。ファイルパスを明示してください |

### 変更したあとに必ずやること

```bash
npm test
```

判定ロジック（`lib/judge/`）を触った場合は特に必須です。
テストが落ちたら「壊した」か「仕様を変えた」かのどちらかで、後者なら期待値表も直します（章11）。

```bash
npm run check:env
```

環境変数を触った場合、およびデプロイ前（章9）。


---

## 13. 用語対応表

> 完了率・稼働率・面積などの**数字の用語**は、計算式と根拠まで含めて[章5-B](#5-b-画面に出る数字の出どころ--テーブル列式根拠)に詳しく書いています。この章は名前の対応だけです。

画面に出る日本語から、コードの名前・DBのカラム名を引くための辞書です。
「コードの中の名前」は API のキー名（snake_case）と型名（`lib/types/index.ts`）を挙げています。

- 用語の**意味**の説明は `CLAUDE.md:380`（§8 用語）と [getting-started.md](getting-started.md)（§7 用語のメモ）にあります。ここでは重複させず、**名前の対応**だけを載せます。
- 実物のカラム名・キー名が確認できないものは「該当なし」または「未確認」と書いています。
- ★行番号は 2026-10-05（`ceeb736`）に全件引き直しました★（第3版のこの章は、それ以前の版の行番号が残っていました）。

### 13.1 主要な対応表

| 画面に出る日本語 | コードの中の名前 | DBのカラム名 | 補足 |
|---|---|---|---|
| 施設 / 施設名 | `Facility` / `id` / `facility_name`（`lib/types/index.ts:1138-1140`）、清掃ログでは `facility_id` / `facility_name`（`:226-227`）、i18n キー「施設名」（`lib/i18n.ts:24`） | `facilities.id` / `facilities.facility_name`（`prisma/schema.prisma:125,131`） | 旧称「店舗」。EN は "Facility" |
| 清掃エリア | `section_name`（`lib/types/index.ts:358`、`:1082`） | `sections.section_name`（`prisma/schema.prisma:173`） | 旧称「区画」。P1は1清掃エリア=1ロボット |
| ロボット | `RobotSummary` / `serial_number` / `model` / `maker`（`lib/types/index.ts:1131-1136`） | `robots.serial_number`（`prisma/schema.prisma:233`）、機種名は `devices.model_name`（`:86`） | 画面の「シリアル」「機種」がこれ |
| 清掃予定（スケジュール） | `Schedule` / `weekdays` / `start_time` / `end_time` / `active`（`lib/types/index.ts:1178-1190`） | `clean_schedules.weeks` / `.start_time` / `.end_time`（`prisma/schema.prisma:564,566,569`）、1回ごとの予定は `clean_timetables.start_at` / `.end_at`（`:680-681`） | `weeks` は7桁文字列。`end_time` は24〜29時を取りうる |
| 完了率（清掃完了率） | `completion_rate`（清掃ログ `lib/types/index.ts:316`）。★1回ごとの値は `runCompletionRate()`（`lib/judge/kpi.ts:166`）、平均は `meanCleanRatio()`（`:122`）★ | ★`clean_task_reports.clean_area` / `.task_area`（`prisma/schema.prisma:534,533`）から計算★。確定期間の集計は `facility_summaries.clean_ratio`（`:743`） | ★**1回ごとの完了率（`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て）の単純平均**★（D-155。★第11回 P-2=A で確定。D-158★。`task_area = 0` は 0%）。★`clean_task_reports.clean_ratio`（`:539`）は使っていない★（先方が式の訂正中。訂正後も同じ値になる＝第11回 P-1）。予定外のタスクは入れない。APIは丸める前の生値 |
| 清掃結果 | `task_status`（`lib/types/index.ts:295`）、表示名は `formatCleanTaskStatus()`（`lib/format/clean-result.ts:61`。`a961829`。★結果待ちは `CLEAN_RESULT_PENDING_LABEL`「結果待ち」 `:73`★） | `clean_task_reports.status`（`prisma/schema.prisma:529`） | ★0 清掃開始前〜5 清掃異常終了の表示名★（`CLEAN_TASK_STATUS_LABELS`。D-155 項2）。表に無い値は数字のまま、レポート行が無ければ「出発失敗」。★判定には使わない★ |
| ★結果待ち★（D-173） | `result_pending`（`lib/types/index.ts:307`。`a961829`）、ダッシュボードの件数は `DashboardResponse.summary.pending`（`:1308`） | 該当なし（`clean_timetables.start_at` / `end_at` とレポート行の有無から `isResultPending()` で計算） | ★予定の終了から120分以内でレポートがまだ無い清掃★。`departed` / `grade` は出発失敗と同じ値のまま（旗で区別する）。[5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173) |
| 稼働率 | `operation_rate`（`lib/types/index.ts:788`、`:854`）、関数 `operationRate()`（`lib/judge/kpi.ts:36`） | `facility_summaries.operation_ratio`（`prisma/schema.prisma:741`）。予定回数は `task_count`（`:728`）。★正常終了の回数の列は無い★ | ★正常終了（status 4）の回数 ÷ 予定回数★（第11回 P-6=A）。EN は "Uptime"（`lib/i18n.ts:91`） |
| 出発成功率 | `launch_rate`（`lib/types/index.ts:789`、`:855`）、関数 `launchRate()`（`lib/judge/kpi.ts:44`） | `facility_summaries.launch_ratio`（`prisma/schema.prisma:742`）。分子は `launch_count`（`:729`） | ★開始できた回数 ÷ 清掃**予定**回数（時刻の条件なし）★ |
| エラー発生率 | `error_rate`（`lib/types/index.ts:791`、`:857`）、関数 `errorRate()`（`lib/judge/kpi.ts:182`） | `facility_summaries.error_ratio`（`prisma/schema.prisma:744`）。分子は `error_count`（`:730`） | ★分母は清掃**予定**回数★（第11回 P-6=B。旧: 実施回数）。上限なし |
| 判定（◎○△×） | `grade` / 型 `Grade`（`lib/types/index.ts:318`、`:17`）、関数 `judgeGrade()`（`lib/judge/grade.ts:29`） | **該当なし**（DBに保存されない。サーバが毎回計算する） | 閾値は `lib/config/index.ts:18`（`GRADE_THRESHOLDS`） |
| エスカレーションレベル（L1/L2/L3） | `escalation_level` / 型 `EscalationLevel`（`lib/types/index.ts:333`、`:18`）、`consecutive_problem_days`（`:323`） | **該当なし**（DBに保存されない。計算値） | 閾値は `lib/config/index.ts:43`（`ESCALATION_THRESHOLDS`） |
| 事前チェック | `PrecheckItem` / `precheck_minutes` / 型 `PrecheckSeverity`（`lib/types/index.ts:542`、`:561`、`:528`） | `clean_timetables.precheck`（`prisma/schema.prisma:604`） | 現行DDLは 0=本清掃 / 1=1回目 / 2=2回目 のコード値。分数への変換は `lib/db/precheck-mapping.ts` |
| 事前チェック時のロボット状態 | `status_1h` / `status_30m`（`lib/types/index.ts:262-263`）、`robot_status`（`:537`） | ★**未確認**★ — `robots.status`（`prisma/schema.prisma:235`）は動作フェーズのみで、ハンドルアップ等を表現していない（D-077 項3） | 現行の日本語6値は fixtures 由来の暫定値。★`f2b91fc` では `status_1h` は `lib/types/index.ts:251`、型は `string \| null`（`null` ＝ その分数の事前チェックの予定が無い。D-171）★ |
| D-Sデバイス（D-S制御盤） | `DsDevice` / `serial_number` / `connection_status`（`lib/types/index.ts:1043-1047`） | `controls.serial_number` / `controls.status`（`prisma/schema.prisma:204,211`） | `status` は 0=正常 / 1=異常 の2値（D-065） |
| 契約フラグ | `monitoring_enabled`（D-Watch）/ `d_security_enabled`（D-Security）（`lib/types/index.ts:1012,1017`）、`parseContract()`（`lib/format/contract.ts:26`） | `facilities.contract`（`prisma/schema.prisma:141`） | ★varchar(10) の文字列★。1桁目=D-Security / 2桁目=D-Watch（`lib/config/index.ts:751`）。★数値比較禁止★ |
| アドバイス | `AdviceItem` / `advices` / `rule`（R1〜R4・B1〜B3）（`lib/types/index.ts:338-346`、`:465`）、`matchAdviceRules()`（`lib/judge/advice.ts:59`） | **該当なし**（メインDBには保存しない。文言は `data/settings/advice.json`。`lib/settings/advice.ts:45`） | 発火条件は仕様のため編集不可（`lib/types/index.ts:481`） |
| 対応状態（未解消/対応中/解消） | `resolution_status` / 型 `PrecheckStatus`（`lib/types/index.ts:572`、`:529`）。★更新の注文は `PrecheckStatusUpdateRequest`（`:604`）★ | `robot_error_reports.status`（`prisma/schema.prisma:332`）。★D-Sec アラートも `robot_error_reports.status`（エラー一覧と同じ。D-180 項6）★ | ★この `status`, `comment`, `responded_at`, `resolved_at` の4カラムだけが書き込み可（`CLAUDE.md:134`）★（★`robot_error_reports` だけは、エラー一覧の PATCH の経路で5列目の `error_list_id` も書く。D-184★）。★2026-10-02 からアラート画面で書き込める★（`robot_error_reports` のみ。章8-10）。0/1/2 との対応は `lib/db/precheck-mapping.ts:38` / `:58`（`f2b91fc`） |
| 備考（アラート） | `note`（`lib/types/index.ts:544`） | `robot_error_reports.comment`（`prisma/schema.prisma:333`） | ★メインDBに保存する★（アラート画面・エラー一覧の編集モードから書ける。400文字まで）。API の注文でも `note`（旧名 `memo` から改名。指示書AT） |
| ★本清掃は成功（アラート）★ | `main_run_succeeded`（`lib/types/index.ts:594`。`a961829`） | 該当なし（親の本清掃の `clean_task_reports` から計算） | ★表示だけ★。true のカードは控えめに出て「本清掃は成功」の Pill が付く。対応状況・件数は変えない（D-171） |
| エラー種別 | `type` / `label`（`lib/types/index.ts:842-843`） | `robot_error_reports.error_type`（`prisma/schema.prisma:330`） | ★生値をそのまま表示する。7分類への変換はしない★（D-077） |
| 備考（D-Secログ） | `note`（`lib/types/index.ts:1050`） | **該当なし**（`data/settings/ds-device-notes.json`。`lib/settings/ds-device-notes.ts:54`） | メインDBには保存しない |
| バッテリー残量 | `battery_level`（`lib/types/index.ts:585`） | `robots.battery`（`prisma/schema.prisma:242`） | ★実DB経由では常に null★（現在値のみで履歴が無い。D-090 項8） |

### 13.2 CSVエクスポートの列名（★運用の会話に最も出る★）

★**CSV は2種類あります**★（改訂4 で1つ増えた。D-163）。★どちらも共通部品 `lib/csv.ts` を通ります★ — `csvDocument()`（`:88`。UTF-8 BOM付き・改行 CRLF・最後の行も CRLF）が★全セルを `csvCell()`（`:50`）に通し★、`=` `+` `-` `@`（とタブ・CR）で始まる値の先頭に `'` を付けて無害化します（表計算ソフトで数式として実行されるのを防ぐ。D-125）。★率の列は `csvRate()`（`:69`）で、値が無ければ空のセル★（D-164。旧: `-` を出し、無害化で `'-` になって Excel に見えていた）。★**見出しも中身も日本語固定**★です（CSV は画面の言語を受け取らないため。指示書AS D11）。行数の上限はどちらも `MAX_CSV_ROWS`（20,000。超えたら 422）。
★`csvCell()` は以前 `lib/clean-logs/csv.ts` にありました★（D-163 で `lib/csv.ts` へ移動。`lib/clean-logs/csv.ts:19` から再エクスポートしているので、古い import 先も動きます）。

#### ① 清掃詳細ログ — `GET /api/clean-logs/export`

組み立ては `lib/clean-logs/csv.ts` の `buildCleanLogCsv()`（`:62-95`）、ヘッダは `CLEAN_LOG_CSV_HEADER`（`:22-40`）です（`a961829`）。★1行の見え方（出発・清掃結果・完了率・判定）は画面と同じ `cleanLogRowView()` で決めます★（D-173）。ファイル名は `clean-logs_{開始日}_{終了日}.csv`（`app/api/clean-logs/export/route.ts:110`）。

| CSVの列名 | コードの中の名前（`csv.ts` の行） | DBのカラム名 |
|---|---|---|
| 施設名 | `facility_name`（`:65`） | `facilities.facility_name`（`prisma/schema.prisma:131`） |
| 機種 | `model_name`（`:66`） | `devices.model_name`（`prisma/schema.prisma:85`） |
| シリアル | `serial_number`（`:67`） | `robots.serial_number`（`prisma/schema.prisma:233`） |
| 日付 | `scheduled_at` を `formatMd()` で整形（`:68`） | `clean_timetables.start_at`（`prisma/schema.prisma:597`） |
| 予定時刻 | `scheduled_at` を `formatHm()` で整形（`:69`） | 同上 |
| 1h前ST | `status_1h`（`:72`）。★`null`（事前チェックの予定が無い）は空のセル★（D-171。画面の「-」は入れない — 無害化で `'-` になるため） | ★未確認★（13.1 の「事前チェック時のロボット状態」参照） |
| 30分前ST | `status_30m`（`:73`。`null` は空のセル） | ★未確認★（同上） |
| 出発 | `departed`（真偽値 → 「出発」/「未出発」。★結果待ちは「結果待ち」（D-173）★。`:74-78`） | 該当なし（`clean_task_reports` の行の有無から判定） |
| ★清掃結果★ | `task_status` を `formatCleanTaskStatus()` で表示名に（★結果待ちは「結果待ち」★。`:79`） | `clean_task_reports.status`（`prisma/schema.prisma:529`）。★例: `清掃正常終了`。表に無い値は数字、行が無ければ `出発失敗`★ |
| 完了率 | `completion_rate` を `csvRate()` で整形（`:80`。★未実施・結果待ちは空のセル★） | ★`clean_task_reports.clean_area` / `.task_area` から `runCompletionRate()` で計算★（`prisma/schema.prisma:534,533`）。`clean_ratio` 列ではない |
| 判定 | `grade`（`:81`。★結果待ちは空のセル★） | 該当なし（計算値） |

★注意★ CSV には `escalation_level`（エスカレーションレベル）と `consecutive_problem_days`（連続問題日数）は
**含まれていません**（`csv.ts:22-40` にその列がない）。画面でしか見られません。

#### ② レポートの施設別パフォーマンス — `GET /api/reports/facility-performance/export`（★改訂4。D-163★）

組み立ては `lib/reports/facility-performance-csv.ts` の `buildFacilityPerformanceCsv()`（`:56`）、ヘッダは `facilityPerformanceCsvHeader()`（`:29`）、ファイル名は `facilityPerformanceCsvFilename()`（`:87`）です。★列は画面の表の9列と同じ順に、施設名の直後へシリアルを足したもの★。行は★表と同じ条件の全施設★（`limit` は無視）・★表と同じ稼働率ワースト順★です（5B-7）。

| CSVの列名 | 中身 | 出どころ |
|---|---|---|
| 施設名 | `facility_name` | `facilities.facility_name` |
| シリアル | `serial_number` | `robots.serial_number` |
| 稼働率 / 稼働率（前週・前月・前年） | `operation_rate.value` / `.prev` を `csvRate()` で | 確定期間は `facility_summaries.operation_ratio`、当期はその場で集計（5B-7） |
| 出発成功率 | `launch_rate.value` | 同 `launch_ratio` |
| 完了率 / 完了率（前◯） | `completion_rate.value` / `.prev` | 同 `clean_ratio` |
| エラー率 | `error_rate.value` | 同 `error_ratio` |
| 変化 | `diffText(overallDiff(item))`（`▲2` / `▼1` / `→`。★画面と同じ関数★ `lib/reports/facility-performance-labels.ts`） | 稼働率と完了率の前期比の平均 |
| ステータス | `FACILITY_STATUS_LABEL[status]`（良好 / 注意 / 要改善 / 判定なし。★画面と同じ表★） | 稼働率から（`judgeFacilityStatus()`。暫定の閾値） |

ファイル名は `facility-performance_{weekly|monthly|yearly}_{期間}.csv`（例 `facility-performance_monthly_2026-09.csv` / `_weekly_2026-10-w1` / `_yearly_2025`）。★ASCII のみ・parse 済みの数値から組み立てる★（`Content-Disposition` に日本語や生のクエリ文字列を入れない）。

### 13.3 日英の対応

画面の日英切替は `lib/i18n.ts` の1つの辞書で行っています。**辞書のキーが日本語の原文そのもの**です
（`lib/i18n.ts:3-6`）。キーが無ければ原文をそのまま返すため（`lib/i18n.ts:493-496`）、
未登録でも画面は壊れませんが日本語のまま出ます。

| 日本語（＝辞書のキー） | 英語 | 場所 |
|---|---|---|
| 施設名 / 機種 / シリアル / 日付 | Facility / Model / Serial / Date | `lib/i18n.ts:24-27` |
| 出発 / 清掃結果 / 完了率 / 判定 | Dep. / Clean Result / Completion / Grade | `lib/i18n.ts:29-32` |
| 1h前ST / 30分前ST | 1H Before ST / 30min Before ST | `lib/i18n.ts:89-90`（2026-10-01 追加） |
| 稼働率 / 出発成功率 / 清掃完了率 / エラー発生率 | Uptime / Dep. Rate / Completion / Error Rate | `lib/i18n.ts:91-94` |
| 要改善 / 注意 / 良好 | Needs Fix / Caution / Good | `lib/i18n.ts:98-100` |
| ★未解消 / 対応中 / 解消★ | Unresolved / In progress / Resolved | `lib/i18n.ts:160-162`（★2026-10-02 追加。指示書AT★） |
| 「{n}分前異常」/ 出発失敗 | Error {n} min before / Launch failed | `lib/i18n.ts:217-218` |
| ★清掃開始前 / 清掃中 / 清掃中断 / 清掃キャンセル / 清掃正常終了 / 清掃異常終了★ | Not started / Cleaning / Interrupted / Cancelled / Completed / Ended abnormally | `lib/i18n.ts:221-226`（★2026-10-02 追加。英語は仮★） |
| 事前チェック異常 | Pre-check error | `lib/i18n.ts:232` |
| 監視 / D-Sec | Monitor / D-Sec | `lib/i18n.ts:175,178` |

★注意★ 「完了率」と「清掃完了率」は**どちらも EN では "Completion"** です（`lib/i18n.ts:31,93`）。
別々のキーとして登録されているので、片方だけ変えると画面によって表記が食い違います。


---

## 14. 設定ファイルと保存ファイル — アプリの外側の決めごと

ここまでの章はアプリの中身の話でした。この章は**その外側**、つまり「どう組み立てるか」「どこに書き出すか」を決めているファイルの話です。

### 14.0 「設定」は3種類あります

混ざりやすいので先に分けます。

| 種類 | 置き場所 | 例 | 変更に必要なこと |
|---|---|---|---|
| **業務のルール** | `lib/config/` | ◎の基準90%、L2は2日連続 | コード変更＋デプロイ（章6） |
| **アプリの動き方** | ルート直下の設定ファイル | どこをテスト対象にするか、何を禁止するか | コード変更＋デプロイ（この章） |
| **環境ごとの値** | `.env.local` | DBの接続先、認証トークン | ファイル書き換え＋再起動（この章） |
| **実行中に増えるもの** | `data/` | 画面から保存した備考・設定 | **画面から操作するだけ**（この章） |

「◎の基準を変えたい」は1番目、「本番のDBに繋ぎたい」は3番目、「備考を書きたい」は4番目です。

### 14.1 開発の道具の設定

いずれも**アプリの動作そのものではなく、作る作業のほうを決めるファイル**です。

| ファイル | 何を決めているか | 押さえどころ |
|---|---|---|
| `package.json`（36行） | 使う外部部品の一覧と、`npm run …` で打てるコマンドの定義 | コマンドは8本（章9-6）。外部部品は本番用6個・開発用12個と**かなり少ない**。`next` / `react` / `@prisma/client` / `pino` が主役 |
| `tsconfig.json`（34行） | TypeScript の厳しさと、パスの書き方 | ★`"@/*": ["./*"]` が `@/lib/db` のような書き方を可能にしています★。`"strict": true` で型チェックが最も厳しい設定です |
| `next.config.ts`（8行） | Next.js の設定 | ★中身は1行だけ★。`pino`（ログ部品）を Next.js に取り込ませず Node.js 側で解決させる指定です。ログが別スレッドで動くため |
| `vitest.config.ts`（21行） | テストの対象範囲 | ★`include` が `lib/**` と `app/**` だけ★。**`components/**` は入っていません**（章11） |
| `prisma7.config.ts`（20行） | DBの接続先をどこから読むか | ★Prisma 7 は接続先を `schema.prisma` に書けません★。また Prisma は既定で `.env` しか読まないため、`.env.local` を明示的に読み込んでいます |
| `next-env.d.ts` | Next.js が自動生成する型定義 | ★編集禁止★とファイル自身に書いてあります。git 管理外 |

### 14.2 ★事故を「機械で」止めている2つのルール★ — `eslint.config.mjs`

このファイルだけは、単なる設定ではなく**安全装置**として働いています。どちらも「レビューで気をつける」ではなく、**違反したらその場でエラーになって先へ進めません。**

**① `console.log` の禁止**

```
"no-console": "error"
```

動作ログは必ず `lib/logger` を通す決まりです（章9-4）。`console.log` を直接書くと、リクエストIDが付かず、個人情報の伏せ字処理も通りません。例外は `lib/logger/**` 自身と `scripts/**`（単発のコマンドで、画面に出すことが目的）だけです。

**② 生SQLの禁止**

`$queryRaw` `$executeRaw` `Prisma.raw` を書くと**エラーになります**。理由が2つ書かれています。

- **SQLインジェクション**（入力欄に細工した文字列を入れてDBを操作される攻撃）の入口を構造的に塞ぐ
- 本番は MariaDB、ローカル検証は MySQL なので、★生SQLは「ローカルで通っても本番で通らない」★

**どうしても必要になったときは、このルールを消すのではなく、その1箇所だけ個別に許可して理由を `docs/decisions.md` に書く**という手順まで指定されています。

### 14.3 環境ごとの値 — `.env` の3兄弟

| ファイル | 何か | git |
|---|---|---|
| `.env.example` | **開発用のテンプレート**。「ローカルで動くこと」優先 | 入っている |
| `.env.production.example` | **本番用のテンプレート**。「本番で安全なこと」優先 | 入っている |
| `.env.local` | ★**実際の値が入ったファイル**★ | ★**入っていない**★ |

**実値を書いてよいのは `.env.local` だけ**です。テンプレート2本には「★このファイルに実値を書かないこと★」と明記されています。

★**開発用と本番用で、わざと値の向きが逆になっている項目が2つあります。**★ これは間違いではなく意図的です。

| 変数 | `.env.example`（開発） | `.env.production.example`（本番） | 逆になっている理由 |
|---|---|---|---|
| `USE_FIXTURES` | `true`（サンプルデータ） | `false`（実DB） | 開発はDBなしで動くことを優先 |
| `ALLOW_DIRECT_ACCESS` | `true` | ★**設定しない**★ | 開発では無いと全ページ403になる。本番で設定すると誰でも入れてしまう |

`USE_FIXTURES` には注意書きが付いています。**未設定だと `true`（サンプルデータ）になり、実DBに繋がらないまま「それらしい数字」が画面に出ます。例外もエラーも出ません。**

だから**デプロイ前に必ずこれを実行します**（章9）。

```bash
npm run check:env
```

#### DBの接続先は2通りの書き方ができる（2026-09-17 追加）

接続先は `DATABASE_URL` という1行で書くのが基本ですが、★**5つに分けた「部品変数」でも書けるようになりました**★。

| 書き方 | 変数 | 優先 |
|---|---|---|
| 1行で書く | `DATABASE_URL="mysql://ユーザー:パスワード@ホスト:3306/DB名"` | — |
| 部品に分ける | `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_USER` / `DATABASE_PASSWORD` / `DATABASE_NAME` | ★`HOST` と `PASSWORD` の両方があれば**こちらが優先**★ |

★**部品変数が追加された理由**★: パスワードに `@` が含まれていると、1行の書き方では `%40` と書き換える必要があり、
しかも**その `%40` が読み込みの途中で壊れて消える**ことが実際に起きました（`p%40ss` → `p`）。
部品変数なら書き換えが要りません。本番用テンプレートでは**こちらを推奨**しています。

★`DATABASE_HOST` を使う場合、`DATABASE_USER` と `DATABASE_NAME` は必須です★。既定値は用意していません
（実在のDBユーザー名をコードに書かないため）。足りないと**どれが足りないかを言って起動時に止まります**。
★黙って別のDBにつながることはありません★。組み立ては `lib/db/prisma.ts` が担当しています。

#### ★書き込み用アカウントの変数（2026-10-02 追加）★

アラート画面・エラー一覧の対応状況・備考（`robot_error_reports` の4列）と、エラー一覧の経路のエラー内容（`error_list_id`。D-184）を書くときだけ使う接続です（章8-10）。`.env.example` にだけ書かれています（`.env.production.example` には未記載）。

| 変数 | 設定のしかた | どうなるか |
|---|---|---|
| `DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD` | ★2つとも書かない★ | ★参照用の接続（上の `DATABASE_*`）で書く★（いまはこれで動く見込み） |
| 同上 | ★片方だけ書く★ | ★書き込みが例外で失敗する★。`npm run check:env` も error を出す |
| 同上 | 2つとも書く | 書き込み専用の接続を作る。★接続先は参照用と同じDB★なので `DATABASE_HOST` / `DATABASE_NAME`（と必要なら `DATABASE_PORT`）も書く。★1行の URL 形式は用意していません★（`%40` が壊れる問題を持ち込まないため） |

★実値はコミットしないこと★（テンプレートにも空欄でしか書かれていません）。

### 14.4 git に入れないもの — `.gitignore`

`node_modules` や `.next` のような「作り直せるもの」に加えて、**この2行が運用上重要**です。

| 行 | 対象 | 意味 |
|---|---|---|
| `.env*` / `!.env.example` / `!.env.production.example` | 実値の環境変数 | 認証情報をコミットしない。テンプレート2本だけ例外で通す |
| `/data` | ★画面から保存したデータ★ | **git にバックアップされません**（14.7参照） |
| `__pycache__/` / `*.pyc` | PDF生成スクリプトが作るキャッシュ | `docs/handoff/pdf/` の Python スクリプト用（2026-09 追加） |

★`!.env.production.example` の行には「**この行が無いと上の `.env*` に飲まれて Git 管理外になる**」というコメントが付いています。★ 消すとテンプレートが配布されなくなります。

### 14.5 自動チェック — `.github/workflows/ci.yml`

**PRを作ったとき**と **main に入ったとき**に、GitHub が自動で4つを順に走らせます。

```
リポジトリ取得 → 依存インストール → Prisma クライアント生成 → Lint → テスト → ビルド
```

★**「Prisma クライアント生成」は 2026-09-17 に追加されました**★。生成先の `lib/generated/prisma/` は git に入れていないため、
チェックアウト直後には存在しません。生成しないと**ビルドが「部品が見つからない」で落ちます**（実際に PR #6 で発生）。
この手順は**スキーマを読むだけでDBには接続しません**。

押さえどころが2つあります。

- ★**ビルド時に `USE_FIXTURES: "true"` を渡しています**★。つまり **CI はデータベースに一切接続しません**。「サンプルデータでビルドが通ること」だけを確認しています
- ★**現時点では、赤くてもマージできます**★。ファイル冒頭に「結果を表示するだけ。運用が安定したら必須化します」と書かれています。**赤いまま放置しない運用でカバーしている状態**です

### 14.6 AI向けの設定ファイル

このプロジェクトは Cursor と Claude Code の両方で開発しているため、AI に読ませる指示書自体がリポジトリに入っています。

| ファイル | 役割 |
|---|---|
| `CLAUDE.md`（23KB） | ★**規約の正**★。確定仕様・禁止事項・実装規約。Cursor も Claude Code も自動で読む |
| `.cursorrules`（6行） | Cursor の旧形式。中身は「**規約は `CLAUDE.md` にある。ここに書き戻すな**」だけ。二重管理を防ぐための番人 |
| `.claude/commands/d-watch.md` | 作業を引き継ぐ手順書。`/d-watch` と打つと読み込まれる |
| `.claude/launch.json` | 開発サーバの起動設定。★**2種類あります**★: `d-watch`（ポート3000。`.env.local` の設定どおり＝現在はDBに接続）と `d-watch-fixtures`（ポート3100。★`USE_FIXTURES=true` を強制してサンプルデータで起動★）。**同時に立ち上げて、DB版とサンプル版を並べて見比べられます** |
| `.claude/settings.local.json` ほか | 個人環境の状態。git 管理外 |

`eslint.config.mjs` が `.claude/**` を検査対象から外しているのは、この下に**作業用のコピー（worktree）**ができることがあり、そこまで検査すると落ちるためです。

### 14.7 保存ファイル — `data/` に何が入るか

★**メインDBに書けないものは、すべてここに JSON で保存されます。**★

DBは TeamWorks の管理で、DFA が書き込めるのは4カラムと `robot_error_reports.error_list_id` だけです（章8-1。★その4カラムにはアラートの対応状況・備考、`error_list_id` にはエラー一覧のエラー内容を書いています★。章8-10）。運用設定や D-S制御盤の手入力メモはそこに入れられないので、アプリが自分でファイルに書いています。

| ファイル | 中身 | いま存在するか |
|---|---|---|
| `data/settings/ds-device-notes.json` | D-S制御盤の備考（手入力メモ） | ✅ 存在。**中身は `{}`** ＝1件も保存されていない |
| `data/settings/display-limits.json` | 連続日数の表示上限（システム管理画面で編集） | ❌ まだ無い |
| `data/settings/advice.json` | アドバイスの文言（編集機能は未実装） | ❌ まだ無い |

★**「ファイルが無い＝壊れている」ではありません。**★ 3つとも**保存操作を一度もしていないだけ**で、その場合はコードに書かれた既定値がそのまま使われます。初めて保存したときに作られます。

#### 共通の仕掛け — `lib/settings/json-store.ts`

3つとも同じ部品を使っており、次の性質を持ちます。

| 性質 | 内容 |
|---|---|
| **書き換えは一瞬で入れ替え** | 一時ファイルに書いてから名前を付け替えます。途中で電源が落ちても、中途半端なファイルが残りません |
| **同じファイルへの書き込みは順番に** | ★プロセス内でファイルパスごとに1件ずつ書きます★（`c5fe519`。D-182）。同時に保存しても Windows の `EPERM` で落ちず、最後に呼んだ保存の値が残ります（後勝ち）。★別プロセス間の鍵はかけません★（施設20件・実質1人・1プロセスの運用のため） |
| **壊れたファイルは退避する** | 読めない JSON を見つけたら、**タイムスタンプ付きの `.bak` に逃がしてから**空の状態で再開します。黙って消しません |
| **退避に失敗したら止まる** | ★空の値を返さず例外を投げます★。「バックアップも取れないのに元ファイルを捨てる」を防ぐため |
| **`__proto__` などを無害化** | JSON に細工したキーが入っていてもプログラムを乗っ取れないようにしています |
| **ログ部品を使わない** | `lib/logger` を読み込むと `lib/db` まで芋づるで付いてくるため、通知は呼び出し側から受け取る作りです |

#### ★いちばん大事な注意★

**`/data` は `.gitignore` に入っているので、git にバックアップがありません。**

サーバーのディスクが消えれば、**画面から入力した備考も設定も一緒に消えます。** 本番運用ではこのフォルダを別途バックアップの対象に入れる必要があります（現時点では未対応）。


---

## 付録A. 全ファイル一覧

人が書いたファイルを1行1ファイルで並べます。**（新）** は 2026-09-14 版の地図（最終更新 `af11453`）以降、第3版（`d23e203`）までに追加されたファイルです（`git diff --diff-filter=A af11453..d23e203` で抽出。削除・改名は0件）。★**（新・10/05）** は第3版のあと 2026-10-05（`ceeb736`）までに追加されたファイルです★（`git diff --diff-filter=A d23e203..ceeb736`。削除・改名は0件）。

**圧縮ルール（該当するファイルは表から省いています）**

1. `components/**/*.module.css` は同名の `.tsx` 専用の見た目なので、`.tsx` の行に含めます
2. `__tests__/` の中身は [章11](#11-テストは何を守っているか) で扱います
3. `lib/generated/prisma/` は自動生成物なので1行にまとめます
4. `app/(dashboard)/*/page.tsx`（11本）は中身が同じ形なので1行にまとめます

### A.1 ルート直下

| パス | 一言でいうと何か |
|---|---|
| `CLAUDE.md` | 開発の前提とルール。★実装前に全文を読む★ |
| `README.md` | リポジトリの入口 |
| `d-watch-api.yaml` | ★API契約の正★。APIを変えるなら先にここ |
| `d-watch-mock.jsx` | ★画面レイアウト・文言・遷移の正★。1文字も書き換えない原型 |
| `proxy.ts` | 全リクエストの横断処理（相関ID → IP制限 → 認証）。Next.js 16 で `middleware.ts` から改名 |
| `instrumentation.ts` | 起動時の初期化ログと、捕捉漏れエラーの記録 |
| `next.config.ts` | Next.js の設定。pino（ログ部品）をバンドルしない指定だけ |
| `prisma7.config.ts` | Prisma 7 の設定（Prisma が生成したもの） |
| `vitest.config.ts` | テストの設定。★`lib/**` と `app/**` だけを走らせる★ |
| `eslint.config.mjs` | コードの書き方チェックの設定 |
| `tsconfig.json` | TypeScript の設定 |
| `package.json` | 依存部品と `npm` コマンドの定義 |
| `package-lock.json` | 依存部品のバージョン固定表（★手で直さない★） |
| `.env.example` | 環境変数の雛形（開発用）。`USE_FIXTURES=true` |
| `.env.production.example` | 環境変数の雛形（本番用） |
| `.env.local` | ★接続情報など本物の値★。git に入らない。★このドキュメントでは中身に触れません★ |
| `.gitignore` | git に入れないものの一覧 |
| `.cursorrules` | Cursor 向けの短い指示 |
| `.claude/commands/d-watch.md` | Claude Code の作業再開コマンド |
| `.claude/launch.json` | Claude Code のプレビュー起動設定 |
| `.github/workflows/ci.yml` | GitHub 上の自動チェック（Lint → テスト → ビルド） |

### A.2 app（画面）

| パス | 一言でいうと何か |
|---|---|
| `app/layout.tsx` | 全ページの一番外側の器（`<html lang="ja">`、フォント） |
| `app/globals.css` | 全画面の色・フォントの定義 |
| `app/page.tsx` | `/` に来た人を `/dashboard` へ送るだけ |
| `app/login/page.tsx` | ログイン画面（共有トークン入力） |
| `app/(dashboard)/layout.tsx` | 11画面に `AppShell`（サイドバー）を着せる |
| `app/(dashboard)/*/page.tsx`（11本。★改訂3 時点 12本★） | 各画面の札。`components/features/` の画面を1つ呼ぶだけ。★`clean-area/page.tsx` が（新）★、★`errors/page.tsx` が（新・10/06）★、`inspections/page.tsx` は `PlaceholderScreen` |

### A.3 app/api（23本。★改訂3 時点 25本・改訂4 時点 26本・改訂9 時点 27本★）

| パス | 一言でいうと何か |
|---|---|
| `app/api/auth/login/route.ts` | 共有トークンを検証して cookie を発行 |
| `app/api/auth/logout/route.ts` | cookie を削除（★画面からの呼び出しは0件★） |
| `app/api/clean-area/route.ts` | **（新）** 清掃面積の集計 |
| `app/api/clean-logs/route.ts` | 清掃詳細ログ一覧（★他の一覧APIの見本★） |
| `app/api/clean-logs/export/route.ts` | 清掃詳細ログのCSV出力 |
| `app/api/clean-logs/[id]/advice/route.ts` | 行を開いたときの推定原因＋確認アクション |
| `app/api/clean-logs/unscheduled/[report_id]/advice/route.ts` | 予定なしの行のアドバイス取得 |
| `app/api/clean-logs/[id]/areas/route.ts` | エリア別完了率（施設×日付） |
| `app/api/customers/route.ts` | **（新）** 顧客一覧（分類マスタ） |
| `app/api/dashboard/route.ts` | ダッシュボードの集計 |
| `app/api/ds-alerts/route.ts` | D-Secアラート一覧＋サマリ |
| `app/api/ds-devices/route.ts` | D-S制御盤の稼働状況一覧＋サマリ |
| `app/api/ds-devices/[serial_number]/note/route.ts` | D-S制御盤の備考を保存 |
| `app/api/errors/route.ts` | ★**（新・10/06）**★ エラー一覧（`robot_error_reports` の全行。削除済みの施設・清掃エリアの行を除く（U8）。2段階で読む） |
| `app/api/errors/[error_id]/status/route.ts` | ★**（新・10/06）**★ エラーの対応状況・備考を更新（PATCH。行があれば書ける）。★10/27 からエラー内容（`error_list_id`）も書く — 5列目を書くのはこのルートだけ（D-184）★ |
| `app/api/error-lists/route.ts` | ★**（新・10/27）**★ エラー内容の選択肢（`error_lists` の有効な行・id の昇順。読み取り専用。D-184） |
| `app/api/facilities/route.ts` | 施設ロボットマスタ（読み取り専用） |
| `app/api/groups/route.ts` | **（新）** ホールディングス一覧（分類マスタ） |
| `app/api/prechecks/route.ts` | アラート（事前チェック）一覧＋サマリ |
| `app/api/prechecks/[id]/status/route.ts` | ★**（新・10/05）**★ アラートの対応状況・備考を更新（PATCH）。★メインDBに書き込むルート★（章8-10。★10/06 から下の `errors/[error_id]/status` と2本★） |
| `app/api/reports/continuous-problems/route.ts` | 連続問題施設（2日以上連続で△以下 or 出発失敗） |
| `app/api/reports/error-breakdown/route.ts` | エラー種別の内訳（生値のまま） |
| `app/api/reports/facility-performance/route.ts` | 施設別パフォーマンス（稼働率ワースト順）。★取得〜並べ替えは CSV と同じ `loadFacilityPerformance()`★ |
| `app/api/reports/facility-performance/export/route.ts` | ★**（新・10/06）**★ 施設別パフォーマンスの CSV 出力（`limit` は無視して全施設。D-163） |
| `app/api/reports/kpi/route.ts` | KPI4指標＋前期比 |
| `app/api/schedules/route.ts` | スケジュール一覧（読み取り専用） |
| `app/api/settings/advice/route.ts` | アドバイス文言の一覧 / 保存 |
| `app/api/settings/display-limits/route.ts` | 表示上限の一覧 / 更新 |

### A.4 components

| パス | 一言でいうと何か |
|---|---|
| `components/layout/AppShell.tsx` | サイドバー・ロゴ・テーマ/言語切替。メニューは固定配列（★契約フラグでの出し分けは未実装★） |
| `components/layout/PlaceholderScreen.tsx` | 「準備中」の札（点検記録が使用中） |
| `components/theme/ThemeProvider.tsx` | ダーク/ライトと JP/EN の状態を全画面へ配る。★選んだ値は localStorage に保存し、`<head>` のスクリプト（`app/layout.tsx`・`lib/ui-prefs.ts`）が描く前に戻す（D-183）★ |
| `components/ui/Pagination.tsx` | ページ送り。★判断は `lib/pagination/` に任せ、描くだけ★ |
| `components/ui/StatCard.tsx` | 数字カード。色は判定（◎○△×）ではなく KPI の意味合い（★D-173 で中立の `mt` を足した。ダッシュボードの「結果待ち」★） |
| `components/ui/ProgressBar.tsx` | 0〜100 の横棒 |
| `components/ui/FilterButton.tsx` | 絞り込みボタン |
| `components/ui/GradeBadge.tsx` | ◎○△× のバッジ |
| `components/ui/Pill.tsx` | 色付きの小さなラベル |
| `components/ui/date-picker.ts` | ★**（新・10/06）**★ `openDatePicker()` — 期間の入力欄の枠のどこを押してもカレンダーを開く（D-162） |
| `components/features/dashboard/DashboardScreen.tsx` | ダッシュボード画面 |
| `components/features/dashboard/RescueRequiredCard.tsx` | **（新）** 要レスキュー店舗カード |
| `components/features/dashboard/ScheduleProgressCard.tsx` | スケジュール進捗カード |
| `components/features/dashboard/SecurityBanner.tsx` | セキュリティ復帰漏れの警告バナー |
| `components/features/detail-log/DetailLogScreen.tsx` | 清掃詳細ログ画面（[章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで)） |
| `components/features/detail-log/CleanLogTable.tsx` | 清掃ログの表。ダッシュボードも5列表示で借りる |
| `components/features/detail-log/AreaModal.tsx` | エリア別完了率のモーダル。ダッシュボードも借りる |
| `components/features/alerts/AlertsScreen.tsx` | アラート画面（★対応状況・備考は編集モードで変える。D-165★。本清掃が成功したカードは控えめ。D-171） |
| `components/features/errors/ErrorsScreen.tsx` | ★**（新・10/06）**★ エラー一覧画面（★モックに無い画面★。★編集モードで対応状況・備考・エラー内容を変え、「保存（N件）」で送る。D-165 / 指示書BE★） |
| `components/features/errors/ErrorContentPicker.tsx` | ★**（新・10/27）**★ エラー内容を選ぶ窓（エラー一覧専用。キーワード・分類・件数・クリア。指示書BE） |
| `components/features/reports/ReportsScreen.tsx` | レポート画面（★804行★。最大。★CSVエクスポートが動く。D-163★） |
| `components/features/reports/KpiCard.tsx` | KPI カード（モックの4枚に対応） |
| `components/features/reports/FacilityPerformanceTable.tsx` | 施設別パフォーマンス表 |
| `components/features/clean-area/CleanAreaScreen.tsx` | **（新）** 清掃面積画面（★モックに無い例外画面★） |
| `components/features/ds-logs/DsLogScreen.tsx` | D-Secログ画面（備考の保存あり） |
| `components/features/ds-alerts/DsAlertsScreen.tsx` | D-Secアラート画面 |
| `components/features/facilities/FacilitiesScreen.tsx` | 施設ロボットマスタ画面（読み取り専用） |
| `components/features/schedules/SchedulesScreen.tsx` | スケジュール画面（完全な参照専用） |
| `components/features/system/SystemScreen.tsx` | システム管理画面（編集できるのは表示上限だけ） |

### A.5 lib（純粋層 — DBにもファイルにも触らない計算）

「純粋関数」とは、**同じ入力には必ず同じ答えを返し、外の世界（DB・ファイル・時計）に触らない関数**です。テストで固定しやすいので、判断はできるだけここへ寄せる設計になっています。

| パス | 一言でいうと何か |
|---|---|
| `lib/judge/index.ts` | 判定ロジックのまとめ役 |
| `lib/judge/grade.ts` | ★◎○△× の判定★ |
| `lib/judge/escalation.ts` | L1〜L3 の判定 |
| `lib/judge/advice.ts` | 対応アドバイスの発火判定（R1〜R4 ＋ B1〜B3） |
| `lib/judge/kpi.ts` | KPI4指標の計算。★1回ごとの完了率 `runCompletionRate()`★ と、その単純平均 `meanCleanRatio()`（章5-4） |
| `lib/judge/rescue.ts` | **（新）** 要レスキュー店舗の判定 |
| `lib/format/date.ts` | 日付・時刻の表示整形 |
| `lib/format/period.ts` | 週次/月次/年次の期間。★月跨ぎ週の帰属は `weekOfMonth()` だけが知る★ |
| `lib/format/search-text.ts` | 検索照合用の文字列正規化（記号ゆれに強くする） |
| `lib/format/contract.ts` | `facilities.contract`（10桁の文字列）↔ 契約フラグの変換 |
| `lib/format/number.ts` | **（新）** 数値の表示整形。面積の単位は `AREA_UNIT` から |
| `lib/format/clean-result.ts` | **（新）** `clean_task_reports.status` の表示整形（★10/02 から表示名★） |
| `lib/format/group-id.ts` | **（新）** `customers.group_id`（DBの0）↔ API の `null` の変換 |
| `lib/format/consecutive-days.ts` | 連続日数の表示上限 |
| `lib/format/schedule.ts` | スケジュールの表示整形（曜日ラベル等をサーバーで生成） |
| `lib/format/robot-model.ts` | 対応機種レジストリに載っているかの判定 |
| `lib/format/raw-label.ts` | 生値ラベルの空値表示 |
| `lib/format/address.ts` | 住所2列を1つに連結 |
| `lib/format/kpi-delta.ts` | ★**（新・10/19）**★ レポートの KPI カードの前期比（矢印・数・色。丸めて0なら「→」・色なし。D-183） |
| `lib/format/precheck-status.ts` | ★**（新・10/07）**★ 1h前ST / 30分前ST のセル（`null` ＝ 事前チェックの予定が無い → 「-」。D-171） |
| `lib/clean-logs/query.ts` | 清掃詳細ログのパラメータ検査と絞り込み規則 |
| `lib/clean-logs/areas.ts` | エリア別完了率の絞り込みと全体率（全体率の分子・分母は `overallCompletionTally()`） |
| `lib/clean-logs/csv.ts` | 清掃詳細ログの CSV の組み立て（★セルの無害化・率の空欄は `lib/csv.ts`★） |
| `lib/csv.ts` | ★**（新・10/06）**★ ★CSV の共通部品★ `csvCell()`（数式インジェクション対策。D-125）・`csvRate()`（値が無い率は空のセル。D-164）・`csvDocument()`（BOM・CRLF）。清掃詳細ログとレポートの2つの CSV が使う（D-163） |
| `lib/clean-logs/detail-log-date-range.ts` | ★**（新・10/06）**★ 清掃詳細ログの期間の検証と URL の組み立て（空の側は付けない。D-159 追記） |
| `lib/clean-logs/result-pending.ts` | ★**（新・10/07）**★ ★結果待ちの判定 `isResultPending()` の1本★（予定の開始 ≤ 現在 ＜ 予定の終了＋`RESULT_WAIT_MINUTES` かつレポートが無い。D-173） |
| `lib/clean-area/aggregate.ts` | **（新）** 清掃面積の集計 |
| `lib/clean-area/query.ts` | **（新）** 清掃面積のパラメータ検査（★日付上限を持たない★） |
| `lib/clean-area/visibility.ts` | **（新）** 清掃面積の内訳を何件見せるか |
| `lib/dashboard/aggregate.ts` | ダッシュボードの集計 |
| `lib/dashboard/rescue-visibility.ts` | **（新）** 要レスキューを何件見せるか |
| `lib/dashboard/latest-run.ts` | ★**（新・10/06）**★ ダッシュボードに出す「各清掃エリアの直近の清掃」の選び方（純粋関数。D-159）。★候補は「予定の終了 ≤ 現在」（D-173 で終了＋30分の待ちを廃止）★ |
| `lib/reports/query.ts` | レポート4本のパラメータ検査 |
| `lib/reports/target-customer.ts` | **（新）** 確定期間KPIの対象顧客をどう決めるか（★`customer_id` と `group_id` は AND。傘下でなければ `customerId: null` → 200・空。D-182★） |
| `lib/reports/block-visibility.ts` | **（新）** レポート画面の4ブロックを1本ずつ出す判定 |
| `lib/reports/facility-performance-csv.ts` | ★**（新・10/06）**★ 施設別パフォーマンスの CSV の列・行・ファイル名（D-163） |
| `lib/reports/facility-performance-labels.ts` | ★**（新・10/06）**★ 「変化」（`overallDiff()` / `diffText()`）とステータスの表示名（`FACILITY_STATUS_LABEL`）。★画面の表と CSV で共有★（D-163） |
| `lib/prechecks/query.ts` | アラートのパラメータ検査（件数バッジの差分反映 `applyResolutionChange()` も） |
| `lib/prechecks/status-update.ts` | ★**（新・10/05）**★ アラートの対応状況・備考の更新規則（本文の検品と、DBに書く列だけのパッチを作る `planPrecheckStatusUpdate()`）。★`error_list_id` は捨てる（D-184 でも挙動は変えていない）★ |
| `lib/errors/status-update.ts` | ★**（新・10/27）**★ ★エラー一覧の更新規則★（`lib/prechecks/status-update.ts` の上に `error_list_id` を足したもの。`parseErrorStatusUpdateBody()` / `planErrorStatusUpdate()` / `errorListIdToLookUp()` / `isEmptyErrorPatch()`。DB を読まない純粋関数。D-184） |
| `lib/errors/edit-drafts.ts` | ★**（新・10/27）**★ エラー一覧の下書き（`error_list_id` を含む。アラートと D-Sec は `lib/prechecks/edit-drafts.ts` のまま。指示書BE） |
| `lib/errors/error-content-label.ts` | ★**（新・10/27）**★ 詳細の列の出し分け（未選択・不明・その他は大分類・選べない行は `-`。指示書BE §3） |
| `lib/errors/error-content-picker.ts` | ★**（新・10/27）**★ 選択窓の絞り込み（キーワードと分類の AND。指示書BE §4-1） |
| `lib/prechecks/label.ts` | アラートの種別ラベル |
| `lib/prechecks/edit-drafts.ts` | ★**（新・10/06）**★ ★編集モードの下書きと一括保存★（アラート・エラー一覧が共有。D-165）。★旧 `status-cycle.ts`（巡回順）は D-161 で削除★ |
| `lib/error-reports/query.ts` | ★**（新・10/06）**★ エラー一覧のパラメータ検査と ★絞り込み・並べ替えの正★（`filterSortErrorReportKeys()`） |
| `lib/error-reports/label.ts` | ★**（新・10/06）**★ エラー一覧の「紐づく予定」の文言 |
| `lib/ds-alerts/query.ts` | D-Secアラートのパラメータ検査 |
| `lib/ds-devices/note-drafts.ts` | ★**（新・10/19）**★ D-Secログの備考の編集モードの下書き（`lib/prechecks/edit-drafts.ts` と同じ規則。D-183） |
| `lib/ds-devices/query.ts` | D-Secログのパラメータ検査 |
| `lib/facilities/query.ts` | 施設マスタのパラメータ検査 |
| `lib/facilities/resolve-by-name.ts` | 施設名 → 施設ID の解決 |
| `lib/facilities/resolve-by-master.ts` | **（新）** 分類（ホールディングス／顧客）→ 施設ID の解決 |
| `lib/facilities/intersect-ids.ts` | **（新）** 施設IDの絞り込み結果どうしの積集合（★`null` と `[]` を区別★） |
| `lib/facilities/filter-by-ids.ts` | **（新）** 解決済みの施設IDで行を絞る |
| `lib/schedules/query.ts` | スケジュールのパラメータ検査 |
| `lib/masters/query.ts` | **（新）** 分類マスタのパラメータ検査 |
| `lib/pagination/page-items.ts` | ページ送りの番号を組み立てる（★`components/` にテストが無いのでここに判断を寄せる★） |
| `lib/api/query-result.ts` | パラメータ検査の結果の共通型（失敗を `null` で返さない） |
| `lib/api/path-params.ts` | `[id]` などパスの穴の検査 |
| `lib/api/master-id.ts` | **（新）** `group_id` / `customer_id` の検査 |
| `lib/auth/client-ip.ts` | 接続元IPの取り出し（`X-Forwarded-For` の末尾）と許可判定 |
| `lib/auth/login-error-message.ts` | ★**（新・10/19）**★ ログインの失敗の文言（状態コード → 辞書のキー。D-183） |
| `lib/auth/next-path.ts` | ログイン後の戻り先を安全な値に丸める |
| `lib/settings/advice-validation.ts` | アドバイス文言の入力検証 |
| `lib/config/env-check.ts` | 環境変数の検証 |

### A.6 lib（データ・副作用層 — DB・ファイル・通信・ログに触る）

| パス | 一言でいうと何か |
|---|---|
| `lib/db/index.ts` | ★データの取り出し口★。`DataSource`（20メソッド）と `USE_FIXTURES` の切り替え |
| `lib/db/use-fixtures.ts` | `USE_FIXTURES` の判定だけ（Edge 対策で独立） |
| `lib/db/prisma.ts` | Prisma クライアントのシングルトン（★静的 import しない★）。★書き込み用の接続 `getWritePrisma()` もここ★ |
| `lib/db/consecutive-source.ts` | 連続日数の入力を作る唯一の読み取り（4経路が共有） |
| `lib/db/clean-area-source.ts` | **（新）** 清掃面積の集計材料の読み取り（面積は `groupBy`、完了率・実施回数は行の取り寄せ） |
| `lib/reports/facility-performance-load.ts` | ★**（新・10/06）**★ 施設別パフォーマンスの取得 → 絞り込み → 並べ替え（`db` を呼ぶのでこちらの層）。★表と CSV の両方のルートが呼ぶ1本★（D-163） |
| `lib/db/robot-error-write.ts` | ★**（新・10/05）**★ ★`robot_error_reports` を UPDATE する唯一のファイル★（`updateMany`。章8-10）。★事前チェックは4列 `writeRobotErrorColumns()`・エラー一覧は5列（＋`error_list_id`）`writeErrorReportColumns()`（D-184）★ |
| `lib/db/facility-name-source.ts` | 施設名解決用の施設マスタ読み取り（`deleted_at` で絞らない） |
| `lib/db/facility-group-source.ts` | **（新）** 分類解決用の施設マスタ読み取り |
| `lib/db/timetable-source.ts` | ★**（新・10/05）**★ 予定を `clean_timetables` と `clean_timetable_histories` の両方から読む唯一の読み口（同じ id は本表。指示書AU 段階3b） |
| `lib/db/robot-error-select.ts` | ★**（新・10/05）**★ `robot_error_reports` を読むときの `select`（`robot_id` / `robot` を読まない。指示書AU 段階0-4）。★`error_list_id` はエラー一覧の行の定数だけが読む（D-184）★ |
| `lib/db/error-list-source.ts` | ★**（新・10/27）**★ ★エラー内容（`error_lists`）の読み取りと名前解決★（選択肢は有効な行だけ・名前解決は絞らない・`id IN` を1回。選べる行の判定 `isErrorListSelectable()` も。D-184） |
| `lib/db/clean-log-mapping.ts` | 清掃ログ1件の組み立て（純粋関数） |
| `lib/db/report-mapping.ts` | レポートの組み立て（純粋関数）。★`periodOfSummaryRow()` がここ★ |
| `lib/db/precheck-mapping.ts` | アラートの組み立て（純粋関数） |
| `lib/db/error-report-mapping.ts` | ★**（新・10/06）**★ エラー一覧の組み立て（純粋関数。SQL の `where` も）。★`isVisibleErrorRow()` は `lib/db/visibility.ts` に委ねる（D-182）★ |
| `lib/db/visibility.ts` | ★**（新・10/19）**★ ★削除済みの施設・清掃エリアの行を出さない規則の1か所★（`isVisibleFacilitySection()` が正・`FACILITY_SECTION_VISIBLE_WHERE` が SQL の写し。全画面。D-182） |
| `lib/db/rescue-mapping.ts` | **（新）** 要レスキューの組み立て（純粋関数） |
| `lib/db/ds-status.ts` | D-Sec のコード値変換（純粋関数） |
| `lib/db/date-range.ts` | JST の日付を「DBの壁時計値」に変換（純粋関数） |
| `lib/db/chunked.ts` | `in` に渡す配列の分割（純粋関数） |
| `lib/db/fixture-clean-logs.ts` | サンプル版 `cleanLogs()` の絞り込み |
| `lib/db/fixture-prechecks.ts` | サンプル版 `prechecks()` の絞り込み |
| `lib/db/fixture-area-completions.ts` | サンプル版 `areaCompletions()` の絞り込み |
| `lib/db/fixture-clean-area.ts` | **（新）** サンプル版 `cleanArea()` の集計材料 |
| `lib/db/fixture-reports.ts` | サンプル版レポート4メソッドの絞り込み |
| `lib/db/fixture-precheck-overrides.ts` | ★**（新・10/05）**★ サンプル版の対応状況の更新（メモリ上の上書き表。再起動で消える） |
| `lib/db/fixture-error-report-overrides.ts` | ★**（新・10/06）**★ サンプル版のエラー一覧の更新（メモリ上の上書き表。AT とは別） |
| `lib/db/local-guard.ts` | シードが `dwatch_local` 以外に書き込むのを防ぐ |
| `lib/fixtures/index.ts` | サンプルデータのまとめ役（★実在の店舗名・住所・氏名を入れない★） |
| `lib/fixtures/_helpers.ts` | 日付を相対で作るヘルパー |
| `lib/fixtures/clean-logs.ts` | 清掃詳細ログ（境界値10ケース等） |
| `lib/fixtures/clean-task-reports.ts` | **（新）** `clean_task_reports` のサンプル |
| `lib/fixtures/task-status-rule.ts` | **（新）** `status` の決め方の唯一の定義 |
| `lib/fixtures/areas.ts` | エリア別完了率 |
| `lib/fixtures/prechecks.ts` | アラート（事前チェック） |
| `lib/fixtures/error-reports.ts` | ★**（新・10/06）**★ エラー一覧（20行。うち見える行は18行（既定15行・解消も含めて18行）。DB の行の形）。★10/27 から5行に `error_list_id`（有効な id 3行・その他(131)＋備考 1行（清掃開始失敗の行）・`error_lists` に無い 9999 が1行）。残りは null★ |
| `lib/fixtures/error-lists.ts` | ★**（新・10/27）**★ エラー内容の選択肢（決定稿「PUDUエラー一覧_20261016」の131行。全行有効。★Pudu の `error_type` は入れない（表に列が無い）★。★実DBの中身と一致するかは未確認★。D-184） |
| `lib/fixtures/reports.ts` | レポート |
| `lib/fixtures/facilities.ts` | 施設マスタ（ロボット0台/1台/3台） |
| `lib/fixtures/groups.ts` | **（新）** ホールディングス |
| `lib/fixtures/customers.ts` | **（新）** 顧客 |
| `lib/fixtures/schedules.ts` | スケジュール |
| `lib/fixtures/ds-devices.ts` | D-S制御盤 |
| `lib/fixtures/inspections.ts` | 点検記録（画面は未着手） |
| `lib/settings/json-store.ts` | `data/settings/` の JSON を安全に読み書きする共通部品 |
| `lib/settings/advice.ts` | アドバイス文言の保存（`advice.json`） |
| `lib/settings/display-limits.ts` | 表示上限の保存（`display-limits.json`） |
| `lib/settings/ds-device-notes.ts` | D-S制御盤の備考の保存（`ds-device-notes.json`） |
| `lib/logger/index.ts` | アプリ共通ロガー（出力は stdout のみ） |
| `lib/logger/api.ts` | APIルートの `catch` から呼ぶ `logApiError()` |
| `lib/logger/context.ts` | 相関IDの格納と `withRequestId()` |
| `lib/logger/events.ts` | 出してよいログイベントの一覧（allowlist） |
| `lib/logger/redact.ts` | 出してはいけない項目名の検出と除去 |
| `lib/logger/timestamp.ts` | ログの時刻形式 |
| `lib/logger/db-write.ts` | ★**（新・10/05）**★ メインDB書き込みの監査ログ `db.write`（成功・失敗とも。備考の中身・エラーメッセージは載せない） |
| `lib/clean-area/anomaly.ts` | **（新）** 清掃面積の異常値を見張ってログに出す |
| `lib/api/client.ts` | ★画面からサーバーへの唯一の通り道★ `apiFetch()` |
| `lib/auth/token.ts` | 共有トークンの検証（定数時間比較） |
| `lib/auth/cookie.ts` | `dwatch_session` cookie の属性 |
| `lib/proxy/authenticate.ts` | IP制限と認証の判定（`PUBLIC_PATHS` はここ） |
| `lib/proxy/request-id.ts` | 相関IDの発行 |

### A.7 lib（その他 — 設定・型・見た目）

| パス | 一言でいうと何か |
|---|---|
| `lib/config/index.ts` | ★設定値（閾値の外出し）★。90/70/10・猶予30分・上限値など |
| `lib/config/advice-rules.ts` | 対応アドバイスのルール定義（既定値） |
| `lib/config/allowed-ips.ts` | IP制限の許可リスト |
| `lib/config/robot-status-match.ts` | B2/B3 アドバイスを発火させるロボット状態の一覧（暫定値） |
| `lib/types/index.ts` | ★型定義★（1,398行。YAML のスキーマ名と揃える） |
| `lib/hooks/use-classification-filter.ts` | **（新）** 分類の絞り込みを5画面で共有するフック |
| `lib/masters/options.ts` | **（新）** 分類の選択肢の組み立て（画面側ロジックの土台） |
| `lib/i18n.ts` | 日本語/英語の対訳表 |
| `lib/ui-prefs.ts` | ★**（新・10/19）**★ 表示設定（ダーク/ライト・JP/EN）の保存のキー・許す値・`<head>` のインラインスクリプト（D-183） |
| `lib/theme.ts` | ダーク/ライトの色トークン |
| `lib/theme-status.ts` | ロボット状態の色分け（★日本語6文字列の完全一致なので、生値ではグレーに落ちる★） |

### A.8 scripts・data

| パス | 一言でいうと何か |
|---|---|
| `scripts/seed-local-db.ts` | ローカルDB（`dwatch_local`）へサンプルを投入する（2,145行）。★ステージングに入っていたダミーもこれ由来だった★（9/30 夜に先方の実データで置き換わった）。★テーブルを消してから入れ直すので、ステージングには絶対に向けない★ |
| `scripts/check-env.ts` | 環境変数の検証（`npm run check:env`） |
| `scripts/local-db/`（2本） | ★**（新・10/27）**★ ★ローカルの `dwatch_local` にだけ★ `error_lists` 表と `error_list_id` 列を足す（`20261027_error_lists.sql` と実行部 `apply-error-lists.ts`。`dwatch_local` 以外では止まる）。★ステージング・本番には絶対に流さない★。中身の131行はそのあと `npm run seed:local` が入れる（D-184 / 指示書BD §8） |
| `data/settings/*.json` | 画面で保存した設定の置き場。★git に入らない★（[章8](#8-データの出どころ) §8.9） |

### A.9 prisma

| パス | 一言でいうと何か |
|---|---|
| `prisma/schema.prisma` | DBの28テーブルの形（952行。★改訂9 時点 985行。`ErrorList`（`error_lists`）を足した★）。★`prisma db pull` の生成物。手で直さない★（★例外: `RobotErrorReport.updated_at` に `@updatedAt` を付けない／★`ErrorList` と `error_list_id` は db pull できないための手書き（D-184）★。章8-8★） |
| `lib/generated/prisma/`（47ファイル） | 上から `prisma generate` で作る部品。★git に入らない・開かない★ |

### A.10 docs

| パス | 一言でいうと何か |
|---|---|
| `docs/spec.md` | ★機能一覧と開発フェーズの正★ |
| `docs/decisions.md` | ★決定事項とその理由の正★（D-001〜D-156） |
| `docs/fixtures.md` | ★サンプルデータに用意するケースの正★ |
| `docs/code-map.md` | このソースコード地図 |
| `docs/architecture.md` | ファイル・フォルダ構成と役割 |
| `docs/logging-design.md` | ログ設計 |
| `docs/deploy.md` | 本番デプロイ手順 |
| `docs/getting-started.md` | 開発手順書 |
| `docs/handoff.md` | 引き継ぎメモ |
| `docs/setup-checklist-daigo.md` | 開発者に渡す前の準備チェックリスト |
| `docs/変更点まとめ_20260914.md` | **（新）** 9/14 時点の変更点のまとめ |
| `docs/UI差分まとめ_20260915.md` | **（新）** モックと実装の画面差分のまとめ |
| `docs/データの流れ_画面1-5_20260917.md` / `_画面6-10_20260917.md` | **（新）** 画面ごとのデータの流れ（2本） |
| `docs/handoff/指示書D〜指示書AT`（42本。AD は欠番） | 実装担当（Cursor）への作業指示書。★AI〜AQ の9本が（新）、AR・AS・AT の3本が（新・10/05）★。最新は `指示書AT_アラートの対応状況を画面から更新する_20261002.md` |
| `docs/handoff/TeamWorks確認事項_第2回〜第11回`（第10回は原稿と送付版の2本、第11回は送付版）＋ `TeamWorks回答_第2回` / `_第10回` ＋ `TeamWorks実DDL照合結果_第1回` | TeamWorks への照会と回答。★第9回が（新）、第10回・第11回と `TeamWorks回答_第10回_20261002.md` が（新・10/05）★。★完了率の式（N-3）・清掃結果の値（N-2）・時刻（N-14）の回答は第10回★ |
| `docs/handoff/PRレビュー手順_20261001.md` | ★**（新・10/05）**★ PR のレビュー手順（レビュー担当は daichi。D-154） |
| `docs/handoff/TeamWorks待ちで止まっていること_20260930.md` | **（新）** TeamWorks の回答待ちで止まっている作業の一覧 |
| `docs/handoff/セッション引き継ぎ_*`（2本）/ `セキュリティ申し送り_20260916.md` / `第9回送付手順_20260916.md` | **（新）** 作業の引き継ぎと申し送り |
| `docs/handoff/UI差分第3章_調査_*` / `UI差分第4章_調査_*` | **（新）** 画面差分の調査（2本） |
| `docs/handoff/テストDBのデータ出所_20260917.md` | **（新）** ★「テストDB」の行が当方のダミーだった件の記録★（[CLAUDE.md](../CLAUDE.md) §6-6 の由来） |
| `docs/handoff/点検記録_決まっていないこと_20260917.md` | **（新）** 点検記録の未決事項（D-150 の前提） |
| `docs/handoff/規模前提の見直し_20260917.md` / `管理画面_設計検討_20260910.md` | **（新）** 規模前提と管理画面の検討メモ |
| `docs/handoff/PR説明_develop_to_main_骨子.md` | develop → main の PR 説明の骨子 |
| `docs/handoff/analysis/`（10本） | 実装解析・DDL差分・テーブル定義の分析資料。★`error-type-mapping.md` は参考資料で実装の下敷きではない（D-077）★ |
| `docs/handoff/pdf/`（`make_*` / `check_*` の Python 20本 ＋ README） | TeamWorks 送付用 PDF の生成・検査スクリプト。★`*qa9*` の2本が（新）、`*qa10*` の2本が（新・10/05）★。PDF 本体は git に入らない |


---

## 付録B. 画面から引く逆引き索引

「この画面のここを直したい」というときに開くファイルの対応表です。空欄（`—`）はそのファイルが存在しないことを意味します。★`lib/db/index.ts` の行番号は 2026-10-05（`a1d7e43`）時点★で、Prisma 版（実DB）のメソッドの位置です。

| サイドバー表示名 | URL | 見た目を直すファイル | 数字の計算を直すファイル | データの取り出しを直すファイル | 数字の出どころ（テーブル） | API |
|---|---|---|---|---|---|---|
| （入口・メニューに出ない） | `/` | `app/page.tsx` | — | — | — | — |
| （ログイン・メニューに出ない） | `/login` | `app/login/page.tsx` | — | `lib/auth/token.ts` `lib/auth/cookie.ts` | — | `POST /api/auth/login` |
| 📊 ダッシュボード | `/dashboard` | `components/features/dashboard/DashboardScreen.tsx`<br/>`RescueRequiredCard.tsx`<br/>`ScheduleProgressCard.tsx`<br/>`SecurityBanner.tsx` | `lib/dashboard/aggregate.ts`<br/>★`lib/dashboard/latest-run.ts`★<br/>`lib/dashboard/rescue-visibility.ts`<br/>`lib/db/rescue-mapping.ts`<br/>`lib/judge/` | `lib/db/index.ts` の `cleanLogs()` `:990` / ★`cleanLogRunMeta()`★ / `schedules()` `:2188` / `dsAlerts()` `:2075` / `rescueRequired()` `:2254`<br/>`lib/db/consecutive-source.ts` | `clean_timetables` `clean_task_reports` `robot_error_reports` `clean_schedules` `facilities` | `GET /api/dashboard`<br/>`GET /api/clean-logs/{id}/advice`<br/>`GET /api/clean-logs/{id}/areas`（★`GET /api/clean-logs` は呼ばない。D-159★） |
| 📋 清掃詳細ログ | `/detail-log` | `components/features/detail-log/DetailLogScreen.tsx`<br/>`CleanLogTable.tsx`（列幅は `CleanLogTable.module.css`）<br/>`AreaModal.tsx`<br/>`components/ui/date-picker.ts` | `lib/clean-logs/query.ts`<br/>`lib/clean-logs/areas.ts`<br/>`lib/clean-logs/csv.ts`<br/>★`lib/csv.ts`★<br/>`lib/db/clean-log-mapping.ts`<br/>`lib/format/clean-result.ts`<br/>★`lib/format/precheck-status.ts`★<br/>`lib/judge/` | `lib/db/index.ts` の `cleanLogs()` `:990` / `areaCompletions()` `:1358`<br/>`lib/db/consecutive-source.ts`<br/>`lib/db/fixture-clean-logs.ts` | `clean_timetables` `clean_task_reports` `robot_error_reports` `facilities` `sections` `robots` `devices` | `GET /api/clean-logs`<br/>`GET /api/clean-logs/export`<br/>`GET /api/clean-logs/{id}/advice`<br/>`GET /api/clean-logs/{id}/areas` |
| 🔔 アラート | `/alerts` | `components/features/alerts/AlertsScreen.tsx`<br/>`components/ui/date-picker.ts` | `lib/prechecks/query.ts`<br/>`lib/prechecks/status-update.ts`<br/>★`lib/prechecks/edit-drafts.ts`★<br/>`lib/db/precheck-mapping.ts`（★`mainRunSucceededFromReport()`★）<br/>`lib/dashboard/aggregate.ts`（`SUCCESS_GRADES`） | `lib/db/index.ts` の `prechecks()` `:1491` / `precheckItemById()` `:1750` / `updatePrecheckErrorColumns()` `:1815`<br/>`lib/db/robot-error-write.ts`（★書き込み★）<br/>`lib/db/fixture-prechecks.ts` / `lib/db/fixture-precheck-overrides.ts` | `robot_error_reports`（★4列を書く★）`clean_timetables` `clean_task_reports` `facilities` `robots` | `GET /api/prechecks`<br/>`PATCH /api/prechecks/{id}/status` |
| 🚨 エラー一覧（★10/06★） | `/errors` | `components/features/errors/ErrorsScreen.tsx`<br/>`ErrorContentPicker.tsx`<br/>`components/ui/date-picker.ts` | `lib/error-reports/query.ts`<br/>`lib/error-reports/label.ts`<br/>`lib/errors/status-update.ts`<br/>★`lib/errors/edit-drafts.ts`★<br/>`lib/errors/error-content-label.ts`<br/>`lib/errors/error-content-picker.ts`<br/>`lib/db/error-report-mapping.ts` | `lib/db/index.ts` の `errorReports()` `:1833` / `errorReportById()` `:1875` / `updateErrorReportColumns()` `:1902`（`f02eb1d`）<br/>`lib/db/robot-error-select.ts`（`ROBOT_ERROR_LIST_*_SELECT`）<br/>`lib/db/robot-error-write.ts`（★書き込み★）<br/>`lib/db/fixture-error-report-overrides.ts` | `robot_error_reports`（★5列を書く★。`error_list_id` を含む）`error_lists` `clean_timetables` `clean_timetable_histories` `facilities` | `GET /api/errors`<br/>`GET /api/error-lists`<br/>`PATCH /api/errors/{error_id}/status` |
| 📈 レポート | `/reports` | `components/features/reports/ReportsScreen.tsx`<br/>`KpiCard.tsx`<br/>`FacilityPerformanceTable.tsx` | `lib/reports/query.ts`<br/>`lib/reports/target-customer.ts`<br/>`lib/reports/block-visibility.ts`<br/>★`lib/reports/facility-performance-csv.ts`★<br/>★`lib/reports/facility-performance-labels.ts`★<br/>★`lib/csv.ts`★<br/>`lib/db/report-mapping.ts`<br/>`lib/format/period.ts`<br/>`lib/judge/kpi.ts` | `lib/db/index.ts` の `reportKpis()` `:2385` / `facilityPerformance()` `:2608` / `continuousProblems()` `:2885` / `errorBreakdown()` `:2965`<br/>★`lib/reports/facility-performance-load.ts`（表と CSV で共有）★<br/>`lib/db/fixture-reports.ts` | 確定期間: `customer_summaries` `facility_summaries` `facility_error_summaries`<br/>当期: `clean_timetables` `clean_task_reports` `robot_error_reports` | `GET /api/reports/kpi`<br/>`GET /api/reports/facility-performance`<br/>★`GET /api/reports/facility-performance/export`（CSV）★<br/>`GET /api/reports/continuous-problems`<br/>`GET /api/reports/error-breakdown` |
| 📐 清掃面積 | `/clean-area` | `components/features/clean-area/CleanAreaScreen.tsx`<br/>`components/ui/date-picker.ts` | `lib/judge/kpi.ts`（`runCompletionRate()`）<br/>`lib/clean-area/aggregate.ts`<br/>`lib/clean-area/query.ts`<br/>`lib/clean-area/visibility.ts`<br/>`lib/clean-area/anomaly.ts`<br/>`lib/format/number.ts` | `lib/db/index.ts` の `cleanArea()` `:2324`<br/>`lib/db/clean-area-source.ts`<br/>`lib/db/fixture-clean-area.ts` | `clean_task_reports`（面積は全行・完了率は予定ありの行）`clean_timetables` `facilities` `customers` `groups` | `GET /api/clean-area` |
| 🔧 点検記録（★メニュー非表示★） | `/inspections` | `app/(dashboard)/inspections/page.tsx`<br/>`components/layout/PlaceholderScreen.tsx` | — | `lib/db/index.ts` の `inspections()` `:2227` / `inspectionDetail()` `:2230`（★Prisma版は未実装★）<br/>`lib/fixtures/inspections.ts` | ★メインDBには置かない★（独自ストアは未着手） | ★なし★ |
| 🔒 D-Secログ | `/ds-logs` | `components/features/ds-logs/DsLogScreen.tsx` | `lib/ds-devices/query.ts`<br/>`lib/db/ds-status.ts` | `lib/db/index.ts` の `dsDevices()` `:1956` / `dsDeviceExists()` `:2055`<br/>`lib/settings/ds-device-notes.ts` | `controls` `control_reports` `facilities`<br/>備考: `data/settings/ds-device-notes.json` | `GET /api/ds-devices`<br/>`PATCH /api/ds-devices/{serial_number}/note` |
| ⚠ D-Secアラート | `/ds-alerts` | `components/features/ds-alerts/DsAlertsScreen.tsx` | `lib/ds-alerts/query.ts`<br/>★`lib/ds-alerts/edit.ts` `lib/db/ds-alert-mapping.ts`★ | `lib/db/index.ts` の `dsAlerts()` ★`:2075`★ | ★`robot_error_reports` `facilities` `sections`★ | `GET /api/ds-alerts`・★`PATCH /api/errors/{error_id}/status`★ |
| 🏢 施設ロボットマスタ | `/facilities` | `components/features/facilities/FacilitiesScreen.tsx` | `lib/facilities/query.ts`<br/>`lib/format/robot-model.ts`<br/>`lib/format/contract.ts` | `lib/db/index.ts` の `facilities()` `:2135` | `facilities` `robots` `devices` `customers` `groups` | `GET /api/facilities` |
| 📅 スケジュール | `/schedules` | `components/features/schedules/SchedulesScreen.tsx` | `lib/schedules/query.ts` | `lib/db/index.ts` の `schedules()` `:2188` | `clean_schedules` `facilities` `sections` `robots` | `GET /api/schedules` |
| ⚙ システム管理 | `/system` | `components/features/system/SystemScreen.tsx` | `lib/settings/advice-validation.ts`<br/>`lib/config/advice-rules.ts` | `lib/settings/advice.ts`<br/>`lib/settings/display-limits.ts`<br/>`lib/settings/json-store.ts` | ★DBは読まない★（`lib/config/` と `data/settings/`） | `GET /api/settings/advice`<br/>`GET/PATCH /api/settings/display-limits` |

**全画面に共通**

| 直したいもの | ファイル |
|---|---|
| サイドバー・ロゴ・テーマ切替・JP/EN切替 | `components/layout/AppShell.tsx` / `app/(dashboard)/layout.tsx` |
| 画面を包む器（全体） | `app/layout.tsx` |
| 分類（ホールディングス／顧客）の絞り込み | `lib/hooks/use-classification-filter.ts` / `lib/masters/options.ts`（`GET /api/groups` `:99` / `GET /api/customers` `:100`） |
| サーバーとの通信 | `lib/api/client.ts` |
| 認証・IP制限 | `proxy.ts` / `lib/proxy/authenticate.ts` / `lib/config/allowed-ips.ts` |
| 共通部品（Pill / StatCard / FilterButton / Pagination / ProgressBar / GradeBadge） | `components/ui/` |
| 閾値・上限などの設定値 | `lib/config/index.ts`（[章6](#6-変えると挙動が変わる数値--libconfig)） |
| 日本語/英語の文言 | `lib/i18n.ts` |
| ◎○△×・L1〜L3・KPIの計算 | `lib/judge/` |
| 契約フラグの読み方 | `lib/format/contract.ts`（★メニューの出し分けには未使用★） |
| サンプルデータ | `lib/fixtures/`（[docs/fixtures.md](fixtures.md)） |


---

## 付録C. 設定値・定数の早見表

「この数字を変えたい」から引く索引です。値の意味は章6、根拠の略号（合意 = TeamWorks 合意・回答 / 指示 = ユーザー指示 / DFA = DFA の判断 / 型 = DB の列の型）は章6の冒頭を見てください。★行番号は 2026-10-05 時点の `develop`（`a1d7e43`）★。

### 判定・エスカレーション

| 変えたい数字 | 現在の値 | ファイル:行 | 定数名 | 根拠 |
|---|---|---|---|---|
| ◎ の基準（★以上★） | 90 | `lib/config/index.ts:20` | `GRADE_THRESHOLDS.excellent` | 合意（§4.1）＋ 境界は指示（D-139） |
| ○ の基準（★以上★） | 70 | `lib/config/index.ts:22` | `GRADE_THRESHOLDS.good` | 同上 |
| × の基準（未満） | 10 | `lib/config/index.ts:24` | `GRADE_THRESHOLDS.poor` | 同上 |
| 1回ごとの完了率の上限 | 100 | `lib/config/index.ts:39` | `RUN_COMPLETION_RATE.cap` | 合意（第10回 N-3）。★確定★（D-155 → D-158） |
| 1回ごとの完了率の残す小数の桁 | 1（小数第1位まで切り捨て） | `lib/config/index.ts:40` | `RUN_COMPLETION_RATE.decimals` | 同上 |
| L2 に上げる連続日数 | 2 | `lib/config/index.ts:45` | `ESCALATION_THRESHOLDS.level2Days` | 合意（§4.5） |
| L3 に上げる連続日数 | 3 | `lib/config/index.ts:47` | `ESCALATION_THRESHOLDS.level3Days` | 合意（§4.5） |
| 完了率「急落」の割合（要レスキュー R-3） | 0.2（20%） | `lib/config/index.ts:66` | `COMPLETION_DROP.dropRatio` | 指示 2026-09-15（D-135） |
| 未実施が何日連続で要レスキューか（R-1） | 3 | `lib/config/index.ts:88` | `RESCUE_THRESHOLDS.notExecutedDays` | 指示 2026-09-15（D-135） |
| 稼働率の 良好 / 注意 の境目 | 90 / 75 | `lib/config/index.ts:553-558` | `FACILITY_STATUS_THRESHOLDS` | DFA（暫定。D-117） |
| エラー率セルを赤くする目安 | 8 | `lib/config/index.ts:573` | `FACILITY_TABLE_THRESHOLDS.errorRateWarning` | モック `:818` |
| 未実施と判定するまでの猶予 | 30 | `lib/config/index.ts:197` | `LAUNCH_GRACE_MINUTES`（⚠️ ★死に定数。変えても何も起きない★） | D-025（暫定）→ D-091 項1 で不使用 |
| ★ダッシュボードの「直近の清掃」を何日前まで遡るか★ | 7 | `lib/config/index.ts:215-221`（`a961829`） | `DASHBOARD_LATEST_RUN` | D-159（7日はユーザー指示）。★旧 `settleMinutes`（30分）は D-173 で削除★ |
| ★結果待ちの長さ（予定の終了からの分）★ | 120 | `lib/config/index.ts:207`（`a961829`） | `RESULT_WAIT_MINUTES` | ユーザー判断 2026-10-07（D-173）。★暫定★（第12回（★R-2★。`04f974a` で送付版）で見直す） |

### 連続日数

| 変えたい数字 | 現在の値 | ファイル:行 | 定数名 | 根拠 |
|---|---|---|---|---|
| 連続日数の表示上限（既定） | 30（3〜365） | `lib/config/index.ts:150-157` | `CONSECUTIVE_DAYS_DISPLAY`（★画面から編集可★） | 指示 2026-09-13（D-130） |
| 連続日数のために何日さかのぼるか | 上限 × 7 ＋ 1 | `lib/config/index.ts:184-189` | `CONSECUTIVE_LOOKBACK` | DFA（D-131） |

### 画面に出す件数・期間・入力の上限

| 変えたい数字 | 現在の値 | ファイル:行 | 定数名 | 根拠 |
|---|---|---|---|---|
| 要レスキューの初期表示件数 | 5 | `lib/config/index.ts:119` | `RESCUE_INITIAL_VISIBLE` | 指示 2026-09-15（D-137） |
| 清掃面積の内訳の初期表示件数 | 10 | `lib/config/index.ts:823` | `CLEAN_AREA_INITIAL_VISIBLE` | DFA（指示書AN） |
| 清掃詳細ログの期間上限 | 731日 | `lib/config/index.ts:444` | `MAX_DATE_RANGE_DAYS` | 指示 2026-09-15（D-136） |
| CSV出力の行数上限 | 20,000 | `lib/config/index.ts:471`（`a961829`） | `MAX_CSV_ROWS`（★清掃詳細ログ・レポートの2つの CSV で共有。D-163★） | 指示 2026-09-15（D-136）。値は暫定（D-144） |
| 一覧の1ページ件数 / 上限 / ページボタン数 | 50 / 200 / 7 | `lib/config/index.ts:409-425` | `PAGINATION` | API契約（`d-watch-api.yaml:1828`） |
| 受け付ける西暦の範囲 | 2000〜2100 | `lib/config/index.ts:507-512` | `REPORT_YEAR` | DFA |
| `in` 句の分割単位 | 1,000 | `lib/config/index.ts:550` | `DB_IN_CLAUSE_MAX_ITEMS` | DFA（指示書AC） |
| D-Sec備考の文字数上限 | 200 | `lib/config/index.ts:590` | `DEVICE_NOTE_MAX_LENGTH` | DFA（暫定） |
| アラートの備考の文字数上限 | 400 | `lib/config/index.ts:587` | `PRECHECK_NOTE_MAX_LENGTH`（★`d-watch-api.yaml:789`（`/errors` 側は `:918`）の `maxLength` と揃える★） | DFA（指示書AT §3-6） |
| ★エラー内容で備考が必須になる id★ ★新（10/27）★ | 131（その他(該当なし)） | `lib/config/index.ts:618` | `ERROR_LIST_NOTE_REQUIRED_IDS` | ユーザー（2026-10-27。D-184） |
| ★エラー内容を選べない種別★ ★新（10/27）★ | `D-S_CONTROL_FAILURE` / `SECURITY_ON` | `lib/config/index.ts:627` | `ERROR_LIST_UNSELECTABLE_ERROR_TYPES` | ユーザー（2026-10-27。D-184） |
| アドバイス見出し / 本文 / 行動 / 件数の上限 | 100 / 400 / 100 / 10 | `lib/config/index.ts:638` / `:640` / `:642` / `:644` | `ADVICE_TITLE_MAX_LENGTH` ほか | DFA（D-124） |
| URLのIDの上限 | 9007199254740991 | `lib/config/index.ts:674` | `PATH_ID_MAX` | DFA（JavaScript の限界） |
| シリアル番号の文字数上限 | 100 | `lib/config/index.ts:691` | `PATH_SERIAL_NUMBER_MAX_LENGTH` | 型（`controls.serial_number` `varchar(100)`） |
| アラートの id（`error_id`）の文字数上限 | 50 | `lib/config/index.ts:703` | `PATH_ERROR_ID_MAX_LENGTH` | 型（`robot_error_reports.error_id` `varchar(50)`） |
| 成功率カードの目標表示 | 95 | `lib/config/index.ts:577` | `SUCCESS_RATE_TARGET` | モック `:494` |

### 対応表・表示

| 変えたい数字 | 現在の値 | ファイル:行 | 定数名 | 根拠 |
|---|---|---|---|---|
| DBの時刻をJSTの壁時計値として読む時差 | 540分 | `lib/config/index.ts:236` | `DB_DATETIME_OFFSET_MINUTES` | 合意（第8回 A2）。D-115 / D-155 項4 |
| 事前チェックのタイミング | 60分前・30分前 | `lib/config/index.ts:261` | `PRECHECK_MINUTES_BEFORE` | 指示 2026-09-11 / 合意（第7回。D-112） |
| 本清掃を表す `precheck` の値 | 0 | `lib/config/index.ts:243-246` | `CLEAN_TIMETABLE_PRECHECK` | 合意（第7回。D-112） |
| 旧・清掃結果コードのラベル | 0〜4 | ★削除（2026-10-19。D-182）★ | 旧 `CLEAN_ERROR_LABELS` / `cleanErrorLabel()`（本番で未使用だった。コード値 `CLEAN_ERROR` は残す） | 定義書（D-146） |
| 清掃結果（`clean_task_reports.status`）の表示名 | 0〜5 | `lib/config/index.ts:348-361` / `:373-380` / `:386` | `CLEAN_TASK_STATUS` / `CLEAN_TASK_STATUS_LABELS` / `cleanTaskStatusLabel()` | 合意（第10回 N-2。D-155 項2）。★判定には使わない★ |
| エラー記録が無いときの代替値（★予定行が無ければ `null`。D-171★） | `Standby.` | `lib/config/index.ts:392` | `ROBOT_STATUS_STANDBY` | 合意（第2回。D-098） |
| メーカー番号→名前 | 1 PUDU / 2 Keenon | `lib/config/index.ts:715-718` | `MAKER_NAMES` | DFA（暫定。D-085） |
| 対応機種の追加 | CC1 のみ | `lib/config/index.ts:738-743` | `ROBOT_MODELS[].supported` | 指示 2026-09-09（D-087） |
| 契約フラグの桁位置 | D-Security 0 / D-Watch 1 | `lib/config/index.ts:751-754` | `CONTRACT_DIGITS` | D-080 |
| 「グループなし」を表す値 | 0 | `lib/config/index.ts:769` | `GROUP_ID_NONE` | 合意（D-011） |
| ロボット状態の色 | 日本語6値 | `lib/config/index.ts:794-801` | `ROBOT_STATUS_TONES`（★実データではほぼ全部グレー★） | fixtures 由来（D-077） |
| 面積の表示単位 | ㎡ | `lib/config/index.ts:815` | `AREA_UNIT` | 指示 2026-09-16（TeamWorks 未確認。D-141） |

### アドバイス・アクセス制限

| 変えたい数字 | 現在の値 | ファイル:行 | 定数名 | 根拠 |
|---|---|---|---|---|
| アドバイス文言の既定値 | R1〜R4・B1〜B3 | `lib/config/advice-rules.ts:34-127` | `DEFAULT_ADVICE_RULES` | R は合意（§4.5）、B はモック |
| B2（ハンドルアップ）の発火値 | `Handrail Lifted` | `lib/config/robot-status-match.ts:35` | `B2_HANDLE_UP_STATUSES` | 指示（暫定。D-078） |
| B3（オフライン）の発火値 | 空 | `lib/config/robot-status-match.ts:46` | `B3_OFFLINE_STATUSES`（★空＝発火しない★） | 値が未判明（D-078） |
| 許可するIPアドレス | 空 | `lib/config/allowed-ips.ts:24-26` | `ALLOWED_IPS` | D-079。★リリース直前の対象（D-154）★ |
| IPリストが空のときの扱い | `true`（制限しない） | `lib/config/allowed-ips.ts:35` | `ALLOW_ALL_WHEN_EMPTY` | D-101。★リリース直前の対象（D-154）★ |
| 直接アクセスを通すか | 未設定＝拒否 | `lib/config/allowed-ips.ts:52` | `ALLOW_DIRECT_ACCESS`（★環境変数由来★） | D-101 |
| 信頼するプロキシの段数 | 1 | `lib/config/allowed-ips.ts:63` | `TRUSTED_PROXY_HOPS` | 指示 2026-09-08（D-079） |

### `lib/config/` の外にある数値（★ここだけ例外★）

| 変えたい数字 | ファイル:行 | 定数名・中身 |
|---|---|---|
| 連続日数の表示上限の**保存値** | `data/settings/display-limits.json` | ファイル（★git に入らない★）。読むのは `loadConsecutiveDaysMax()`（`lib/settings/display-limits.ts:223`） |
| アドバイス文言の**保存値** | `data/settings/advice.json` | ファイル（★git に入らない★）。読むのは `loadAdviceRules()`（`lib/settings/advice.ts:110`） |
| 浮動小数の誤差を吸収する桁 | `lib/judge/kpi.ts:131` | `FLOAT_ERROR_DIGITS`（5）。★仕様の閾値ではなく計算上の都合なので `lib/config` に置かない★（指示書AS D3）。`0.57 / 1 * 100` が `56.99999…` になるのを 57.0 に戻すため |
| レポートの「もっと表示」の件数 | `components/features/reports/ReportsScreen.tsx:46` / `:55` | `DEFAULT_LIMIT`（10）/ `SHOW_ALL_LIMIT`（＝ `PAGINATION.maxPerPage`） |
| 空欄の表示文言 | `lib/format/raw-label.ts:19` | `EMPTY_VALUE_LABEL`（`"(値なし)"`） |

