# CLAUDE.md — D-Watch 開発コンテキスト

このファイルは Cursor / Claude Code が最初に読むプロジェクトコンテキストです。
**実装前に必ず全文を読んでください。** 特に「4. 確定仕様」と「7. 禁止事項」は仕様のズレが直接バグになる箇所です。

---

## 0. ルールの優先順位

セッション開始時に自動注入される全社共通の作業ルールと、本ファイルの記述が衝突した場合は
以下を優先する。

1. **仕様が不明なら止まる。**
   全社ルールの「自動モード中は許可待ちで止まらない」は*権限確認*についての取り決めであり、
   *仕様の不明点*には適用しない。§7 の通り、推測で実装せず確認を求める。
   判定基準・閾値・API契約の不明点は特に危険（仕様のズレが直接バグになる）。

2. **判定ロジックは「期待値表の確定 → テスト作成 → 実装」の順を崩さない。**
   実装を分担・委譲する場合も、**期待値表の確定は分担しない**（§4.3 / §6.2）。
   境界値（完了率 90.0 / 90.1 / 70.0 / 70.1 / 10.0 / 9.9 / 0 / 未実施、
   および `89.5 → 表示90% だが判定は ○`）は本ファイル §9.4 の表を正とする。

3. **日本語を含む成果物には日本語フォントを明示する。**
   欧文フォント（Geist 等）は日本語グリフを持たないため、指定しないと
   フォールバックで中国語フォントに落ち、漢字が中華字形で表示される。
   `app/globals.css` のフォントスタックを正とし、日本語フォントを外さない。

---

## 1. プロダクト概要

**D-Watch（DFAロボット監視システム）** は、店舗等に配置された清掃ロボットの稼働を監視するSaaSビューアーです。

- 夜間に自動実行された清掃の結果を、**翌朝DFA運用担当が確認する**「翌朝確認型」の運用
- 問題のある施設を検知し、AIが推定原因と対応アドバイスを提示する
- オプションモジュール **D-Security**（セキュリティ解除→清掃→復帰の自動化）の監視画面も含む

**P1スコープ**
| 項目 | 範囲 |
|---|---|
| 対象顧客 | 株式会社ドン・キホーテのみ |
| ロボットメーカー | PUDU Roboticsのみ（KeenonはP2以降。Whiz iは機能的に劣るため取り扱わない） |
| 対応機種 | PUDU CC1のみ（将来候補: PUDU MT1 / Keenon C30 / C40。`lib/config/index.ts` の `ROBOT_MODELS` が正。D-087） |
| 利用者 | DFA社内のみ（顧客アカウントはP2以降） |
| セキュリティ会社 | SECOMのみ |

---

## 2. 責任分界 — 何を作り、何を作らないか

| 領域 | 担当 |
|---|---|
| MySQLデータベース本体（設計・構築・運用） | **TeamWorks**（外部エンジニア会社） |
| データ収集処理（Pudu Cloudポーリング/コールバック、D-S制御盤報告のDB書き込み） | **TeamWorks** |
| サマリ集計バッチ（顧客/店舗/障害サマリ） | **TeamWorks** |
| APIサーバー + ビューアー（本リポジトリ） | **DFA（内製）** |
| 点検記録機能（独自データストア・写真管理） | **DFA（内製）** |
| 当期（進行中の週/月/年）のレポート集計 | **DFA（内製）** |
| ★サーバー・ネットワークの用意と構築保守★（本番のアプリVM / DB / テスト環境） | ★**TeamWorks**★（D-143） |

★**DFA が作るのはビューアー（本リポジトリ）だけです**★（2026-09-16 ユーザー確認。D-143）。
★**サーバーを契約し費用を払うのは DFA ですが、構築・保守は TeamWorks です**★。
★**D-100 項5 の「DFA が構築するのはアプリ側（VM）」は誤りなので従わないこと**★
（★ただし TeamWorks 側の明示確認は未取得。第9回 Q12-1 で照会中★）。

**重要:** メインDBのスキーマ変更・データ投入処理は本プロジェクトの仕事ではありません。
DBは基本的に**読み取り専用**で、書き込みは後述の4カラム（`robot_error_reports` は `error_list_id` を含む5カラム）のみです。

**「APIサーバー」の意味:** 本アプリ内の API ルート（`app/api/`）を指します。TeamWorks が API を
提供するわけではありません。DB へは Prisma で直接アクセスし、間に外部サービスは入りません。
画面はブラウザから自アプリの API ルートを呼び、その中で Prisma が DB を読みます（D-046）。

---

## 3. 技術スタック

| 項目 | 採用 |
|---|---|
| フレームワーク | **Next.js**（App Router、React + APIルート一体型） |
| 言語 | **TypeScript**（フロント・API共通） |
| DBアクセス | **Prisma**（MySQL） |
| メインDB | **MariaDB 10.11**（TeamWorks管理・**DFAはスキーマを変更しない**）。MySQL互換だが同一ではない。★下の注記★を必ず読むこと（D-100） |
| 点検記録ストア | SQLite + オブジェクトストレージ（**メインDBとは完全に分離**） |
| 認証 | P1は簡易トークン認証 + IP制限（社内利用のみ）。★IP制限は `X-Forwarded-For` の**末尾**を見る（nginx が追記する。D-077 項5）。**先頭は詐称可能なので絶対に使わない**★ |
| インフラ | アプリ: さくらのクラウド VM（**レンタルサーバではない**。Node.js常駐が必要なため）。DB: さくらインターネットの**データベースアプライアンス**（PostgreSQL か MariaDB の2択のため MariaDB）。★アプリ（VM）とDB（アプライアンス）が別サービスという理解だが、**明示の確認は未取得**★（D-100）。★**用意・構築保守はいずれも TeamWorks**★（D-143。第9回 Q12-1〜Q12-6 で照会中） |
| 開発用データ | **`lib/fixtures/` のサンプルデータ**（環境変数 `USE_FIXTURES` で実DBと切替） |

