# 指示書AV — ダッシュボードを「各清掃エリアの直近の清掃（7日以内）」にする

**日付**: 2026-10-06 / **担当**: Cursor（★実装は Claude Code（2026-10-06。ユーザー指示「ここで実装する」）。§13★）/ **基点**: develop `6c944cf`（D-159 を記録したコミット）
**前提**: ★D-159（本書の根拠。`docs/decisions.md:12578-12591`）★ / D-049（スケジュール進捗。`docs/decisions.md:300-310`）/
D-091 項1（出発の判定に「予定＋猶予」を当方で計算しない。`docs/decisions.md:3449` 以降）/
`CLAUDE.md` §4.3・§4.4・§6・§7・§9.4・§9.6

★**期待値表（§3 の表1）は監督が確定した正。変えないこと**★（`CLAUDE.md` §0-2・§6-2・§9.4）。
★**表2・表3 は本書で表1 から導いたもの**★。★監督の確認が要る★（§11）。
★**§11 の質問に回答が来て本書の暫定が変わったら、§11 の各行に書いた「直す場所」を本文と同時に直すこと**★
（本文と末尾の決定が食い違ったまま渡さない）。

★行番号はすべて 2026-10-06 の `6c944cf` で確かめたもの★。段階が進むとずれるので、関数名・文で探すこと。

---

## 0. 何をするか

| # | やること | 段階 |
|---|---|---|
| 1 | ★`GET /api/dashboard` を `date` なしで呼んだとき、数値カード（`summary`）を「★清掃エリアごとに、終わった本清掃のうち一番新しい1件（7日以内）★」で数える★ | 段階2〜4 |
| 2 | ★ダッシュボードの「清掃詳細ログ」（コンパクト版の表）を同じ母集団にする★。★`/api/dashboard` のレスポンスに `problem_logs` を足し、画面は `/api/clean-logs` を呼ばなくなる★（§2 D7） | 段階4・5 |
| 3 | 見出しを「直近の清掃結果サマリー（各清掃エリアの最後の清掃・7日以内）」に。表の各行に日付を出す | 段階5 |
| 4 | `date` を指定したときは ★従来どおり★（その日の清掃） | — |

★**変えないもの**★（D-159 末尾）: ★スケジュール進捗（`schedule_progress`。当日。D-049）★／★要レスキュー（`rescue_required`。基準日は当日）★／
★セキュリティバナー★／★`?date=` を指定したとき★／★1行ごとの判定（◎○△×・出発・L1〜L3）★／★`/api/clean-logs` の契約★。

---

## 1. 背景

- ダッシュボードは「昨夜の清掃結果サマリー」と出しながら、★当日の日付（`todayJst()`）の清掃★を数えていた
  （`app/api/dashboard/route.ts:55` / `:90` / `:117`）。画面のコンパクト版の表も ★当日だけ★ を引いている
  （`components/features/dashboard/DashboardScreen.tsx:77-85`。`date_from = date_to = today`）。
- サンプルデータは深夜 1:30〜3:30（当日の日付）なので合っていたが、★ステージングの実データは 10:00 や 19:00〜22:00（前日の日付）★で、
  翌朝に開くと ★空★ になる（D-159。2026-10-06 ステージング: 当日0件／前日指定で成功9・失敗1・未実施7）。
- ユーザー指示（2026-10-06）「①で、出発失敗も含めて7日まで」。★「終わった予定だけ」は監督の判断★（D-159 項4）。

---

## 2. 決定（Cursor は従うこと）

| # | 決定 | 理由 |
|---|---|---|
| D1 | ★`date` なし（★`?date=` の空文字も含む★）のとき「直近モード」★。母集団は ★清掃エリアごとに1件★: 本清掃（`precheck = 0`）のうち ★(a) 予定の終了（`clean_timetables.end_at`）＋ 落ち着き時間 ≤ 現在★ かつ ★(b) 予定の開始（`start_at`）≥ 現在 − 遡り日数★ を満たすものの中で ★開始が一番新しいもの★ | D-159 項1・3・4。★`?date=` を省略と同じにするのは現行の `||`（`route.ts:49-55`）と同じ★ |
| D2 | ★出発失敗（レポート行が無い予定）も選ぶ★。選ばれた行は `computeSummary()` で ★未実施★ に数える | D-159 項2。行の作り方（`departed`）は変えない |
| D3 | ★(a)(b) を満たす予定が無いエリアは出さない★（0件の行を作らない） | D-159 項3 |
| D4 | ★境界は両方とも「含む」★: (a) は `終了 + 落ち着き == 現在` を採る、(b) は `開始 == 現在 − 遡り` を採る | 表1 L4・L7（監督確定） |
| D5 | ★同じエリアで開始が同じ予定が2件あったら `id` の大きいほう★（★暫定。§11 Q1★） | 表1 に無い。決定的にするため |
| D6 | ★設定値を `lib/config/index.ts` に新設★: `DASHBOARD_LATEST_RUN = { lookbackDays: 7, settleMinutes: 30 }`。★`LAUNCH_GRACE_MINUTES`（`lib/config/index.ts:195`）を流用しない★ | `CLAUDE.md` §7（閾値のハードコード禁止）。★意味が違う★ — こちらは「どの予定を見せるか」で、1行の判定（出発・◎○△×）には一切効かない。D-091 項1 が禁じた「設定を変えると過去のログの判定が変わる」状態にはならない |
| D7 | ★コンパクト版の表は `/api/dashboard` のレスポンスに `problem_logs`（`CleanLogItem[]`）として載せる★。★`/api/clean-logs` に新しいモードは足さない★ | 下の「D7 の理由」 |
| D8 | ★選び方の規則は純粋関数1本★: `pickLatestRunPerSection()`（`lib/dashboard/latest-run.ts`。新規）。★DB の窓（どの日付範囲を読むか）も純粋関数★: `latestRunWindow()`（同じファイル） | `CLAUDE.md` §9.4（判定関数は純粋関数）。fixtures でも Prisma でも同じ規則が走る（§9.5） |
| D9 | ★データ層は `CleanLogItem` を変えない★。★清掃エリアと予定の終了は、別の小さなメソッド `cleanLogRunMeta()` で id ごとに返す★ | 下の「D9 の理由」 |
| D10 | ★fixtures には予定の終了時刻が無いので、★終了 ＝ 予定の開始（`scheduled_at`）★ とみなす★（＝落ち着き時間を ★開始から★ 数える）。★清掃エリアの代わりに ★ロボット（`serial_number`）★ を使う★ | fixtures の `CleanLogItem` に終了も清掃エリアも無い（`lib/fixtures/clean-logs.ts:46-94`）。★fixtures のデータは足さない★（`docs/fixtures.md` の正を崩さない）。P1 は1エリア＝1ロボット（`CLAUDE.md` §8） |
| D11 | ★`problem_logs` は件数で切らない★（母集団は1エリア最大1行なので、エリア数が自然な上限）。★画面が先頭5件を出す★（★暫定。§11 Q4★） | 要レスキューと同じ考え方（`lib/config/index.ts:92-119`）。切ると切られたことに気づけない |
| D12 | ★`date` 指定のときの `problem_logs` は「その日の行のうち △×」★ | `summary` と同じ母集団にする |
| D13 | ★`problem_logs` にも `capCleanLogs()` を通す★（連続日数の表示上限） | `/clean-logs` と連続日数の表示が食い違わないため（`lib/clean-logs/query.ts:190-220` の docstring。D-109） |

