# 設計 — 事前チェック結果の取得と、予定への時刻での紐づけ（2026-10-27）

**状態**: ★取り下げ（2026-10-27 同日）★。実装しない。却下した案の記録として残す。
- ユーザー指示「方針変更で、プリチェックは顧客にも別の方法で知らせられるのでリアルタイム性は気にしないでいいです」
- 「前日以降の清掃タスクは clean_timetable_histories など…に clean_timetable_id が入っているのでそれで予定タスクか予定外タスクかの判断をしてください、プリチェックはテーブルにあるのでそれで判断してください」
- → 予定への紐づけは ★`clean_timetable_id` のまま★（NULL ＝ 予定外のタスク）、事前チェックかどうかは ★予定の行の `precheck` 列★で決める。★今のコードがすでにこの作り★なので変更なし
- 時刻での紐づけ（§3）と事前チェックの状態（§4）は採らない。理由: リアルタイム性が不要になり、`clean_timetable_id` が入ることが確認できたため

（以下は取り下げ前の設計案）
**根拠**: 監督の実測（ステージング・読み取りのみ・2026-10-27 時点の先方投入ダミー）と、サブエージェント4本の調査。

---

## 0. ユーザーの要望（2026-10-27）

- 「清掃詳細などは time_table_id をみていますが、見ない形に変更してください。started_at など時刻で確認してください」
- 「エラーも clean_timetable_id だと思うので同じように確認してください」
- 「事前チェックが成功しているか失敗しているかはアラートで確認して、必要であれば店舗に連絡する必要があるので、自動更新にする必要はないですが、リロードしたときにその時の値が入ってくるのが望ましいです」
- 「どこのテーブルを正として情報を引っ張ってくるかは検討中なので、リロードしたときに見えるような仕組みの設計を考えてください」

## 1. 今の作り（調査結果）

### 1-1. リロードで今の値が出るか

- 出る。キャッシュの層はリポジトリ内に無い
  - API ルート（`/api/prechecks`・`/api/clean-logs`）は `dynamic` / `revalidate` / `use cache` なし。Next 16 の Route Handler は既定でキャッシュしない（`node_modules/next/dist/docs/01-app/01-getting-started/15-route-handlers.md`）
  - `lib/api/client.ts` の `apiFetch` は `cache` 指定なし。応答に `max-age` / `Last-Modified` なし
  - `lib/db` のメモリ上のキャッシュは Prisma の接続だけ
  - 本番の nginx の `proxy_cache` は未確認（リポジトリに設定が無い）
- 期間の既定は両画面とも「指定なし（全期間）」なので今夜の分も入る

### 1-2. 事前チェックの結果の決め方

- 結果は「事前チェックの予定行に `robot_error_reports.clean_timetable_id` でつながるエラー行があるか」だけで決まる（`lib/db/precheck-mapping.ts` `statusColumnsFromPrecheckRows`）
  - エラーあり → その内容 ／ エラーなし → 「待機」 ／ 予定行なし → 「-」
- 「異常なしで通過」「エラー行がまだ書かれていない」「まだ実施前」が全部「待機」になる。成功を示す記録は読んでいない
- アラート（`/api/prechecks`）に出るのは、エラー行がある回だけ（と過去の本清掃の出発失敗）
- 清掃詳細ログは、本清掃の開始時刻を過ぎるまで行そのものを出さない（`lib/db/index.ts` の `isFutureSchedule`）。02:00 の事前チェックの結果は 03:00 の本清掃が始まるまで詳細ログに出ない

### 1-3. `clean_timetable_id` を使っている箇所

| 列 | 使い道 |
|---|---|
| `clean_task_reports.clean_timetable_id` | 清掃詳細ログ・CSV・ダッシュボード・清掃エリアの完了率と未実施（`cleanLogs()` / `areaCompletions()`）、アラートの出発失敗（`prechecks()` / `precheckItemById()`）、レポート当期KPI・施設別成績、連続日数 L1〜L3、清掃エリア（`clean-area-source.ts`。重複検知も id の groupBy）。突き合わせは `latestReportByTimetableId()`（`lib/db/clean-log-mapping.ts`）に集まるが、レポートを引くクエリ（`clean_timetable_id IN (...)`）は7か所に重複 |
| `robot_error_reports.clean_timetable_id` | 清掃詳細ログの事前チェック列、アラート（事前チェックかどうか・何分前か・親の本清掃）、レポートのエラー率の分子とエラー内訳（本清掃のエラーだけ数える。D-114）、エラー一覧の「紐づく予定」（なし／不明／本清掃／事前チェック） |

### 1-4. ステージングの実データ（2026-10-27・先方投入のダミー・読み取りのみ）

- `clean_task_reports` 1行（`clean_timetable_id` は NULL）。時刻での照合はまだ試せない
- `clean_timetables` 1,047行（本清掃 539・事前チェック 60分前 253・30分前 254・10分前 1）
  - 事前チェックの行は開始＝終了（幅ゼロ）。全行に `parent_id`（本清掃の id）がある
  - 同じ清掃エリアで本清掃の時間帯が重なる組が112組（開始が同じ組は0）