> ★**メインDBについての注記**★（根拠: D-100。TeamWorks 回答 2026-09-10）
> - ★**なるべく MySQL 互換の SQL を使う**★
> - ★★**MariaDB 特有の DML を使いたい場合は、その箇所を TeamWorks に知らせること**★★
> - ★ローカル検証環境は **MySQL 8.4**（`dwatch_local`）。本番は MariaDB 10.11 なので、
>   ★**ローカルで通っても本番で通らない可能性がある**★。生SQL（`$queryRaw` / `$executeRaw`）を
>   書くときは特に注意する★（現状、アプリコードに生SQLは1件も無い。すべて Prisma のクエリビルダ経由）

> ★★**接続先についての注記**★★（D-152。2026-10-01 ユーザー確認）
> - ★★`133.125.238.144` / `clean_watch` は **ステージング**★★。★**本番DBは別にある**★（2026-10-01 ユーザー回答
>   「これはステージングです、本番は他にあります」）
> - ★★**D-148（「本番DBそのもの」2026-09-17）は破棄**★★。★第9回までの文書の「ステージング」「テスト環境」という呼称は
>   ★正しかった★（D-148 項1 の「誤り」という訂正のほうが誤りだった）
> - ★★**2026-09-30 23:25〜10-01 00:09（JST）に実データが入った**★★（2026-10-01 実測・先方投入。読み取りのみ）。
>   ★当方のダミー（`scripts/seed-local-db.ts` 由来）は消えている★。
>   `facilities` は ★ドン・キホーテの実店舗10件＋本社★。
> - ★★**DB に入っている時刻の値は、列の型（TIMESTAMP / DATETIME）によらず JST の壁時計の値**★★
>   （ユーザー指摘 2026-10-01「データベース上の時間は全てJSTに変換済みです」。★実測で確認★: `robots.updated_at` の最新が生の値で
>   15:00、そのときの JST が 15:17・DB の `NOW()` が 06:17。生の値が UTC なら9時間後の未来になり、ありえない）。
>   ★**生の値に9時間を足さないこと**★。
>   - ★★**2026-10-02 TeamWorks 回答（第10回 N-14・N-6c）**★★: ★それまでサーバーの時計は UTC（`@@system_time_zone = UTC`）で、
>     一部のテーブルはフレームワークの機能を切って書いていたため、`current_timestamp()` の既定値で UTC の値が入っていた★。
>     ★DB の既定のタイムゾーンを 'Asia/Tokyo' に変え、データを再登録済み★（先方の申告。★当方の再実測は未了★ — 10-02 は認証エラーで接続できず）。
>     ★先方の結論: 「timezone を JST で統一したので、以降はズレは生じない」★
>   - ★当時の9時間ずれの正体★: `control_reports.reported_at` は ★UTC で書かれていた★（N-14a）。
>     `robot_error_reports.errored_at` は ★正しい JST の値で、ずれていたのは `created_at`（UTC）の側★（N-14b）。
>     ★旧記述の「`errored_at` は二重補正・エラー検知時刻は9時間遅く出る」は誤り★
>   - ★**アプリのコードは変えない**★（2026-10-02 調査）: アダプタは `dateStrings: true` で、サーバーが返した文字列を UTC とみなすだけ。
>     ★サーバーが JST になったことで、TIMESTAMP 列も JST の文字列で返り、`fromDbJst()` の前提どおりになる★。
>     ★残る危険は、再登録されなかった古い TIMESTAMP 行だけ★（そこだけ +9時間に見える）。★再実測で確かめること★
>   - ★一度「TIMESTAMP は UTC で返るので +9時間」と誤って書き（9c175a2）取り消した（d540335）経緯あり★。★推測で9時間ずらさないこと★
>   ★`clean_timetables` / `clean_schedules` / `clean_status_histories` は0行★、
>   `clean_task_reports` は2行（どちらも `clean_timetable_id` が NULL）。★サーバー本体は昨日と同一★（`@@hostname` 一致）
> - ★帰結★: ★予定が0行なので、清掃詳細ログ・ダッシュボード・アラートは空★。★総清掃面積だけは実績を直接集計するので2回と出る★
>   （画面どうしで食い違う。★詳細ログを予定起点・実績起点・両方のどれにするかは、データが入ってから検討★。D-151 項7）
>   ★以上は 2026-10-01 時点★。★2026-10-06 は予定が入り始め（先方投入中。予定と実績はまだつながっていない。D-158 末尾）、
>   ダッシュボードは「各清掃エリアの直近の清掃（7日以内）」（D-159）で11エリアを出した★（先方投入。`docs/handoff.md` 追記61）
> - ★`robot_error_reports.serial_number` の実値★（2026-10-19 監督の SELECT・先方投入）: セキュリティ（`SECURITY_ON`）・清掃開始失敗（`NOT_STARTED`）の行は ★`---`★（第11回 P-8 の回答は `0`）。★`serial_number` の値や `robot_id IS NULL` で区分を見分けない — `error_type` で見分ける★（D-180 項5）。どちらに揃うかは TeamWorks に照会予定（指示書AX §12-1-2）
> - ★接続元の制限・TLS（第9回12章）は★**検証段階のため保留。顧客へ配布するときに検討する**★（ユーザー判断 2026-10-01）。
>   ★**セキュリティの強化はリリース直前にまとめて行う**★（ユーザー判断 2026-10-01。D-154）。★その時の対象★: 接続元の制限・TLS（または閉域）、★ステージングDBのパスワード変更★（コメントに実物が入っていた。履歴に残る）、★`ALLOW_ALL_WHEN_EMPTY` を拒否に倒すか許可リストを入れる★（`lib/config/allowed-ips.ts`）、★ログインの試行回数制限と cookie の中身★（`lib/auth/cookie.ts`）、★制御盤のログ中継（作ったら）: 読めるのはログのフォルダだけ・読み取り専用・制御盤ごとの認証★／★許可リストを入れると店舗の制御盤（`/api/ds-agent/`）が弾かれる — 店舗の IP を載せるか、その経路だけ IP 制限を外してトークン必須にするか★（D-179。未実装）／★CSP を入れるなら `app/layout.tsx` の `<head>` のインラインスクリプト（表示設定の復元。D-183）に nonce が要る★
>   ★第10回では照会しない★。★本番を組むときは、アプリとDBを閉域でつなぐか、DBに TLS を入れるかのどちらかが要る★
> - ★★**ステージングには、4カラム（`status` / `comment` / `responded_at` / `resolved_at`）の UPDATE を書き込んでよい**★★
>   （ユーザー指示 2026-10-01「書き込んでよい」）。★実データ（ドン・キホーテのエラー行）の対応状況が書き換わることを承知のうえ★。
>   ★**4カラム以外への書き込みは引き続き禁止**★（§7。権限も無い）。★例外は `robot_error_reports.error_list_id` だけ★（下の「DB接続アカウント」。D-184）。★`scripts/seed-local-db.ts` は★ステージングに絶対に向けない★
>   （テーブルを消してから入れ直すため、実データが消える。`lib/db/local-guard.ts` が `dwatch_local` 以外を拒否する）
> - ★旧: 「本番DBへは書き込まない（2026-09-17）」「書き込んで安全な環境ができるまで着手しない」★ — D-152 で解除