### D7 の理由（`/api/clean-logs?latest=true` を採らなかった理由）

- ★数値カードと表が ★同じ `now`・同じ行の集合★ から作られる★。別々の API にすると2本のリクエストが別々に「現在」を取るので、
  境界（終了＋30分ちょうど付近）で ★カードと表の母集団がずれうる★（例外もテスト失敗も出ない）。
- 変更が小さい: `/clean-logs` に足すと `parseCleanLogQuery()`（`lib/clean-logs/query.ts:80`）・`CleanLogQuery` の型・
  ★`/clean-logs/export` が同じパーサを共有している★ ので CSV 側の扱い・`date_from` / `date_to` との同時指定の規則まで決める必要がある。
  `/dashboard` に足すなら、ルートが既に持っている行を返すだけ。
- ★YAML の「コンパクト版清掃詳細ログは本APIに含めない」（`d-watch-api.yaml:219-220`）を覆す★ことになる。あれの趣旨は
  「既存の一覧APIで足りるものを二重に作るな」（同 `:313-316`）で、★「各エリアの直近1件」は既存の一覧APIでは表せない★ので趣旨には反しない。
  ★YAML の記述は段階4 で書き直す★（★§11 Q3 で監督に確認★）。

### D9 の理由（`CleanLogItem` に内部用の列を足さない理由）

- `CleanLogItem` は `/clean-logs`・`/clean-logs/export`・`/clean-logs/{id}/advice` など ★多くの経路が共有★ し、YAML のスキーマ名と揃えてある（`CLAUDE.md` §9.2）。
  内部用の列を足すと ★どこかで剥がし忘れた瞬間に API に漏れる★（例外もテスト失敗も出ない）。
- `prismaSource.cleanLogs()`（`lib/db/index.ts:765-1037`）は約270行の繊細な関数（指示書AC 禁止事項14「レスポンスを1バイトも変えない」）。
  ★中身に手を入れず★、`where` を1つの関数に切り出して2本で共有するだけにする（段階3）。
- 代わりに ★id → { 清掃エリア, 予定の終了 }★ を返す軽いメソッドを足し、純粋関数の中で id で突き合わせる。★突き合わせに失敗したら例外★（D9 の不変条件。段階2）。

---

## 3. ★期待値表（正）★

### 表1（`pickLatestRunPerSection()`。★監督確定・正・変えない★）

★条件★: 現在 ＝ 2026-10-06 09:00 JST、遡り 7日、落ち着き 30分。

| # | エリア | 予定（開始〜終了）と出発 | 選ばれる予定 | 確かめること |
|---|---|---|---|---|
| L1 | A | 10/4 19:00〜21:10 出発 / 10/5 19:00〜21:10 出発 | 10/5 19:00 | 新しいほう |
| L2 | B | 10/5 02:00〜04:10 出発 / 10/6 02:00〜04:10 出発 | 10/6 02:00 | 日付が変わった後の清掃も拾う |
| L3 | C | 9/28 19:00〜21:10 だけ | なし（出さない） | 7日より前は出さない |
| L4 | D | 9/29 09:00〜11:10 だけ | 9/29 09:00 | 境界: ちょうど7日前は含む |
| L5 | E | 9/29 08:59〜11:09 だけ | なし | 境界: 7日前より1分古いと出さない |
| L6 | F | 10/5 08:00〜10:10 出発 / 10/6 08:00〜10:10（終了前） | 10/5 08:00 | 終わっていない予定は採らない |
| L7 | G | 10/6 06:00〜08:30 出発 | 10/6 06:00 | 境界: 終了＋30分ちょうど（09:00）は採る |
| L8 | H | 10/5 06:00〜08:00 出発 / 10/6 06:00〜08:31 出発 | 10/5 06:00 | 境界: 終了＋30分が09:01 だと採らない |
| L9 | I | 10/5 19:00〜21:10 出発しなかった（レポート行なし） | 10/5 19:00（未実施として数える） | 出発失敗も含める |
| L10 | J・K（同じ店舗の2エリア） | J: 10/5 19:00〜21:10 / K: 10/5 22:00〜23:30 | J・K それぞれ1件（計2件） | エリアごと |

★サマリ★: L1〜L10 で選ばれるのは ★A・B・D・F・G・H・I・J・K の9行★（I 以外は出発している）。
★サマリのテストは、選ばれた9行を ★既存の `computeSummary()`（`lib/dashboard/aggregate.ts:31`）にそのまま渡した結果★ と、
`pickLatestRunPerSection()` の戻りを `computeSummary()` に渡した結果が ★等しい★ ことで確かめる★（`computeSummary()` は変えない）。
各行の `grade` は ★既存の判定★（`judgeGrade(completion_rate)`）で作る。完了率の値はテストを書く人が決めてよい（I は `departed: false` / `completion_rate: null`）。

★テストの書き方の注意★:
- ★入力には表に書いた予定を全部入れる★（L3・L5 の行、L6 の 10/6 08:00 の行、L8 の 10/6 06:00 の行も入れる。★選ばれないことを確かめるため★）。
  ★L6 の 10/6 08:00 は「開始は過ぎているが終わっていない」★ので、`cleanLogs()` は実際にこの行を返す（`isFutureSchedule()` は開始しか見ない）。`departed: false` で入れる。
- ★エリア（`section_key`）は J と K で別、施設（`facility_id`）は J と K で同じ★にする。
- ★日時は `+09:00` 付きの ISO 文字列で書く★（`"2026-10-05T19:00:00+09:00"`）。`now` は `new Date("2026-10-06T09:00:00+09:00")`。

### 表2（本書で表1 から導いたもの。★監督の確認が要る★）

| # | 入力 | 期待 | 確かめること |
|---|---|---|---|
| E1 | 行の id に対応する `CleanLogRunMeta` が無い | ★例外★（メッセージに id を含める） | D9 の不変条件。黙って落とすと行が静かに消える |
| E2 | 同じ id の `CleanLogRunMeta` が2件 | ★例外★ | 同上 |
| E3 | 行が0件 | `[]` | — |
| E4 | 表1 の入力を逆順に並べて渡す | 戻りは ★入力の順を保つ★（並べ替えない） | 順序の正はデータソース（`lib/db/index.ts:1029-1036`）。ルートで並べ替え直さない |
| E5 | 同じエリアで開始が同じ2件（id 5 と 9） | id 9 | D5（★暫定。§11 Q1★） |
| E6 | `CleanLogRunMeta` はあるが行が無い id（未来の予定など） | 無視（例外にしない） | `cleanLogs()` は未来の予定を返さないが、メタは返しうる |

### 表3（`latestRunWindow(now, lookbackDays)`。本書で導いたもの。★監督の確認が要る★）

DB から読む日付範囲（JST の `YYYY-MM-DD`。両端を含む）。★窓は表1 の判定より広くてよい（細かい判定は純粋関数）★。

| # | now（JST） | lookbackDays | `date_from` | `date_to` |
|---|---|---|---|---|
| W1 | 2026-10-06 09:00 | 7 | 2026-09-29 | 2026-10-06 |
| W2 | 2026-10-06 00:00 | 7 | 2026-09-29 | 2026-10-06 |
| W3 | 2026-10-05 23:59:59 | 7 | 2026-09-28 | 2026-10-05 |
| W4 | 2026-09-30 09:00 | 7 | 2026-09-23 | 2026-09-30 |