- `robot_error_reports` 49行中48行に `clean_timetable_id` あり
  - 本清掃のエラー31件: 全件 `errored_at` が予定の時間帯に入る。うち16件は重なる別の本清掃にも入るが、「開始が直前の予定」を採れば全件が今の id と一致
  - 事前チェックのエラー17件: `errored_at` が事前チェックの時刻ちょうど。下の区間の決め方なら全件が今の id と同じ回に入る

## 2. 方針

1. **予定への紐づけは、時刻と清掃エリアで決める純粋関数1本にまとめる**（レポートにもエラーにも使う）
2. **事前チェック1回ごとに「今の状態」を出す判定を、純粋関数1本にまとめる**（`lib/judge/`）
3. **結果の取得元（テーブル）は差し替えられる読み口（アダプタ）の裏に隠す**。画面と API は取得元を知らない
4. 自動更新はしない。リロードのたびに DB を読み、その時刻（`now`）で判定する

## 3. 時刻での紐づけ（`lib/db/time-match.ts`）

`matchEventsToSchedules(schedules, events, policy) → { bySchedule, unmatched }`

- 予定: `id` / `sectionId` / `startAt` / `endAt` / `precheck` / `parentId`（本表と履歴の2表。同じ id は本表を採る。`lib/db/timetable-source.ts`）
- 出来事: レポートは `started_at`、エラーは `errored_at ?? created_at`（`detectedAtFromError()` を使う）。清掃エリアは `section_id`
- 区間（案。§7 の Q1〜Q3 で決める）
  - 事前チェックの回: [その回の時刻, 次の回の時刻、無ければ本清掃の開始)
  - 本清掃: [開始, 終了]
  - 例: 本清掃 03:00〜04:30、60分前 02:00、30分前 02:30 → 02:00〜02:29:59 は60分前、02:30〜02:59:59 は30分前、03:00〜04:30 は本清掃
- 重なったとき: 区間に入る候補のうち、開始が出来事の時刻以前で最も遅いもの（同時刻なら id の大きい方）
- どれにも入らない出来事は `unmatched` で返し、ログに出す（黙って捨てない）
- 未決の点は `lib/config` の設定（`SCHEDULE_TIME_MATCH`）に置く: 本清掃の終了後の余裕、開始より前を許すか、レポートを完全一致で照合するか時間帯で照合するか
- 帰結
  - 予定外のタスク ＝ どの本清掃にも当たらないレポート（旧: `clean_timetable_id` が NULL）
  - エラー一覧の「紐づく予定」の「不明（id はあるが予定が無い）」は無くなる
  - 清掃エリアの重複検知は「1つの予定に2件以上のレポートが当たった」に変わる

## 4. 事前チェック1回の状態（`lib/judge/precheck-state.ts`）

`judgePrecheckSlot({ slotAt, now, evidence, reportsPass, waitMinutes }) → { state, basis, label }`

上から順に評価し、最初に当てはまったものを採る（時刻は `fromDbJst()` で直した値）。

| 順 | 条件 | 状態 | 画面の表示（案） |
|---|---|---|---|
| 1 | 事前チェックの予定行が無い | なし | -（D-171 のまま） |
| 2 | 今 ＜ 回の時刻 | 予定前 | 予定前 |
| 3 | 失敗の記録が1件でもある | 失敗 | エラーの内容（今と同じ） |
| 4 | 成功の記録がある | 成功 | 成功 |
| 5 | 今 ＜ 回の時刻＋待ち時間（`PRECHECK_RESULT_WAIT_MINUTES`） | 結果待ち | 結果待ち |
| 6 | 取得元が成功を記録できない表（`robot_error_reports` だけ） | 成功（推定） | 異常なし |
| 7 | 取得元が成功を記録できる表なのに記録が無い | 判定不能 | 結果なし |

例（本清掃 03:00、事前チェック 02:00 / 02:30、待ち時間15分、取得元は `robot_error_reports`）:

| 見た時刻 | 1回目（02:00） | 2回目（02:30） |
|---|---|---|
| 01:50 | 予定前 | 予定前 |
| 02:05（エラーはまだ無い） | 結果待ち | 予定前 |
| 02:05（02:01 のエラーが書かれた後） | エラーの内容 | 予定前 |
| 02:40（エラーなし） | 異常なし | 結果待ち |
| 翌朝 | 異常なし／エラーの内容 | 異常なし／エラーの内容 |

- 遅れて書かれたエラーで「異常なし」が後から失敗に変わりうる。自動更新は無いので、リロードした時点の値が正
- ◎○△× と出発成功・失敗には使わない（D-170。`lib/dashboard/__tests__/aggregate.test.ts` が見張る）

## 5. 取得元の差し替え（`lib/prechecks/result-source.ts`・`lib/db/precheck-sources/`）

- 型: `PrecheckResultSource { name; reportsPass: boolean; evidenceForSlots(slots) → Map<slotId, PrecheckEvidence[]> }`
  - `PrecheckEvidence = { kind: "fail" | "pass"; at: Date; label: string | null; errorId?: string }`