### DB接続アカウント（2種）
1. **参照用**: 全テーブル SELECT のみ
2. **更新用**: `robot_error_reports` / `control_error_histories` の
   `status`, `comment`, `responded_at`, `resolved_at` **のみ** UPDATE 可
   - ★加えて `robot_error_reports.error_list_id`（エラー内容。`error_lists.id`）★（D-184。2026-10-27 ユーザー回答「すでに更新権限があると思う」。
     ★`SHOW GRANTS` での確認は未了★ — ステージングに接続できないため）。★`control_error_histories` には無い列★

この4カラム（`robot_error_reports` は `error_list_id` を含む5カラム）以外への書き込みコードを書かないでください。権限エラーになります。
★`error_list_id` を書くのはエラー一覧の `PATCH /errors/{error_id}/status` だけ★（事前チェックの `/prechecks` と D-Sec の保存は4カラムのまま。`lib/db/robot-error-write.ts`）。

---

## 4. 確定仕様 — 実装時に必ず守るルール

> ここに書かれた数値・ロジックは TeamWorks との合意事項です。
> 変更したくなった場合は勝手に変えず、`docs/decisions.md` を確認し、必要なら合意を取り直してください。

### 4.1 判定基準（◎○△×）
清掃完了率にもとづく4段階判定。**閾値は設定値として外出しする**（ハードコード禁止）。

| 判定 | 条件 |
|---|---|
| ◎ | 完了率 90%以上 |
| ○ | 完了率 70%以上 |
| △ | 完了率 10%以上 70%未満 |
| × | 完了率 10%未満、または未実施 |

★**境界はすべて「上のランクに入る」**★（ユーザー指示 2026-09-16「◯、×は90%以上のように境はふくめてください」。**D-139**）。
90.0 は ◎、70.0 は ○。★**× の境界だけは「10%未満」のまま**★（同日「10％未満を×としてください」で確定）なので、10.0 は △。
★**D-020b（境界はすべて下のランクに落ちる）は破棄**★。

### 4.2 KPI計算式（週次/月次/年次の集計期間内）

```
稼働率       = 正常終了（status = 4）の回数 ÷ 清掃予定回数 × 100   ← ★第11回 P-6=A（2026-10-05）★
出発成功率   = 開始できた回数（レポート行がある回数）÷ 清掃予定回数 × 100   ← ★時刻の条件なし（P-6=A）★
清掃完了率   = Σ 1回ごとの完了率 ÷ 1回ごとの完了率が null でない清掃の回数   ← ★1回ごとの完了率 = min(clean_area ÷ task_area × 100, 100) を小数第1位までで切り捨て★
エラー発生率 = エラー発生回数 ÷ 清掃予定回数 × 100   ← ★分母は予定回数（P-6=B）★
```

★**3つの率はサマリ（`operation_ratio` / `launch_ratio` / `error_ratio`）と同じ式**★（第11回 P-6。ユーザー指示 2026-10-05「合わせてください」。D-158）。★旧: 稼働率 ＝ 実施回数 ÷ 予定・出発成功率 ＝ 定刻出発回数 ÷ 予定（実際は同じ値になっていた）・エラー発生率 ＝ ÷ 実施回数★。実装は指示書AU

★**清掃完了率は「1回ごとの面積比（上限100・小数第1位まで切り捨て）」の単純平均**★（★**ユーザー判断 2026-10-02「一旦B」。D-155**★）。
★TeamWorks のサマリと同じ式★（第10回 N-3 の回答）。★`clean_task_reports.clean_ratio` 列は使わない★（先方が「計算式が間違っている。訂正したら回答する」）
- ★**確定**★（第11回 P-2=A「今の式のまま」。P-1: 訂正後の `clean_ratio` 列もこの式と同じ値になる。2026-10-05。D-158）。★D-151 の「`clean_ratio` 列の単純平均」は破棄★。
  ★切り替えが1関数で済むよう、1回ごとの完了率を決める関数は1つにする★（`lib/judge/kpi.ts`。指示書AS）
- ★既知の弱点（承知のうえで採用）★: `clean_area` は★同じ場所を複数回清掃した面積の総和★（N-4）なので、面積比は清掃の網羅率ではない。
  ユーザー提供CSV 189行では ★`clean_area > task_area` が 129行（68.3%）で、これらは全部 100% になる★。★◎が多めに出る★
- ★全画面で同じ定義★（レポートKPI 当期 / 総清掃面積 F-08 / エリア別完了率 / 要レスキュー R-3 / 詳細ログの1行）
- ★対象は本清掃（`precheck = 0`）に対応する行と、予定外のタスク★。★事前チェックの回は別の1件として数えない★。
  - ★★予定外のタスク（`clean_timetable_id` が 0 か NULL）も1件として数える（A案）★★（ユーザー指示 2026-10-29「エラー一覧以外も、予定外タスクを計算して出すようにしてください」「割合はA案で」。指示書BG）:
    予定外1件 ＝ 清掃予定回数 +1・開始できた回数 +1・`status = 4` なら正常終了 +1・1回ごとの完了率を平均に入れる。予定外のエラー（`clean_timetable_id` が 0 か NULL）もエラー発生回数・エラー内訳に入れる（期間は `errored_at ?? created_at`）。
    予定外の期間の振り分けは `started_at`（実際に始めた時刻）。★予定外は未実施・結果待ちにならない★
  - ★今の期間（アプリが計算）だけ★。★確定期間は TeamWorks のサマリのまま★（ユーザー「サマリは変えて貰う予定です」）。サマリが変わるまで、今の期間と確定期間で予定外の扱いが違う
  - ★旧（2026-10-02〜10-29）: 予定外は完了率にも実施回数にも入れず、総清掃面積の面積の合計にだけ含めていた（D-155 項3）★