★W4 はルートのテスト（`app/api/dashboard/__tests__/route.test.ts:243` の `NOW`）と同じ日時★。

---

## 4. ★落とし穴（全段階で読むこと）★

| # | 罠 | どう避けるか |
|---|---|---|
| 4-1 | ★スケジュール進捗・要レスキューまで直近モードにする★ | ★変えない★。`computeScheduleProgress()` には ★当日の行（`logsOfDay`）★ を、`db.rescueRequired()` には ★当日（`date`）★ を渡し続ける（`route.ts:101` / `:128-132`）。★`data.anomaly`（`:138-147`）も当日の行数のまま★ |
| 4-2 | ★`facility_ids` の `null` と `[]` を混同する★ | ★`null` = 絞らない / `[]` = 0件★（罠1。`route.ts:66-72`）。★`cleanLogs()` と `cleanLogRunMeta()` に**同じ配列**を渡す★。`?.length` で分岐しない |
| 4-3 | ★`end_at` を `toJstIso()` に直接渡す★ | `end_at` も `start_at` と同じ ★DATETIME の JST 壁時計値★（`prisma/schema.prisma:597-598`）。★`jstIsoFromDb()`（`lib/format/date.ts:96`）を通す★。★9時間を足さない・引かない★（`CLAUDE.md` §3） |
| 4-4 | ★時刻の比較を文字列でする★ | 日付の切り出し（先頭10文字）は文字列でよいが、★「終了＋30分 ≤ 現在」「開始 ≥ 現在−7日」は `Date.parse()` したミリ秒で比べる★ |
| 4-5 | ★`now` を2回以上作る★ | ルートで ★`const now = new Date()` を1回だけ★作り、`today`・窓・`pickLatestRunPerSection()` に同じものを渡す（`isFutureSchedule()` の docstring `lib/db/clean-log-mapping.ts:298-300` と同じ理由） |
| 4-6 | ★`LAUNCH_GRACE_MINUTES` を使う／`departed` の判定に落ち着き時間を混ぜる★ | ★使わない・混ぜない★（D6）。`notExecutedFromReport()` / `isFutureSchedule()` は変えない |
| 4-7 | ★全件読みに戻る★ | ★窓（表3）で `start_at` を絞る★。`date_from` / `date_to` を `null` にしない。読む量は1日 → 8日（本番想定で約360行 → 約2,900行。`route.ts:82` の見積もりの8倍）。★`cleanLogRunMeta()` も同じ `where`★ |
| 4-8 | ★`cleanLogRunMeta()` の `where` を別に書く★ | ★`cleanLogs()` の `where` を関数に切り出して2本で共有★（段階3）。別に書くと片方だけ直したときに突き合わせが静かにずれる |
| 4-9 | ★`pickLatestRunPerSection()` を `try` の外で呼ぶ★ | E1・E2 の例外が `{ error: ... }` の形にならない。★`try` の中で呼ぶ★（`route.ts:78-113`） |
| 4-10 | ★`problem_logs` に `capCleanLogs()` を通し忘れる★ | D13。`/clean-logs` と同じく ★絞り込みの後★ に通す（`app/api/clean-logs/route.ts:55-63` / `:104` の作法） |
| 4-11 | ★「問題あり」の規則を書き写す★（`grade === "△" \|\| grade === "×"`） | ★`lib/clean-logs/query.ts:180` の規則を関数に切り出して共有★（段階4-3） |
| 4-12 | ★生SQL（`$queryRaw` / `$executeRaw`）★ | ★禁止★（`CLAUDE.md` §3）。Prisma のクエリビルダ（`findTimetables()`）だけ |
| 4-13 | ★7・30 を直書きする★（画面の見出しの「7日」も） | ★`DASHBOARD_LATEST_RUN` を読む★（`CLAUDE.md` §7）。見出しは i18n の `{n}` に差し込む |
| 4-14 | ★画面の文字列を i18n に通さない★ | 新しい見出しは `t()`（`lib/i18n.ts`）。キーは日本語原文と1文字も違わないこと（全角括弧・中点） |
| 4-15 | ★fixtures に終了時刻・清掃エリアを足す★ | ★足さない★（D10）。fixtures 版の `cleanLogRunMeta()` が `scheduled_at` と `serial_number` から作る |
| 4-16 | ★`git add -A`★ | ★禁止★（`CLAUDE.md` §7）。ファイルパスを明示する |

---

## 段階0 — 着手前

1. `git status` が clean で、develop が `6c944cf` 以降であること（★`.claude/worktrees/` ではなく本体で作業する★）。
2. `npx tsc --noEmit -p .` と `npm test` が通る状態から始める（通らなければ着手前に報告）。
3. 次の grep で「当日」に依存している箇所を確かめる（本書の段階4・5 で直すのはこのうち dashboard の2ファイルだけ）:
   ```
   grep -rn "todayJst" app/api/dashboard components/features/dashboard app/api/__tests__/master-filter-dashboard-reports.test.ts
   ```

---

## 段階1 — 期待値表をテストにする（実装より先）

1. ★`lib/dashboard/__tests__/latest-run.test.ts`（新規）★:
   - 冒頭コメントに表1・表2・表3 を写し、「★表1 は監督確定（D-159 / 指示書AV §3）。変えないこと★」「表2・表3 は指示書AV で導いたもの」と書く
   - `describe("pickLatestRunPerSection — 表1")`: `it.each` で L1〜L10 を1件ずつ（★選ばれた id の集合★を比べる）＋ ★全部まとめて渡したときに9行★
   - `it("サマリは既存の computeSummary() に選ばれた行を渡したものと等しい")`: §3 表1 の下の書き方どおり
   - `describe("pickLatestRunPerSection — 表2")`: E1〜E6
   - `describe("latestRunWindow — 表3")`: W1〜W4
   - ★設定値は `DASHBOARD_LATEST_RUN` から読む★（`7` / `30` を直書きしない。表1 の前提が設定値と一致することも1件で確かめる）
2. ★`lib/config/__tests__/dashboard-latest-run.test.ts`（新規）★: `lookbackDays` は1以上の整数、`settleMinutes` は0以上の整数。
   ★`DASHBOARD_LATEST_RUN.settleMinutes` が `LAUNCH_GRACE_MINUTES` の参照ではない★ことはテストにできないので、コメントで D6 を書く
3. ★この時点ではテストが落ちる（関数・設定値が無い）のが正しい★。コミットはテストだけで1つ（「テストだけ。この時点では落ちる」と明記）

---

## 段階2 — 設定値と純粋関数

### 2-1. `lib/config/index.ts`

`LAUNCH_GRACE_MINUTES`（`:191-195`）の ★直後★ に足す:

```ts
/**
 * ダッシュボード（F-01）の「直近の清掃」の選び方（D-159 / 指示書AV）
 * ★`LAUNCH_GRACE_MINUTES` とは別物。流用しないこと★ — あちらは出発失敗の猶予（D-091 項1 で使わないと決めた）。
 *   こちらは「どの予定を見せるか」だけに効き、1行の判定（出発・◎○△×・L1〜L3）には効かない。
 * ★消費者★: `app/api/dashboard/route.ts` と `components/features/dashboard/DashboardScreen.tsx`（見出しの日数）だけ
 */
export const DASHBOARD_LATEST_RUN = {
  /** 何日前の予定まで遡るか（予定の開始で数える。★ちょうどは含む★） */
  lookbackDays: 7,
  /** 予定の終了から何分たったら「終わった」とみなすか（★ちょうどは含む★）。レポートは清掃の後に書かれる（2026-10-01 ユーザー回答） */
  settleMinutes: 30,
} as const;
```