- アダプタ
  - `robot-error-reports.ts`（今すぐ作る。`reportsPass = false`。`section_id` と時刻の範囲で引き、§3 で回に振り分ける）
  - `fixture.ts`（fixtures 用）
  - 取得元が決まったら同じ型で1本足し、`lib/config` の `PRECHECK_RESULT_SOURCE` を変えるだけで切り替える。複数の表を合わせる `combineSources()` も用意する
- 取得元の候補（`prisma/schema.prisma`）
  - `robot_error_reports`: 失敗だけ。今使える
  - `clean_status_histories`: 予定の行ごとに `clean_error`（0 正常 / 1 未実施 / 2 セキュリティ / 3 制御盤 / 4 ロボット異常）。成功も失敗も取れる最有力候補。ただし CLAUDE.md §4.7 で「読まない」と決めてあり、ステージングは0行（10-01 時点）
  - `clean_statuses`: 同じ列だが現在値だけ。過去に遡れない
  - `robots.status` などの現在値: チェック時点の状態は復元できないので使わない

## 6. API と画面（`d-watch-api.yaml` を先に直す。追加だけ）

- `CleanLogItem` に `status_1h_result` / `status_30m_result`（`upcoming | pending | passed | failed | unknown`。`status_1h` が null のときだけ null）を足す。既存の `status_1h` の文字列は変えない
- アラートで成功も見せるなら、事前チェックの回を1件とする一覧が要る（今の `/prechecks` はエラー行が1件の単位で、PATCH の id もエラー）。案: `GET /precheck-slots`（絞り込みは `/prechecks` と同じ。1件 = 1回。`state`・何分前・回の時刻・本清掃の予定時刻・エラーの id 一覧）
- 両ルートに `Cache-Control: no-store` を明示する（今もキャッシュされる根拠は無いが、本番の nginx 未確認のための保険）

## 7. ユーザーに決めてもらう論点

| # | 論点 | 案 |
|---|---|---|
| Q1 | レポートを予定に当てる方法 | 推奨: 時間帯（開始 ≦ started_at ≦ 終了、重なれば開始が直前の予定）／ 開始時刻の完全一致 |
| Q2 | 事前チェックの回の区間 | 推奨: 次の回の手前まで ／ 回の時刻ちょうどだけ |
| Q3 | 本清掃の終了後のエラー | 推奨: 紐づく予定なし ／ 直前の本清掃に寄せる |
| Q4 | 結果待ちの時間（`PRECHECK_RESULT_WAIT_MINUTES`） | TeamWorks がエラー行を書くまでの遅れ次第。仮に15分 |
| Q5 | アラート画面に出す回 | 失敗と結果待ちだけ ／ 全部の回（異常なしも） |
| Q6 | エラーしか無い表での「異常なし」の表示 | 「異常なし」と出す ／ 「エラーなし」など推定と分かる文言 |

## 8. 段階

0. 期待値表を先に固める（分担しない）
   - 時刻の照合: 回の時刻ちょうど、次の回の時刻ちょうど、本清掃の開始・終了ちょうど、終了の1秒後、重なる本清掃、`errored_at` が null、日またぎ（23:30 のチェック → 00:00 の本清掃）、別の清掃エリア、予定外のタスク
   - 状態: §4 の例、回の時刻の1秒前・ちょうど、待ち時間の1秒前・ちょうど、成功と失敗の両方、`reportsPass` が true で記録なし、遅れて書かれたエラー
1. 取得元が決まらなくても作れる部分: 設定値、`time-match.ts`、`precheck-state.ts`、読み口の型、`robot_error_reports` と fixtures のアダプタ、テスト
2. 既存の経路を時刻照合へ移す: `cleanLogs()` / `areaCompletions()` / `prechecks()` / `precheckItemById()` / レポート当期 / 連続日数 / 清掃エリア / エラー一覧。7か所に重複しているレポートの取得は1本にまとめる
3. YAML → API → 画面（アラート・清掃詳細ログ・エラー一覧の「紐づく予定」）
4. 取得元が決まったらアダプタを足して設定を切り替える（API は変わらない）

## 9. 危険

1. レポートの紐づけを変えると、出発失敗・完了率・◎○△× が変わりうる。TeamWorks のサマリ（確定期間）が `clean_timetable_id` で集計していると、当期（時刻照合）と確定期間（id）で数え方が食い違う。照会が要る
2. `robot_error_reports` だけでの「異常なし」は推定。データ収集が止まっていると、誤って異常なしに見える
3. `robot_error_reports.errored_at` と `clean_task_reports.started_at` には索引が無い（`section_id` の索引に頼る）
4. `errored_at` が NULL で `created_at` に頼る行は、TIMESTAMP の時刻ずれが残っていると誤る
5. PATCH で使う `precheckItemById()` も時刻照合になり、404 になる条件が変わる
6. fixtures（`lib/fixtures/clean-task-reports.ts`）の `clean_timetable_id` が NULL でも予定時刻と同じ `started_at` の行は、時刻照合では「予定あり」に変わる。テストの期待値の見直しが要る