- ★`task_area` が 0 の行は、1回ごとの完了率を 0% とする★（第11回 P-5=B。2026-10-05。実装は指示書AU）。★旧（D-155 の暫定）: null として分子にも分母にも入れない★
- ★未実施（レポート行が無い清掃）は分子にも分母にも入れない★（0% として混ぜない）
- ★切り捨ては「1回ごとの値」だけ★。★平均した値は丸めずに判定し、表示のときだけ整数に丸める★（§4.3）
- ★浮動小数の罠★: `0.57 / 1 * 100` は `56.99999…` になる。★切り捨ての前に誤差を吸収すること★（期待値表は指示書AS）

#### 4.2.1 ダッシュボードの清掃結果（ユーザー指示 2026-10-29「4の計算式は仕様書に残して」。D-049 / D-173。実装は `lib/dashboard/aggregate.ts` の `computeSummary()`）

```
成功率 = 成功 ÷ (成功 + 失敗 + 未実施) × 100      ← ★結果待ちは分母に入れない★。分母0なら null（画面は「-」）
```

| 区分 | 数える清掃 | 成功率での扱い |
|---|---|---|
| 成功 | 清掃結果（`clean_task_reports` の行）があり、判定が ◎○ | 分子・分母 |
| 失敗 | 清掃結果があり、判定が △×（★出発した清掃だけ★。未実施を二重に数えないため） | 分母 |
| 未実施（＝出発失敗） | 清掃結果が無く、予定の終了＋`RESULT_WAIT_MINUTES`（120分）を過ぎた | 分母 |
| 結果待ち | 清掃結果が無く、予定の開始 ≤ 今 ＜ 予定の終了＋120分 | 入れない（件数だけ出す） |

- ★対象の清掃★: 日付を指定しないとき（直近）は、★清掃エリアごとに1件★ — 開始が直近7日以内（`DASHBOARD_LATEST_RUN.lookbackDays`）で予定の終了を過ぎた本清掃のうち、開始が最新のもの（D-159）。日付を指定したときは、その日の本清掃すべて
- ★予定外のタスク（`clean_timetable_id` が NULL か 0）も1件として数える★（指示書BG。2026-10-29）。直近モードの候補は開始＝`started_at`・終了＝`ended_at`（null なら開始）。成功か失敗のどちらかになり、未実施・結果待ちにはならない。同じ清掃エリアで開始が同時刻なら予定のある行を採る（`lib/dashboard/latest-run.ts` の `preferOnTie()`）
- ★事前チェックの結果は使わない★（§4.4。D-170）
- ★丸めない★。表示のときだけ整数に四捨五入（§4.3）
- 進み具合（`schedule_progress`）= 対象日に出発した件数 ÷ 対象日の予定件数 × 100（予定件数0なら null）。★対象日は、日付を指定しないとき日本時間の前日、指定したときはその日★（ユーザー判断 2026-10-29「B」 — 清掃結果は前夜の分なので、今日の予定で数えない）。
  ★予定件数は予定の表（`clean_timetables` / `clean_timetable_histories` の本清掃）の行数★。★`clean_schedules` の曜日の設定は使わない★（旧。2026-10-29 ステージングで今日の予定が 7件 対 5件と食い違った）。結果待ち・未実施は予定件数にだけ入る。★対象日に始まった（`started_at`）予定外タスクは予定件数にも出発件数にも1件ずつ入る（A案。指示書BG）★。応答の `schedule_progress.date` が対象日。`lib/dashboard/aggregate.ts` の `scheduleProgressTargetDate()` / `computeScheduleProgress()`。★要レスキュー・セキュリティのバナーは当日のまま★

### 4.3 端数処理 ★事故りやすい★

- **表示**: 小数第1位を四捨五入して**整数**で表示する（例: 91.4% → `91%`）
- **判定**: ◎○△× と L1〜L3 の判定は、**丸める前の生値**で比較する

```ts
// ✅ 正しい
const grade = judgeGrade(rawRate);        // 生値で判定
const display = Math.round(rawRate);       // 表示用に丸める

// ❌ 間違い（89.5%が丸めで90%になり、本当は○なのに◎になる）
const rounded = Math.round(rawRate);
const grade = judgeGrade(rounded);
```

### 4.4 清掃開始/終了/失敗の判定

| 事象 | 判定条件 |
|---|---|
| 清掃開始 | RobotWorkStatusコールバック（清掃開始）受信、またはポーリングでステータスが「清掃中」に変化 |
| 清掃終了 | タスク実行結果（タスクレポート）の受信 |
| 清掃失敗 | 予定開始時刻 + **猶予30分**を経過してもタスク開始が確認できない（未実施）、またはタスクレポートが中断で返る |

猶予30分は暫定値。**設定値として外出しする。**

★**レポートがまだ無い清掃は「結果待ち」**★（ユーザー判断 2026-10-07。D-173）: 予定の開始 ≤ 今 ＜ 予定の終了＋`RESULT_WAIT_MINUTES`（120）のあいだは失敗に数えない（`lib/clean-logs/result-pending.ts` の1本。API は `CleanLogItem.result_pending`）。過ぎたら出発失敗（未実施）。★上の「猶予30分」（`LAUNCH_GRACE_MINUTES`）はコードで使っていない★（D-091）

★**事前チェックの結果も有無も、成功・失敗の判定に使わない**★（ユーザー指示 2026-10-07「事前チェックを行っていない清掃や事前チェックが失敗した清掃でも本清掃が成功していれば成功にしてください」。D-170 / D-048）。判定は本清掃のレポート行の有無と完了率だけ。`lib/dashboard/__tests__/aggregate.test.ts` が見張っている

★**「タスクレポートが中断で返る」は、いまは判定に使っていない**★（ユーザー判断 2026-10-02。D-155 項2）。`clean_task_reports.status`（2 清掃中断 / 3 清掃キャンセル / 5 清掃異常終了）は★表示名として出すだけ★で、◎○△× は完了率で決め、未実施（出発失敗）は★レポート行の有無★で決める（§4.7）。★実データが入ってから見直す★