### 2-2. `lib/types/index.ts` — 内部用の型

`CleanLogDbQuery`（`:117-166`）の後に足す。★YAML には載せない（API に出ない）★ことを docstring に書く:

```ts
export interface CleanLogRunMeta {
  /** `clean_timetables.id`（＝ `CleanLogItem.id`） */
  id: number;
  /** 清掃エリアを区別するキー。Prisma: `section:${section_id}` / fixtures: `fixture-robot:${serial_number}`（D10） */
  section_key: string;
  /** 予定の終了。ISO8601（+09:00）。Prisma: `jstIsoFromDb(end_at)` / fixtures: `scheduled_at` と同じ（D10） */
  planned_end_at: string;
}
```

★`section_key` を文字列にした理由★: fixtures に清掃エリアの id が無く、数値を作り出すと実在の id と取り違えうるため。

### 2-3. `lib/dashboard/latest-run.ts`（新規）— ★純粋関数のみ★

```ts
export interface LatestRunOptions { now: Date; lookbackDays: number; settleMinutes: number }

export function latestRunWindow(now: Date, lookbackDays: number): { date_from: string; date_to: string }
export function pickLatestRunPerSection(
  rows: readonly CleanLogItem[],
  meta: readonly CleanLogRunMeta[],
  options: LatestRunOptions
): CleanLogItem[]
```

- `latestRunWindow()`: `date_to = toJstIso(now).slice(0, 10)`、`date_from = toJstIso(new Date(now.getTime() − lookbackDays × 86_400_000)).slice(0, 10)`。
  ★`toJstIso()`（`lib/format/date.ts:37`）を使う★（実行環境の TZ に依存しない。`getFullYear()` 等を使わない）
- `pickLatestRunPerSection()`:
  1. `meta` を id で引けるようにする。★同じ id が2回 → 例外★（E2）
  2. 各行について ★meta が無ければ例外★（E1。メッセージ例: `[dashboard] pickLatestRunPerSection: clean_timetables(id=…) の予定の終了・清掃エリアがありません`）
  3. 候補にする条件（★両方とも「含む」★。D4）:
     `Date.parse(row.scheduled_at) >= now − lookbackDays × 86_400_000` かつ
     `Date.parse(meta.planned_end_at) + settleMinutes × 60_000 <= now`
  4. `section_key` ごとに、開始（`Date.parse(scheduled_at)`）が一番大きい行。★同じなら id が大きいほう★（D5）
  5. ★戻りは `rows.filter(選ばれた id)`★（入力の順を保つ。E4）。★行を書き換えない・丸めない★
- ★`isFutureSchedule()` をここで呼ばない★（未来の予定は `cleanLogs()` が既に外している。条件3 の「終わった」で足りる）

段階1 のテストが通ること。コミット。

---

## 段階3 — データ層（`cleanLogRunMeta()`）

### 3-1. `DataSource`（`lib/db/index.ts:203-215`）

`cleanLogs()` の直後に足す:

```ts
/**
 * ★清掃ログの id → { 清掃エリア, 予定の終了 }★（ダッシュボードの直近モード専用。D-159 / 指示書AV）。
 * ★`cleanLogs()` と同じ引数を渡すこと★（同じ `where`。突き合わせは `pickLatestRunPerSection()`）。
 * ★`cleanLogs()` が返さない id（未来の予定）を含んでよい★。逆（`cleanLogs()` の行に対応が無い）は呼び出し側の例外
 */
cleanLogRunMeta(query: CleanLogDbQuery): Promise<CleanLogRunMeta[]>;
```

### 3-2. fixtures 版（`lib/db/index.ts:476-478` の `cleanLogs()` の直後）

```ts
async cleanLogRunMeta(query: CleanLogDbQuery) {
  return filterFixtureCleanLogs(cleanLogs, query).map((row) => ({
    id: row.id,
    section_key: `fixture-robot:${row.serial_number}`,
    planned_end_at: row.scheduled_at,
  }));
}
```

★docstring に D10 を書く★: 「fixtures には予定の終了も清掃エリアも無い。★終了＝開始★（落ち着き時間は開始から数える）、★清掃エリア＝ロボット★」。
★`filterFixtureCleanLogs()` を通す★ので `facility_ids` の `null` / `[]` の扱いは `cleanLogs()` と同じになる。

### 3-3. Prisma 版

1. ★`cleanLogs()` の `where`（`lib/db/index.ts:779-803`）を関数に切り出す★（同じファイル内の非公開関数。例 `mainTimetableWhere(query: CleanLogDbQuery)`）。
   `const range = dbDateRange(...)`（`:774`）も中に入れる。★中身は1文字も変えない★（コメントも一緒に移す）。
   `cleanLogs()` は `where: mainTimetableWhere(query)` にする。★`include` / `orderBy`（`:804-823`）は動かさない★
2. `cleanLogRunMeta()` を足す:
   ```ts
   const rows = await findTimetables(prisma, {
     where: mainTimetableWhere(query),
     select: { id: true, section_id: true, end_at: true },
   });
   return rows.map((row) => ({
     id: Number(row.id),
     section_key: `section:${row.section_id.toString()}`,
     planned_end_at: jstIsoFromDb(row.end_at),
   }));
   ```
   - ★`findTimetables()`（`lib/db/timetable-source.ts:233`）を使う★（2表から読む・同じ id は本表が勝つ。指示書AU 段階3b）。★`prisma.cleanTimetable.findMany` を直接呼ばない★
   - ★`select` に `id: true` は必須★（型で強制されている。`timetable-source.ts:57-74`）
   - ★`end_at` は `jstIsoFromDb()`★（4-3）
3. ★テスト★（`lib/db/__tests__/` に足すか、段階4 のルートテストで兼ねる）:
   - fixtures 版: `planned_end_at === scheduled_at`、`facility_ids: []` で `[]`、`null` で全件
   - Prisma 版: ★`cleanLogRunMeta()` の `where` が `cleanLogs()` の `where` と `toEqual`★（4-8）／★2表の両方に同じ引数が渡る★／`end_at` が壁時計値のまま ISO になる（`2026-10-05 21:10:00` → `"2026-10-05T21:10:00+09:00"`）
4. ★`cleanLogs()` のレスポンスが変わっていないこと★: 既存の `app/api/clean-logs/__tests__/route.test.ts` / `app/api/dashboard/__tests__/route.test.ts`（`?date=` のもの）が ★期待値を変えずに★ 通ること

コミット。

---

## 段階4 — API（YAML → 型 → ルート）

### 4-1. `d-watch-api.yaml`（★先に直す。`CLAUDE.md` §6-1★）

1. `/dashboard` の `description`（`:218-220`）を書き直す:
   - `date` を省略すると ★summary と problem_logs は「各清掃エリアの直近の清掃（7日以内・終わった予定のみ・出発失敗を含む）」★。D-159
   - ★schedule_progress / rescue_required / security_banner は省略時も当日★（D-049 / D-159）
   - ★コンパクト版の表は problem_logs で返す★（旧: GET /clean-logs を再利用）。理由は D7 の1行目