### 4.5 エスカレーション（Phase A: R1〜R4）

| レベル | 条件 | 想定対応 |
|---|---|---|
| L1 | 初回の問題 | 現場へ連絡（障害物確認・ブラシ清掃・再起動等） |
| L2 | 2日連続 | 現場責任者へのヒアリング・運用手順の再教育・消耗品交換 |
| L3 | 3日以上連続 | DFAオンサイト訪問・マップ再作成・機体交換検討 |

| # | 条件 | アドバイス |
|---|---|---|
| R1 | 出発失敗（初回） | 事前チェックのアラート内容を確認 |
| R2 | 完了率×（10%未満） | 現場の通路状況＋ロボット状態を確認 |
| R3 | 同一施設で2日以上連続（出発失敗 or △以下） | 現場責任者に状況をヒアリング |
| R4 | 同一施設で3日以上連続（出発失敗 or △以下） | DFAオンサイト訪問を検討 |

**「連続」はカレンダー日ではなく「スケジュール存在日の連続」**で数えます。清掃予定がない日は連続を途切れさせません。

★**予定外のタスクも、その施設のその日（`started_at`）の結果に入れる**★（指示書BG。2026-10-29）— 連続日数・L1〜L3・要レスキュー・連続問題施設のすべて。★予定外だけの日も「スケジュール存在日」★（完了率 70 未満なら問題日、以上なら連続が切れる）。★予定外は未実施にならない★ので、要レスキューの「未実施の連続」は予定外だけの日で切れる。清掃詳細ログの予定外の行の連続日数は施設×日付で引く（`consecutiveProblemDaysByFacilityDate()`）

### 4.6 契約フラグ

- **`facilities.contract` のみが正**。★**`varchar(10)` の文字列**★であり、★**数値ではない**★（D-080）
  - ★**桁位置がサービスに対応する**★。★1桁目 = D-Security / 2桁目 = D-Watch★。
    ★契約済み（有効）なサービスの桁を `1` にする★
  - 値の例: `0000000000` = 未設定（既定値） / `1000000000` = D-Securityのみ /
    `0100000000` = D-Watchのみ / ★`1100000000` = 両方★
  - 3桁目以降は将来のサービス用の予備と読める（★定義書に明記なし。P1では読まない★）
  - ❌ ★**数値として比較しないこと**★。`Number("0000000000")` は**例外を出さずに** `0` になり、
    `contract === 3` のような比較は**絶対に真にならない**。結果として
    ★**全施設が「契約なし」になりメニューが全部消えるが、例外もテスト失敗も出ない**★
  - `boolean`（`monitoring_enabled` / `d_security_enabled`）への変換は
    ★`lib/format/` か `lib/config/` の純粋関数1箇所に閉じ込め、桁位置をハードコードしない★（D-080）
- `customers.contract` は**削除依頼済み**。参照してはいけません
- ★★**`contract` という列は2つあります**★★（2026-09-17 実測。`prisma/schema.prisma:141` と `:571`。行番号は 2026-10-05 時点）
  - ★`facilities.contract`★（`:141`）＝ ★**これが正**★
  - ★`clean_schedules.contract`★（`:571`）＝ ★**読まないこと**★。★型も既定値も `facilities` と同一★
    （`varchar(10)` / `"0000000000"`）なので、★**取り違えても型エラーにならない**★。
    ★現在のアプリは読んでいない（正しい状態）★
  - ★用途は未確認★。TeamWorks の定義書にも説明が無い
- メニューの表示制御もこのフラグに基づく（契約のないセクションはメニューごと非表示）
  - ★**未実装**★（2026-10-06 確認。`components/layout/AppShell.tsx` は全メニューを無条件に描く）。★**P2 で作る。顧客に使わせる前に必ず入れる**★（ユーザー判断 2026-10-06。D-160 追記4）

### 4.7 集計期間とバッチ

| 種別 | 実行タイミング | 対象 |
|---|---|---|
| 週次 | 毎週日曜 6:00 | 前週（**日曜〜土曜**） |
| 月次 | 毎月1日 6:00 | 前月 |
| 年次 | 毎年1月1日 6:00 | 前年 |

- バッチは **TeamWorks が実装**。DFAはサマリテーブルを**読むだけ**
- ★**週次バッチの「前週」は、月をまたぐと2行になる**（D-071 の帰結。**第5回 X-3 は TeamWorks 承認済み。D-074 項3。2026-09-08**）★
  例: 2026-09-06(日) 6:00 のバッチは `2026/8/6週`(08-30〜08-31) と `2026/9/1週`(09-01〜09-05) の**2行**
- ★**サマリの粒度は `month` / `week` が 0 かどうかで判別する**（`week=0` が月次、`month=0 AND week=0` が年次。
  **第5回 X-2 は TeamWorks 承認済み。D-074 項3。2026-09-08**）。`week=0` は予約値なので、API の `week` は 1 以上のみ受け付ける★
- **進行中の期間（当週・当月・当年）はサマリテーブルに存在しない。** 詳細テーブルからその場集計すること
- ★★**データの取得元**（ユーザー指示 2026-09-30。指示書AQ）★★

  | 何を | どこから |
  |---|---|
  | ★確定期間（週・月・★年★）のKPI★ | ★`customer_summaries` / `facility_summaries`（率4列）/ `facility_error_summaries`★ |
  | ★直近・昨日・当期の清掃実績★ | ★`clean_task_reports`★ |
  | 清掃の予定 | ★`clean_timetables` と `clean_timetable_histories` の両方★（`precheck = 0` が本清掃）。★同じ id は `clean_timetables` 側を採る★（履歴の id は本表の id のコピー。D-158 項4・指示書AU 段階3b。読み口は `lib/db/timetable-source.ts` の1か所） |
  | ★エラー（制御盤のエラーも含めて）★ | ★`robot_error_reports` に集約★（★制御盤・セキュリティ・清掃開始失敗のエラーも入る（第11回 P-8〜P-10。D-158）★。★先方の DDL で `robot_id` は NULL 可になったが、Prisma は未 db pull（NOT NULL のまま）★。読み取りは `lib/db/robot-error-select.ts` で `robot_id` / `robot` を読まない（指示書AU 段階0）。★D-Sec アラートは `robot_error_reports` のうち `error_type` が `D-S_CONTROL_FAILURE` の行だけ（完全一致。`SECURITY_ON`・`NOT_STARTED` は出さない）を読み、対応状況・備考はエラー一覧と同じ `PATCH /errors/{error_id}/status` で書く（D-180）★。★`control_error_histories` は読まない★。★削除済み（`deleted_at` が NULL でない）の施設・清掃エリアの行は、エラー一覧（`meta.total` も）にも D-Sec アラート（`summary`・ダッシュボードも）にも出さず、その PATCH は404（U8。D-180 項11）★。★施設名・分類の解決は従来どおり `deleted_at` で絞らない（指示書AX 罠7）★。★区分（ロボット／制御盤／セキュリティ／清掃開始失敗）を画面に作るのは次フェーズ★。★全行（削除済みの施設・清掃エリアの行を除く。U8）の一覧と4カラムの更新はエラー一覧（`/errors`。D-160・指示書AW）★） |

  - ★**`clean_status_histories` は読まない**★（`clean_task_reports` に一本化。テーブル自体は TeamWorks の管理下なので残る）
  - ★**年次は解禁済み**★。★D-072 項4 の「P1では年次を読まない」は破棄★。
    ★`periodOfSummaryRow()`（`lib/db/report-mapping.ts`）は `month` も見ること★ — `week===0` だけで判定すると
    ★年次行が月次に化けて混入し、重複検知で500になる★
  - ★**未実施 ＝ 本清掃に対応する `clean_task_reports` の行が無いこと**★。`clean_task_reports` には
    `clean_error` 相当の列が無い。★「出発失敗」と「未実施」は同じ扱い★（ユーザー確認 2026-09-30）
  - ★**`clean_task_reports.status` は表示名に変換して表示する**★（第10回 N-2 で TeamWorks が定義。D-155）: 0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了 / ★6 清掃キャンセル（3 と同じ名称）★（N-2 の表に無い値。ユーザー指示 2026-10-30「6はキャンセルだそうです」「6の名称も3と同じ清掃キャンセルにしてください」）。★表に無い値（7 以上など）は「不明(7)」と数字を残して出す★（ユーザー指示 2026-10-30。「その他」に潰さない。§4.8 と同じ考え方）。★判定には使わない★（中断・異常終了でも完了率で ◎○△× を出す。ユーザー判断 2026-10-02。§4.4 の「中断で返る＝失敗」は実データを見て見直す）
  - ★**`clean_task_reports.clean_timetable_id` は nullable**★。CSV実測では189行すべて NULL。
    ★NULL は「予定外のタスク」（下の項目。第10回 N-1 で TeamWorks も同意）★で、★欠損ではない★。★2026-10-29 から今の期間の集計・清掃詳細ログ・ダッシュボード・清掃エリアに1件として入れる（§4.2・指示書BG）★（旧: 完了率・実施回数には入れない。D-155 項3）。
    ★旧: 「NULL は TeamWorks が今後埋める前提」（2026-09-30）は、10-01 のユーザー回答で取り下げ★
  - ★**ステージングの `clean_task_reports` は 2026-10-01 に2行入った**★（それまでは0行。2026-09-30 実測で `update_time` が NULL＝作成以来一度も書かれていなかった）。
  - ★★**`clean_timetable_id` が NULL の行は「予定外のタスク」**★★（ユーザー回答 2026-10-01「清掃予定をサーバーで決められるものは
    今後 `clean_timetable_id` が発行されるが、予定外のタスクは NULL になる」）。★NULL は欠損ではなく、実在する清掃★
  - ★★**`clean_timetable_id` が `0` の行も「予定外のタスク」**★★（TeamWorks 回答 2026-10-27「clean_timetable_id が0以外は予定/実行済タスクになりますし, 0は予定外タスクになります」。
    `clean_task_reports` と `robot_error_reports` の両方）。★NULL も引き続き予定外★。★判定は `lib/db/timetable-link.ts` の `scheduledTimetableId()` 1本★（`=== null` / `{ not: null }` を直書きしない）。
    ★予定タスクを持つのは `clean_timetables` / `clean_statuses` / `clean_timetable_histories` / `clean_status_histories` の4表★（同回答。アプリが予定として読むのは `clean_timetables` / `clean_timetable_histories` の2表）。
    ★事前チェックの回（`precheck ≠ 0`）には `clean_task_reports` / `robot_error_reports` の行が入らず、`clean_statuses` / `clean_status_histories` は `clean_status`=2・`clean_status_trans`=0・`clean_error`=0 になる予定★（同回答）。★事前チェックの表示の仕様は変えない★（ユーザー指示 2026-10-27「この仕様から特に変更はない」 — 事前チェックの回に紐づく `robot_error_reports` を失敗として出す）
  - ★★**`clean_task_reports.started_at` に入る値**★★（ユーザー回答 2026-10-05「A」で確定）: ★予定のある清掃は予定の `clean_timetables.start_at` と同じ値★、★予定外のタスクは実際に清掃を始めた時刻★。★過去形の列（`started_at` / `ended_at` 等）は実施後に書かれる★（2026-10-01 ユーザー回答）
    例: 9/30 23:50 の予定が 10/1 00:10 に始まっても `started_at` は 9/30 23:50。★帰結: 期間の振り分けは、レポート当期（予定の `start_at`）・総清掃面積（`started_at`）・サマリ（第10回 N-9=A `started_at`）で一致する★。★予定のある清掃の実際の開始時刻は、この列からは分からない★（定刻出発の判定には使えない）。★旧: 「`started_at` は実際に清掃を始めた時刻」（10-01）は、予定外のタスクについてだけ正しい★
    ★2行ともどちらも `clean_timetable_id` が NULL、status は 4★（2026-10-01 実測・先方投入。§3 の接続先注記も参照）
  - ★ユーザー提供CSV（189行）は、当時のステージングとは別のDBから出たもの★（IDの体系が違う）。
    ★ただし 2026-10-01 に入った2行の対象面積（1377.39502 / 898.892509）がCSVの値と完全に一致する★ので、
    ★同じ収集処理のデータと考えられる★。★出所は第10回で確認する★