2. `date` の説明（`:225`）: 「対象日。指定するとその日の清掃で summary / problem_logs を作る。省略（空文字を含む）すると直近モード」
3. `summary` に説明を足す（母集団が `date` の有無で変わる）
4. `rescue_required` の説明の「★上の『コンパクト版清掃詳細ログは本APIに含めない』には抵触しない★」（`:313-316`）を、新しい説明に合わせて直す（その前提の文が無くなるため）
5. `rescue_required` の後（`:357` の後）に `problem_logs` を足す:
   - `type: array`、`items: { $ref: "#/components/schemas/CleanLogItem" }`
   - 説明: summary と同じ母集団のうち判定が △× の行／並びは scheduled_at 降順 → id 昇順（データソースの順のまま）／★件数の上限は無い★（1エリア最大1行。画面が先頭5件を出す）／連続日数は表示上限で頭打ち（`consecutive_problem_days_capped`）

### 4-2. `lib/types/index.ts` の `DashboardResponse`（`:1146`）

- `problem_logs: CleanLogItem[]` を足す（★optional にしない★。0件は「問題の行が無い」という事実）
- `summary` の docstring に母集団（直近モード／`date` 指定）を書く。`schedule_progress` は「当日」のまま

### 4-3. 「問題あり」の規則を共有する（`lib/clean-logs/query.ts`）

- `:180` の `!["△", "×"].includes(r.grade)` を ★`export function isProblemGrade(grade: Grade): boolean`★ に切り出し、`filterCleanLogs()` はそれを呼ぶ（★動作は変えない★）
- ★`critical`（`:183`）は触らない★

### 4-4. `app/api/dashboard/route.ts`

1. ★冒頭コメント `:12-13`★（「コンパクト版清掃詳細ログはこのAPIに含めません」）を D7 に合わせて書き直す
2. `:48-55`: ★`const now = new Date()` を1回だけ★ 作り、`today = toJstIso(now).slice(0, 10)`、`const latestMode = !dateRaw`、`const date = dateRaw || today`。
   ★`||` の理由のコメント（`:49-54`）は残し、「`?date=` は直近モードになる」に直す★
3. 取得（`:80-102`）:
   - 直近モード: `const window = latestRunWindow(now, DASHBOARD_LATEST_RUN.lookbackDays)`。
     ★`cleanLogs()` と `cleanLogRunMeta()` に**同じ** `{ id: null, date_from: window.date_from, date_to: window.date_to, facility_ids: facilityIds }` を渡す★（オブジェクトを1つ作って2本に渡す）
   - `date` 指定: 今までどおり `cleanLogs({ id: null, date_from: date, date_to: date, facility_ids })`。★`cleanLogRunMeta()` は呼ばない★
   - `schedules()` / `dsAlerts()` / `rescueRequired({ date, facility_ids })` は ★そのまま★（`date` は直近モードでは当日）
   - ★`loadConsecutiveDaysMax()` を `try`/`catch` で読む★（`app/api/clean-logs/route.ts:55-63` と同じ作法。読めなければ `CONSECUTIVE_DAYS_DISPLAY.defaultMax`）
   - ★`pickLatestRunPerSection(logs, meta, { now, ...DASHBOARD_LATEST_RUN })` は `try` の中★（4-9）
4. 集計（`:115-147`）:
   - `logsOfDay` は ★今までどおり `today`（= `date`）の行★。`computeScheduleProgress(logsOfDay, …)` と `data.anomaly` は ★変えない★
   - `const summaryRows = latestMode ? picked : logsOfDay`
   - `summary = computeSummary(summaryRows)`
   - `problem_logs = capCleanLogs(summaryRows.filter((r) => isProblemGrade(r.grade)), maxDays)`
5. `:66-72` / `:85-89` のコメントの「当日」「4つすべてに同じ配列」を、`cleanLogRunMeta()` を含めて直す（★同じ `facilityIds` を5本に渡す★）。`:82` の「約360行」には「直近モードは8日ぶん」を足す

### 4-5. テスト

1. `app/api/dashboard/__tests__/route.test.ts`:
   - `StubTimetable`（`:42-55`）と `timetable()`（`:186`）に ★`end_at`★ を足す（例: 開始＋1時間）。★`select` はスタブの `project()` が処理する★
   - `:300` 「`date` 省略時は当日の1日ぶん」→ ★「直近モードは表3 W4 の窓（2026-09-23〜2026-09-30）で `start_at` が絞られる」★（`gte` = `2026-09-23T00:00:00.000Z`、`lt` = `2026-10-01T00:00:00.000Z`。壁時計値）
   - `:305` 「`?date=`（空文字）でも当日に倒れ…」→ ★「`?date=` でも直近モードの窓に倒れ、全件読みに戻らない」★（★`start_at` の条件が消えないこと★がこのテストの本題。残す）
   - `:316` 「`?date=` と省略が同じ結果」は ★そのまま★
   - ★新規★: 直近モードの `summary` が ★`{ success: 1, failed: 0, not_executed: 1, success_rate: 50 }`★（施設1の 09-30 02:00 ＝ 成功、施設2の 09-29 02:00 ＝ 未実施。★本書で導いた値★）。
     ★同じデータで `?date=2026-09-30` は `[1, 0, 0]` のまま★（`:346-352` の既存テスト）
   - ★新規★: 直近モードの `problem_logs` は施設2の 09-29 の1行だけ。`?date=2026-09-30` の `problem_logs` は `[]`
   - ★新規★: `cleanLogRunMeta()` の `where` が `cleanLogs()` の `where`（`include` 付きの1本目。`mainQueryWhere()` `:252`）と `toEqual`（4-8）。
     ★メタの呼び出しは「`select` のキーがちょうど `id` / `section_id` / `end_at` の3つ」で見分ける★ —
     要レスキュー（`loadConsecutiveInputs()` など）も `cleanTimetable.findMany` を `select` 付きで呼ぶので、「`select` 付き」だけでは見分けられない
   - ★新規★: `?date=2026-09-29` では上のメタの呼び出しが ★0回★（日付指定ではメタを読まない）
   - ★新規★: `schedule_progress` が直近モードと `?date=2026-09-30` で ★同じ★（4-1）
2. `app/api/__tests__/master-filter-dashboard-reports.test.ts`:
   - `todaysLogFacilityIds()`（`:83-90`）は ★直近モードの母集団★ に直す。★`pickLatestRunPerSection()` を呼ばずに★ テストの中で素直に書く:
     「fixtures の `cleanLogs` のうち、施設が `allowed` に入り、`Date.parse(scheduled_at) >= now − 7日` かつ `Date.parse(scheduled_at) + 30分 <= now` の行の ★`serial_number` の種類数★」（7・30 は `DASHBOARD_LATEST_RUN` から読む）
   - ★`/dashboard` の `describe` だけ時計を止める★: `vi.useFakeTimers({ toFake: ["Date"] })` → `vi.setSystemTime(new Date(\`${todayJst()}T12:00:00+09:00\`))`（★`todayJst()` は止める前に読む★）。`afterEach` で `vi.useRealTimers()`。
     ★理由★: 実時刻のままだと、深夜0時〜4時に走らせたとき当日の fixtures 行が「終わっていない」扱いになり、期待値が時刻で揺れる
   - `:156` の分割の検査（4つの顧客を足すと絞らないときと一致）は ★そのまま★ 通ること
3. `lib/clean-logs/__tests__/query.test.ts`（既存の `filterCleanLogs()` のテスト）が ★期待値を変えずに★ 通ること（4-3）