- サマリには丸め前の小数と**分子・分母**が格納される（検算可能にするため）
  - ★★**実DDLと食い違っています**★★（2026-09-30 実測・構造由来）。
    ★`task_count`（予定回数）/ `launch_count` / `error_count` はあるが、★**実施回数の列が無い**★★
    （`prisma/schema.prisma:694-696` / `:728-730`。2026-10-05 時点）。
    ★率は4つとも実在する★（`operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio`）
  - ★帰結★: ★**稼働率は検算できず、複数顧客の合算もできない**★（★率は加重平均できないため★）。
    ★第9回 Q6-2 で実施回数の列の追加を依頼中★
- ★**月跨ぎ週は「月境界で分割する」**★（**D-071 で確定。2026-09-07。TeamWorks のサマリ集計に合わせた**）
  - 週は日曜〜土曜。ただし**月をまたぐ週は1日で切り、それぞれの月に別々の週として数える**
  - ★**週の長さは1〜7日で変わる。月の週番号は1〜6**★（第6週は年2〜3回発生する）
  - 各月の第1週は**必ず1日から**始まり、最終週は**必ず末日で**終わる
  - 例（2026年）: 8月第1週 = **08-01 のみ（1日）** / 8月第6週 = **08-30〜08-31（2日）** /
    9月第1週 = **09-01〜09-05（5日）** / 9月第2週 = 09-06〜09-12（7日）
  - 帰属ルールを知っているのは `lib/format/period.ts` の `weekOfMonth()` **だけ**にする
  - ★**D-067 の旧DFA案（1日を含む週がその月の第1週）は破棄。同じ週を2つの月に分けない
    方式だったため、TeamWorks の週次サマリと帰属がズレる**★

### 4.8 エラー種別は変換しない（D-077）

- ★**`robot_error_reports.error_type` も `robots.status` も、DBから来た値をそのまま表示する**★
- ★**7分類（ハンドルアップ / LostLocation / オフライン / ブラシエラー / 緊急停止 / 充電不足 / その他）
  への変換は行わない**★
- 理由: 現状 Pudu API から何が返るか不明なため。★**推測でマッピングしない**★
  （§7 の「仕様が不明な箇所を推測で実装すること」の禁止に合致）
- ★**D-057 / D-067 の「サーバ側で7分類に寄せる／未知の値は `other` に寄せる」は破棄**★
- ★**`robots.status = -200`（LostLoaction）→「LostLocation」の変換もしない**★
  （D-070 でそう確定していたが取り下げ。**「-200 が LostLocation である」という事実認識は取り下げない。
  取り下げるのは画面に出す前に変換する実装方針だけ**）
- ★**`docs/handoff/analysis/error-type-mapping.md` は参考資料。実装の下敷きではない**★
- ★**帰結: 画面には英語識別子（例 `EmergencyStop`）がそのまま出る。それが意図した挙動**★
- ★**注意: `lib/theme-status.ts` の色分けは日本語6文字列の完全一致なので、生値では全部グレーに
  フォールバックする**★

---

## 5. 参照資料 — 何が「正」か

| 資料 | 何の正か |
|---|---|
| `d-watch-api.yaml` | **API契約の正**。エンドポイント・JSONスキーマ・enum・エラー形式 |
| `docs/spec.md` | **機能一覧と開発フェーズの正**。何をどの順で作るか |
| `docs/decisions.md` | **決定事項とその理由の正**。「なぜこの仕様か」を遡る時に読む |
| `docs/fixtures.md` | **サンプルデータの正**。DB接続前の開発で必ず用意するケース一覧 |
| モック（`d-watch-mock.jsx`） | **画面レイアウト・文言・遷移の正**。フロント実装はこれを移植する |
| 清掃ロボット監視テーブル定義書（28テーブル） | **DBスキーマの正**。Prismaスキーマの突き合わせ先 |
| UI仕様書 v6.0 | 画面ごとの詳細仕様。モックで迷った時の裏付け |
| 要件定義書 P1 v2.2 | 全体要件・Phase A/B/Cの線引き。★**DFA側の想定であり、TeamWorks の DB との整合は保証されない。型・値域・カラム名は実DDLが正**★（2026-09-08 ユーザー指示。D-074 / D-057）。**所在: Google Drive の「D-Watch / D-Security 要件定義書 (P1)」`1thSYHk-ODCht-K4WqFlCP-Es2OyAz4Wj`**。★**PHP/Laravel と書かれているが実装は Next.js（D-046）。トークン期限の記述も現行方針では禁止。古い箇所がある**★ |
| 運用ガイド | 誰がいつどう使うか。UXの判断に迷った時に読む |

資料間で矛盾があったら、上の表の「正」に従ってください。それでも解決しない場合は**実装を止めて確認を求めてください**（推測で埋めない）。

---

## 6. 作業ルール

1. **API契約を変えたくなったら、先に `d-watch-api.yaml` を直す。** 実装だけ変えてYAMLと乖離させない
2. **判定ロジック（◎○△×・L1〜L3・KPI計算・丸め）には必ずテストを書く。** 期待値表を先に作り、それをテストで固定する。目視確認だけで済ませない
3. **開発は `lib/fixtures/` のサンプルデータで行う。** 実DBの完成を待たない。
   - APIルートは `USE_FIXTURES` で fixtures / Prisma を切り替える。画面側のコードは切替を意識しない
   - **判定ロジックは切替の外側**に置く（fixture開発中も本番同等のロジックが走る）
   - 用意すべきケースは `docs/fixtures.md` を参照（境界値・異常系を先に仕込む）
4. **TeamWorks待ちの機能はスタブ/モックで先行し、解消後に差し替える。** ブロックされない
5. **PRは必ずもう1人がレビューしてからmainにマージする**（★レビュー担当は daichi。D-154★）
6. **「実測」と書くときは、その数字の出所を必ず併記する。**
   「当方のシード由来（`scripts/seed-local-db.ts` / `lib/fixtures/`）」/「先方投入」/
   「構造（`information_schema` / `SHOW` / `EXPLAIN`）」のどれかを書く。**出所を書けない数字は
   「実測」と呼ばない。**
   - 理由: テストDBの行が当方のダミーだと気づかないまま、それを根拠に外部（TeamWorks）へ
     照会する文書を作りかけた（2026-09-17。送付前に気づいた。
     `docs/handoff/テストDBのデータ出所_20260917.md`）
   - この事故は `docs/decisions.md` D-111 項7（`:6683-6689`。2026-09-11）が5日前に予告していた
     — 「**テストDBでは判定できない: 本番のデータ量 / サマリバッチの実挙動 / 本番の `time_zone` 設定**」。
     表はあったのに守られなかった

---

## 7. 禁止事項

- ❌ **メインDBへのマイグレーション作成・スキーマ変更**（TeamWorksの領域。`prisma db pull` で取り込むだけ。
  `db pull` 実施前の `prisma/schema.prisma` 手書きは対象外、D-056参照）
- ❌ **§3 で指定した4カラム（`robot_error_reports` は `error_list_id` を含む5カラム。D-184）以外へのDB書き込み**
- ❌ **表示用に丸めた値で判定すること**（4.3参照）
- ❌ **`customers.contract` の参照**（削除依頼済み。`facilities.contract` を使う）
- ❌ **閾値（90/70/10、猶予30分、L2=2日/L3=3日）のハードコード**（設定値として外出し）
- ❌ **点検記録データをメインDBに保存すること**（DFA独自ストアに分離）
- ❌ **トークン期限/残日数の実装**（仕様から除外済み。古い資料に記述が残っているが実装しない）
- ❌ **モックに残っている実在の住所・電話番号をそのまま使うこと**（ダミー値に置換する）
- ❌ **仕様が不明な箇所を推測で実装すること**（確認を求める）
- ❌ **エラー種別を推測で7分類に変換すること**（D-077。DBの値をそのまま表示する。§4.8参照）
- ❌ **`git add -A` の使用**（過去に別作業の途中ファイルを巻き込む事故が起きている。
  ★**必ずファイルパスを明示する**★）

---

## 8. 用語

| 用語 | 意味 |
|---|---|
| 清掃エリア（sections） | 施設内の清掃対象区域。旧称「区画」。**「清掃エリア」に統一** |
| 施設（facilities） | ロボットが配置される店舗・拠点。旧称「店舗」 |
| D-Sデバイス / D-S制御盤 | セキュリティ会社システムと制御サーバ間を中継する物理デバイス |
| 事前チェック | 清掃開始の60分前/30分前にロボット状態を確認する仕組み |
| Pudu Cloud | PUDU Robotics のクラウドAPI。ロボット状態・タスク結果の取得元 |

**P1の前提: 1清掃エリア = 1ロボット。** テーブル構造は複数台対応可能ですが、P1ではこの前提で実装します。

## 9. 実装規約

### 9.1 ディレクトリ構成

```
app/
  (dashboard)/           画面。モックの画面IDに合わせる
    dashboard/ detail-log/ alerts/ ds-logs/ ds-alerts/
    facilities/ inspections/ reports/ schedules/ system/
  api/                    APIルート
    clean-logs/
      route.ts
      export/route.ts
      [id]/advice/route.ts
      [id]/areas/route.ts
    settings/
      advice/route.ts
components/
  ui/                     Pill, FilterButton, GradeBadge 等の共通部品
  features/               画面固有のコンポーネント（例: detail-log/）
  layout/                 AppShell 等
  theme/                  ThemeProvider
lib/
  judge/                  ★判定ロジック（◎○△×、L1〜L3、KPI計算、丸め）+ __tests__/
  fixtures/               サンプルデータ（DB接続前の開発用。docs/fixtures.md 参照）
  db/                     USE_FIXTURES で fixtures / Prisma を切り替える
  config/                 閾値等の設定値
  clean-logs/             清掃ログ関連のクエリ
  settings/               運用設定（アドバイス等）
  types/                  型定義。YAMLのスキーマ名と揃える
  format/                 表示整形（日時等）
docs/                     spec.md, decisions.md, fixtures.md 等
prisma/schema.prisma      db pull実施後は生成物を正とし手で書き換えない（D-056）。実施前は定義書ベースで手書き
d-watch-api.yaml          API契約
d-watch-mock.jsx          画面レイアウト・文言・遷移の正
```

### 9.2 命名

- ファイル/ディレクトリ: kebab-case（`detail-log/`）
- コンポーネント: PascalCase（`CleanLogTable.tsx`）
- 関数・変数: camelCase
- DBカラム: snake_case（テーブル定義書の物理名をそのまま使う。勝手にリネームしない）
- API のクエリパラメータ・レスポンスキー: snake_case（YAML準拠）
- 型: `lib/types/` に集約し、YAMLのスキーマ名と揃える（`CleanLogItem`, `PrecheckItem` 等）

### 9.3 コーディング規約

- `any` 禁止。型が不明ならYAMLのスキーマから起こす
- APIレスポンスは必ず型を定義してから実装する
- 日時は ISO 8601（JST, +09:00）。表示整形はコンポーネント側
- 数値の丸めは表示層でのみ行う。ロジック層には生値を渡す
- エラーレスポンスは `{ error: { code, message, details? } }` で統一
- 一覧APIは必ず `meta: { total, page, per_page }` を返す
- 環境変数は `.env.local`。認証情報を絶対にコミットしない

### 9.4 判定ロジック（`lib/judge/`）

- **テストを先に書く。** 入力→期待値の表を作り、境界値を必ず含める
  - 完了率: 90.0 / 90.1 / 70.0 / 70.1 / 10.0 / 9.9 / 0 / 未実施(null)
  - 連続日数: 1日 / 2日 / 3日 / スケジュール非存在日を挟むケース
  - 丸め: 90.4 → 表示90だが判定は◎、89.5 → 表示90だが判定は○
- 閾値は `lib/config/` から読む。ハードコード禁止
- 判定関数は純粋関数にする（DBアクセスを混ぜない）

### 9.5 サンプルデータ（`lib/fixtures/`）

- APIルートは `USE_FIXTURES` で fixtures / Prisma を切り替える。**画面側は切替を意識しない**
- 判定ロジックは切替の外側に置く（fixtureのデータにも本番同等の判定を通す）
- fixtures は DB接続後も削除しない（テスト・デモ用）

### 9.6 フロント

- モック（`d-watch-mock.jsx`）の画面レイアウト・文言・遷移を正とする
- 判定結果（◎○△×、L1〜L3、ソート順）はAPIから受け取る。フロントで再計算しない
- ダーク/ライト、JP/EN切替の器を壊さない

---

<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