YAML → 型 → ルート → テストの順でコミット（1つでも分けてもよい）。

---

## 段階5 — 画面

### 5-1. `components/features/dashboard/DashboardScreen.tsx`

1. ★`/api/clean-logs` の取得をやめる★（`:75-86`）。`apiFetch<DashboardResponse>("/api/dashboard")` だけにし、表には `data.problem_logs` を使う。
   - `logs` の state（`:52`）・`setLogs`（`:88` / `:97`）・`ListResponse` の import は不要になれば消す
   - ★画面に出すのは先頭5件★（`data.problem_logs.slice(0, 5)`）。★「5」を新しい名前の定数にするか既存のまま直書きするかは、元のコード（`per_page: "5"`）と同じ扱いでよい★。`RESCUE_INITIAL_VISIBLE`（`lib/config/index.ts:119`）の docstring が「コンパクト版が5件（`per_page: "5"`）なので揃えた」と書いているので、★その文を「`problem_logs` の先頭5件」に直す★
   - `:89-95` の「2本のうちどちらが失敗しても…」のコメントは1本になったので直す
2. ★見出し（`:214`）★: `昨夜の清掃結果サマリー — {today}` を
   `t("直近の清掃結果サマリー（各清掃エリアの最後の清掃・{n}日以内）").replace("{n}", String(DASHBOARD_LATEST_RUN.lookbackDays))` にする。
   - ★モック（`d-watch-mock.jsx:476`）と文言が違う★ので、D-120 / D-121 と同じく ★JSX コメントで「D-159 でユーザーが変えた。モックは書き換えない」★と残す
   - `today`（`:49`）と `todayJst` の import（`:14`）が使われなくなれば消す
3. `:201-207` のコメント（「当日の清掃ログ行数」）を ★「直近モードでは、7日以内に終わった予定がある清掃エリアの数」★ に直す（`sub={\`/ ${totalLogsToday}件\`}` の変数名は `totalLogs` 等に変えてよい）
4. `:260-273` の要レスキューのコメントの「★数値カード（上）とは母集団が違う★」に「数値カードは直近モード、要レスキューは当日基準」を足す

### 5-2. `lib/i18n.ts`

`"直近の清掃結果サマリー（各清掃エリアの最後の清掃・{n}日以内）": "Latest cleaning results (last run per area, within {n} days)"` を足す（★EN は暫定。ユーザーが後でまとめて見直す★）。★キーの全角括弧・中点を原文と揃える★。重複は lint が落とす。

### 5-3. 表の各行に日付を出す（`components/features/detail-log/CleanLogTable.tsx`）

- ★compact のときだけ★、「予定」のセル（`:207`）を `${formatMd(r.scheduled_at)} ${formatHm(r.scheduled_at)}`（例 `10/5 19:00`）にする。★compact でないとき（清掃詳細ログ）は変えない★（日付列 `:206` が別にある）
- ★列は増やさない★（`<col>` / `<th>` / `<td>` の3箇所と `colSpan={compact ? 5 : 11}` を変えずに済む）。compact の「予定」列は 200px（`CleanLogTable.module.css:61`）で、`10/5 19:00` は収まる
- `CleanLogTable` の `compact` の docstring（`:24-38`）の「★落とす列★は … 日付 …」に「compact では日付を予定のセルに含める（D-159）」を足す
- ★暫定★（§11 Q2）。エリアモーダルの `dateLabel`（`DashboardScreen.tsx:163`）は既に `formatMd(row.scheduled_at)` なので変えない

### 5-4. 確認

- ★画面のテストは vitest で走らない★（`vitest.config.ts` の `include` は `lib/**` と `app/**`）ので段階7 の画面確認で見る

---

## 段階5b — 清掃詳細ログの画面の既定を「期間を指定しない（全期間）」にする（★ユーザー指示 2026-10-06「C」。D-159 追記★）

★背景★: ダッシュボードのカードと「全項目 →」は `/detail-log` に移るが、画面は開いたときに `date_from` = `date_to` = 今日（`todayJst()`）で API を呼ぶ（`components/features/detail-log/DetailLogScreen.tsx` の `useState(today)` 2つ。2026-10-06 時点で :60-61 付近）。
★昨夜 19:00 の出発失敗はダッシュボードには出るのに、移った先には出ない★。

1. ★画面の初期値を「期間なし」にする★: `dateFrom` / `dateTo` の初期値を空文字にし、★空のときは `date_from` / `date_to` を URL に付けない★（API は期間なし＝全期間を返す。`lib/clean-logs/query.ts` の既存の挙動。★API は変えない★）。
2. 期間の入力欄は空で表示する。片方だけ入れた場合・期間の検証（`MAX_DATE_RANGE_DAYS`、開始 ≦ 終了）は★両方入っているときだけ★行う。既存の検証メッセージ（`DetailLogScreen.tsx` :167 / :175 付近）の条件をそれに合わせる。
3. ★絞り込みの既定「問題あり」（`gradeFilter` = `"problem"`）は変えない★。
4. ★CSV 出力も同じクエリを使う★ので、期間なしなら全期間の CSV になる。画面の件数表示・ページ送りは既存のまま。
5. 文言: 期間が空のときに「全期間」と分かる表示を入れる（例: 入力欄の placeholder「指定なし（全期間）」。EN「Not set (all)」。i18n 経由）。
6. テスト: 初回の取得 URL に `date_from` / `date_to` が付かないこと、片方だけ入れたとき検証エラーにならず送らない（または両方そろうまで送らない — ★既存の挙動に近いほうを採り、理由をコミットに書く★）、両方入れたときは従来どおり。
7. ★性能の注意（監督→ユーザーに伝えた）★: 期間なしの取得は、Prisma 版では ★保持期間（2年）の予定を全部読んでからページ分けする★（`lib/db/index.ts` の `cleanLogs()`。指示書AC で日付を必須にして避けていた形）。ステージングでは100行程度で問題ないが、本番の想定（約26万行）では重くなる。★本書では対策しない★（リリース前に見直す）。★ログ（`db.query` 系の所要時間）で観測できることだけ確かめる★。

---

## 段階6 — 資料（本書で直すのは次の2つだけ）

1. `docs/fixtures.md` §12（`:363-`）:
   - `:371-372` の「`date` 省略時は当日＝daysBack=0」に「★`schedule_progress` は省略時も当日。`summary` と `problem_logs` は直近モード（D-159）★」を足す
   - ★§12.3 を新設★: 「`date` 省略時（直近モード）に何が出るか」。fixtures は ★終了＝開始・清掃エリア＝ロボット★（D10）。★JST 4:00 以降★ に開くと
     ★15行（ロボット15台）: 成功9 / 失敗5 / 未実施1 / 成功率60%★、△× は6行で表には当日の5行（施設12・10・6・9・8）が出る。
     ★訂正（2026-10-06 実装時）: 並びは 12・10・★9・6★・8★。施設9 と施設6 は同じ 02:30 で、fixtures の `cleanLogs` の並べ替えは `scheduled_at` しか見ないため、
     同じ時刻は実際には id の降順になる（Prisma 版は id 昇順なので DB では 6・9）。顔ぶれは同じ。`docs/fixtures.md` §12.3 が正（§13）★
     ★4:00 より前は当日の行の一部が「終わっていない」扱いになり、前日以前の行が出る（施設12 は当日しか行が無いので消える）。それが正しい★
     （★この数字は本書で fixtures から数えたもの。段階7 の画面確認で違えば、どちらが正しいかを報告★）
2. ★`app/api/dashboard/route.ts` / `lib/types/index.ts` / `d-watch-api.yaml` の説明★（段階4 で直したもの）

★`CLAUDE.md` / `docs/decisions.md` / `docs/code-map.md` / `docs/spec.md` は監督が直す★（§9）。

---

## 段階7 — 全体の確認

1. `npx tsc --noEmit -p .` — エラー0件
2. `npm run lint` — 指摘0件（i18n の重複キー・`$queryRaw`・`any`・未使用の import が無いこと）
3. `npm test` — ★全件通過★（表1〜表3 を含む）
4. ★`USE_FIXTURES=true` で `npm run build`★ — 成功（PowerShell なら `$env:USE_FIXTURES='true'; npm run build`）
5. ★画面で確かめる（`USE_FIXTURES=true`）★: `.claude/launch.json` の ★`d-watch-fixtures`（ポート 3100）★ で起動する（Claude Code なら `preview_start` の `d-watch-fixtures`）。
   ★確かめ終わったら、自分で起動した dev サーバーは必ず止める★
   - ★見出し★が「直近の清掃結果サマリー（各清掃エリアの最後の清掃・7日以内）」。EN で "Latest cleaning results (last run per area, within 7 days)"
   - ★数値カード★が段階6 §12.3 の値（JST 4:00 以降）。★スケジュール進捗は今までと同じ値★（`docs/fixtures.md` §12.1 の「当日」の行: 予定10・実施9）
   - ★清掃詳細ログ（コンパクト版）★が5行で、各行の「予定」が `M/D HH:mm`。行を開くとアドバイスが出る・エリア別詳細のモーダルが開く（`id` は今までと同じ）
   - ★要レスキュー・セキュリティバナー★が変わっていないこと
   - ★ネットワーク★: 初期表示で `/api/clean-logs` が ★呼ばれない★こと（`/api/dashboard` の1本だけ）
   - ★JP/EN 切替で表の列幅が動かないこと★、モバイル幅（375px）で見出しが不自然に崩れないこと。崩れていたら直さずに報告
6. 最後の grep（★0件になること★）:
   ```
   grep -rn "昨夜の清掃結果サマリー" app components lib --include=*.ts --include=*.tsx
   grep -rn "api/clean-logs?" components/features/dashboard
   grep -rnE "(lookbackDays|settleMinutes)\s*[:=]\s*[0-9]" app components lib --include=*.ts --include=*.tsx | grep -v "lib/config/index.ts" | grep -v __tests__
   ```

---

## 8. テスト一覧（本書で足す・直すもの）

| ファイル | 内容 | 段階 |
|---|---|---|
| `lib/dashboard/__tests__/latest-run.test.ts`（新規） | 表1 L1〜L10・サマリ／表2 E1〜E6／表3 W1〜W4 | 1 |
| `lib/config/__tests__/dashboard-latest-run.test.ts`（新規） | 設定値の範囲 | 1 |
| `lib/db/__tests__/`（新規または既存に追加） | `cleanLogRunMeta()` の fixtures 版・Prisma 版（`where` の一致・`end_at` の変換） | 3 |
| `app/api/dashboard/__tests__/route.test.ts` | 窓・直近モードの `summary` / `problem_logs`・メタの `where`・日付指定でメタを読まない・`schedule_progress` が変わらない | 4 |
| `app/api/__tests__/master-filter-dashboard-reports.test.ts` | 母集団の期待値を直近モードに・時計を止める | 4 |
| `lib/clean-logs/__tests__/query.test.ts` | `isProblemGrade()` の切り出しで期待値が変わらないこと | 4 |
| `lib/dashboard/__tests__/aggregate.test.ts` | ★変えない★（`computeSummary()` / `computeScheduleProgress()` は変えない） | — |

---

## 9. 触ってはいけないもの

- `CLAUDE.md` / `docs/decisions.md` / `docs/code-map.md` / `docs/spec.md`（監督が直す）
- `d-watch-mock.jsx`（文言の違いはコメントで残す。5-1）
- `prisma/schema.prisma` / `lib/generated/prisma/**`（★DB の定義は TeamWorks の領域。`CLAUDE.md` §7★）
- `computeSummary()` / `computeScheduleProgress()` / `computeSecurityBanner()` の中身（`lib/dashboard/aggregate.ts`）
- ★`db.rescueRequired()` とその基準日（当日）★・`buildRescueRequired()` の並び
- `isFutureSchedule()` / `notExecutedFromReport()` / `latestReportByTimetableId()` / `completionRateFromReport()` の中身（`lib/db/clean-log-mapping.ts`）
- ★`prismaSource.cleanLogs()` の `include` / `orderBy` / 行の組み立て / 連続日数★（★`where` の切り出し以外は触らない★。段階3-3）
- `findTimetables()` / `mergeTimetableRows()` の中身（`lib/db/timetable-source.ts`）
- `LAUNCH_GRACE_MINUTES` / `GRADE_THRESHOLDS` / `judgeGrade()` / `displayRate()`
- `lib/fixtures/clean-logs.ts` / `lib/fixtures/schedules.ts` のデータ（★終了時刻・清掃エリアを足さない★。D10）
- `/api/clean-logs` と `/api/clean-logs/export` の契約・`parseCleanLogQuery()`（★新しいパラメータを足さない★。D7）
- 清掃詳細ログ画面（`DetailLogScreen.tsx`）の既定の期間（当日）— §11 Q5
  ★→ 段階5b で上書き（ユーザー指示「C」。D-159 追記）。既定は「期間を指定しない（全期間）」に変えた★
- 4カラム UPDATE（`status` / `comment` / `responded_at` / `resolved_at`）
- `scripts/seed-local-db.ts` の接続先とガード（`lib/db/local-guard.ts`）

---

## 10. 範囲外（本書ではやらない。監督が扱う）

- ★清掃詳細ログ画面の既定の期間★、★数値カード（失敗・未実施）を押したときの遷移先の条件★（§11 Q5）
  ★→ 段階5b で上書き（ユーザー指示「C」）。既定の期間は本書で扱った（全期間）。遷移先の条件（URL に絞り込みを載せること）は範囲外のまま★
- 遡り日数・落ち着き時間をシステム管理画面から変えられるようにすること（§11 Q6）
- 7日以内に予定が無い清掃エリアを画面に知らせること（§11 Q7）

---

## 11. 監督への質問 → ★監督の決定（2026-10-06）: Q1〜Q10 はすべて「本書の暫定」のとおりで確定★

★本文は暫定のまま正しい（直す箇所は無い）★。Q5・Q7・Q8 は範囲外としてユーザーに伝える（監督）。
★→ Q5 はその後ユーザー指示「C」（2026-10-06）で段階5b として本書に入った（既定の期間を全期間に）★。

（以下は作成時の質問。記録として残す）

### 作成時の質問

★回答で暫定が変わったら、「直す場所」の列の箇所を ★同時に★ 直してから渡すこと★。

| # | 論点 | 本書の暫定 | 直す場所 |
|---|---|---|---|
| Q1 | 同じ清掃エリアで開始が同じ予定が2件あったら、どちらを出すか | id の大きいほう（D5） | §2 D5 / §3 表2 E5 / 段階2-3 の 4 |
| Q2 | コンパクト版の表に日付をどう出すか（モックは日付列なし） | 「予定」のセルを `M/D HH:mm` にする（列は増やさない） | §2（D 行の追加）/ 段階5-3 / 段階7-5 |
| Q3 | YAML の「コンパクト版は本APIに含めない」を覆して `problem_logs` を `/dashboard` に載せてよいか（対案: `/clean-logs?latest=true`） | 載せる（D7） | §2 D7 / 段階4 全体 / 段階5-1 の 1 |
| Q4 | `problem_logs` の件数 | 上限なしで返し、画面が先頭5件（D11）。「もっと表示」は付けない（今と同じ） | §2 D11 / 段階4-1 の 5 / 段階5-1 の 1 |
| Q5 | 数値カード（失敗・未実施）と「全項目 →」は `/detail-log` に飛ぶが、清掃詳細ログの既定は ★当日だけ★（`DetailLogScreen.tsx:46` / `:60`）。ダッシュボードに出た前日 19:00 の失敗が遷移先に出ない | 本書では変えない（§10）。★→ 段階5b で上書き（ユーザー指示「C」）。既定を全期間にした★ | 段階5b / §9 / §10 |
| Q6 | 遡り日数・落ち着き時間をシステム管理画面の設定にするか | コードの定数（`lib/config`） | §2 D6 / 段階2-1 |
| Q7 | 7日以内に予定が無いエリアは黙って消える（収集処理が止まったエリアも消える）。件数を出すか | 出さない（D3。D-159 項3 のまま）。要レスキュー（当日基準）・スケジュール進捗の `data.anomaly` が手がかり | §2 D3 |
| Q8 | 落ち着き30分で足りるか（レポートが書かれるまでの遅れは未確認）。足りないと、終わった清掃が一時的に「未実施」に見える | 30分（D-159 項4 の既定）。TeamWorks に書き込みの遅れを聞くかは監督判断 | — |
| Q9 | fixtures で清掃エリアの代わりにロボット（`serial_number`）を使ってよいか | 使う（D10） | §2 D10 / 段階3-2 / 段階6 §12.3 |
| Q10 | 表2（E1〜E6）・表3（W1〜W4）・段階4-5 の直近モードの期待値（成功1・未実施1）・段階6 の fixtures の数字を正にしてよいか | 本書で導いた値 | §3 / 段階4-5 / 段階6 |

---

## 12. コミット

段階ごとに分ける（段階1 / 段階2 / 段階3 / 段階4 / 段階5 / 段階6）。★`git add` はファイルパスを明示する★（`git add -A` 禁止）。
メッセージには ★何を変えたか★ と ★なぜ（D-159。ダッシュボードを各清掃エリアの直近の清掃にする）★ を書く。
★段階1 のコミットは「テストだけ（この時点では落ちる）」と明記する★。
★PR の作成とマージは daichi がレビューしてから行う★（D-154）。

---

## 13. 実装の記録（2026-10-06）

★**実装は Cursor ではなく Claude Code**★（ユーザー指示 2026-10-06。本書の扱いを聞いた質問に「ここで実装する」と回答）。
監督がサブエージェント2本（★バックエンド: 段階1〜4★ / ★画面・資料: 段階5・5b・6★）に分け、ワークツリーで実装 → develop に cherry-pick。
★期待値表（§3 表1）は変えていない★。記録の正は `docs/decisions.md` D-159 の「実装の記録」と `docs/handoff.md` 追記61。

### 13-1. コミット（develop。★push 済み。別セッションの `f424689` の push に含まれた（2026-10-06）★）

| 段階 | コミット |
|---|---|
| 1（テストだけ。この時点では落ちる） | `1501173` |
| 2（設定値・純粋関数） | `59c5b97` |
| 3（`cleanLogRunMeta()`） | `53e306b` |
| 4（YAML → ルート → テスト） | `ee26db1` / `b9adbaa` / `a21ddd7` |
| 5-2・5-3（i18n・表の「予定」に日付） | `a1ef9a4` |
| 6（`docs/fixtures.md` §12.3） | `ac2b894` |
| 5-1（ダッシュボードを `/api/dashboard` の1本に・見出し） | `939a2c5` |
| 5b（清掃詳細ログの既定を全期間に） | `137a94b` |

`939a2c5` のメッセージ本文の1行で `t("…")` の二重引用符が落ちている（中身に影響なし。直していない）。

### 13-2. 確認（監督。`137a94b` 時点）

- `npx tsc --noEmit -p .` 0件 / `npm run lint` 0件 / vitest ★111ファイル・2,601件 通過★（着手前 106・2,540）/ `USE_FIXTURES=true npm run build` 成功 / 段階7-6 の grep 3本とも0件
- ★画面（当方の fixtures 由来）★: 本番ビルドを `next start -p 3100`、Edge ヘッドレスで撮影（2026-10-06 15:1x JST）。
  見出しは JP・EN とも本書どおり／数値カード ★9・5・1・60%（/ 15件）★／表の「予定」は `10/6 03:30` 形式／
  ★初期表示の API は `/api/dashboard` の1本だけ★／スケジュール進捗 予定10・実施9（従来どおり）／
  清掃詳細ログは「指定なし（全期間）」で `date_from`・`date_to` なしで取得し、前日の行も出る
- ★fixtures の `/api/dashboard`（当方の fixtures 由来）★: `problem_logs` 6行、先頭5件は施設 ★12・10・9・6・8★ → `docs/fixtures.md` §12.3 と一致。
  ★段階6 に書いた 12・10・6・9・8 は誤り★（同時刻 02:30 の施設9・6 は、fixtures では id 降順。Prisma 版は id 昇順）
- ★ステージング（先方投入データ）★: 既存の dev サーバー :3000（`USE_FIXTURES=false`）、2026-10-06 15:0x JST。
  直近モードで ★11エリア、成功9・失敗1・未実施1（成功率81.8%）★、`problem_logs` 2行（10/5 19:26 の未実施、10/5 10:00 の ×）。
  `?date=当日` は従来どおり0件。スケジュール進捗は両モードで同じ（予定11・実施0）

### 13-3. 段階5b の「片方だけ日付を入れた」扱い

- ★入っている側だけを送る（検証エラーにしない）★。理由: 5b 以前の画面と同じ挙動（以前も片方を消すと空文字が飛び、API は空の側を指定なしと読んでいた）
- 却下: 「両方そろうまで送らない」（入力途中に古い結果が残る）／「片方だけなら捨てる」（入力した日付が黙って無視される）
- 検証と URL の組み立ては ★`lib/clean-logs/detail-log-date-range.ts`★（新規・純粋関数。`appendDateRangeParams()` / `dateRangeError()`。テスト20件）
- `<input type="date">` の placeholder は多くのブラウザで出ないので、★両方空のあいだ入力欄の前にも「指定なし（全期間）」を出している★（§段階5b-5 の例より一歩踏み込んだ）

### 13-4. 残したもの

- ★375px 幅で見出しが4行に折り返す★。原因はサイドバーが畳まれず幅の大半を占めること（本書以前からの作り）。★直していない★（段階7-5「崩れていたら直さずに報告」）
- ★期間なしの取得の性能★（段階5b-7）— リリース前に見直す（D-159 追記）
- 運用上の知見: ★ワークツリーでは Turbopack が node_modules のジャンクションを拒否して `npm run build` が落ちる★（"Symlink [project]/node_modules is invalid"）。
  ビルドは本体で行った（dev サーバーは `.next/dev` を使うので :3000 を動かしたままで可）
