日付: 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 で確かめたもの★。段階が進むとずれるので、関数名・文で探すこと。
| # | やること | 段階 |
|---|---|---|
| 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 の契約★。
todayJst())の清掃★を数えていた (app/api/dashboard/route.ts:55 / :90 / :117)。画面のコンパクト版の表も ★当日だけ★ を引いている (components/features/dashboard/DashboardScreen.tsx:77-85。date_from = date_to = today)。| # | 決定 | 理由 | ||
|---|---|---|---|---|
| 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) |
/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 に足すなら、ルートが既に持っている行を返すだけ。d-watch-api.yaml:219-220)を覆す★ことになる。あれの趣旨は 「既存の一覧APIで足りるものを二重に作るな」(同 :313-316)で、★「各エリアの直近1件」は既存の一覧APIでは表せない★ので趣旨には反しない。 ★YAML の記述は段階4 で書き直す★(★§11 Q3 で監督に確認★)。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)。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)。
★テストの書き方の注意★:
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")。| # | 入力 | 期待 | 確かめること |
|---|---|---|---|
| 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() は未来の予定を返さないが、メタは返しうる |
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-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)。ファイルパスを明示する |
git status が clean で、develop が 6c944cf 以降であること(★.claude/worktrees/ ではなく本体で作業する★)。npx tsc --noEmit -p . と npm test が通る状態から始める(通らなければ着手前に報告)。grep -rn "todayJst" app/api/dashboard components/features/dashboard app/api/__tests__/master-filter-dashboard-reports.test.ts
lib/dashboard/__tests__/latest-run.test.ts(新規)★:
describe("pickLatestRunPerSection — 表1"): it.each で L1〜L10 を1件ずつ(★選ばれた id の集合★を比べる)+ ★全部まとめて渡したときに9行★it("サマリは既存の computeSummary() に選ばれた行を渡したものと等しい"): §3 表1 の下の書き方どおりdescribe("pickLatestRunPerSection — 表2"): E1〜E6describe("latestRunWindow — 表3"): W1〜W4DASHBOARD_LATEST_RUN から読む★(7 / 30 を直書きしない。表1 の前提が設定値と一致することも1件で確かめる)lib/config/__tests__/dashboard-latest-run.test.ts(新規)★: lookbackDays は1以上の整数、settleMinutes は0以上の整数。 ★DASHBOARD_LATEST_RUN.settleMinutes が LAUNCH_GRACE_MINUTES の参照ではない★ことはテストにできないので、コメントで D6 を書くlib/config/index.tsLAUNCH_GRACE_MINUTES(:191-195)の ★直後★ に足す:
/**
* ダッシュボード(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;
lib/types/index.ts — 内部用の型CleanLogDbQuery(:117-166)の後に足す。★YAML には載せない(API に出ない)★ことを docstring に書く:
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 と取り違えうるため。
lib/dashboard/latest-run.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():
meta を id で引けるようにする。★同じ id が2回 → 例外★(E2)[dashboard] pickLatestRunPerSection: clean_timetables(id=…) の予定の終了・清掃エリアがありません)Date.parse(row.scheduled_at) >= now − lookbackDays × 86_400_000 かつ Date.parse(meta.planned_end_at) + settleMinutes × 60_000 <= nowsection_key ごとに、開始(Date.parse(scheduled_at))が一番大きい行。★同じなら id が大きいほう★(D5)rows.filter(選ばれた id)★(入力の順を保つ。E4)。★行を書き換えない・丸めない★isFutureSchedule() をここで呼ばない★(未来の予定は cleanLogs() が既に外している。条件3 の「終わった」で足りる)段階1 のテストが通ること。コミット。
cleanLogRunMeta())DataSource(lib/db/index.ts:203-215)cleanLogs() の直後に足す:
/**
* ★清掃ログの id → { 清掃エリア, 予定の終了 }★(ダッシュボードの直近モード専用。D-159 / 指示書AV)。
* ★`cleanLogs()` と同じ引数を渡すこと★(同じ `where`。突き合わせは `pickLatestRunPerSection()`)。
* ★`cleanLogs()` が返さない id(未来の予定)を含んでよい★。逆(`cleanLogs()` の行に対応が無い)は呼び出し側の例外
*/
cleanLogRunMeta(query: CleanLogDbQuery): Promise<CleanLogRunMeta[]>;
lib/db/index.ts:476-478 の cleanLogs() の直後)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() と同じになる。
cleanLogs() の where(lib/db/index.ts:779-803)を関数に切り出す★(同じファイル内の非公開関数。例 mainTimetableWhere(query: CleanLogDbQuery))。 const range = dbDateRange(...)(:774)も中に入れる。★中身は1文字も変えない★(コメントも一緒に移す)。 cleanLogs() は where: mainTimetableWhere(query) にする。★include / orderBy(:804-823)は動かさない★cleanLogRunMeta() を足す:
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)lib/db/__tests__/ に足すか、段階4 のルートテストで兼ねる):
planned_end_at === scheduled_at、facility_ids: [] で []、null で全件cleanLogRunMeta() の where が cleanLogs() の where と toEqual★(4-8)/★2表の両方に同じ引数が渡る★/end_at が壁時計値のまま ISO になる(2026-10-05 21:10:00 → "2026-10-05T21:10:00+09:00")cleanLogs() のレスポンスが変わっていないこと★: 既存の app/api/clean-logs/__tests__/route.test.ts / app/api/dashboard/__tests__/route.test.ts(?date= のもの)が ★期待値を変えずに★ 通ることコミット。
d-watch-api.yaml(★先に直す。CLAUDE.md §6-1★)/dashboard の description(:218-220)を書き直す:
date を省略すると ★summary と problem_logs は「各清掃エリアの直近の清掃(7日以内・終わった予定のみ・出発失敗を含む)」★。D-159date の説明(:225): 「対象日。指定するとその日の清掃で summary / problem_logs を作る。省略(空文字を含む)すると直近モード」summary に説明を足す(母集団が date の有無で変わる)rescue_required の説明の「★上の『コンパクト版清掃詳細ログは本APIに含めない』には抵触しない★」(:313-316)を、新しい説明に合わせて直す(その前提の文が無くなるため)rescue_required の後(:357 の後)に problem_logs を足す:
type: array、items: { $ref: "#/components/schemas/CleanLogItem" }consecutive_problem_days_capped)lib/types/index.ts の DashboardResponse(:1146)problem_logs: CleanLogItem[] を足す(★optional にしない★。0件は「問題の行が無い」という事実)summary の docstring に母集団(直近モード/date 指定)を書く。schedule_progress は「当日」のままlib/clean-logs/query.ts):180 の !["△", "×"].includes(r.grade) を ★export function isProblemGrade(grade: Grade): boolean★ に切り出し、filterCleanLogs() はそれを呼ぶ(★動作は変えない★)critical(:183)は触らない★app/api/dashboard/route.ts:12-13★(「コンパクト版清掃詳細ログはこのAPIに含めません」)を D7 に合わせて書き直す:48-55: ★const now = new Date() を1回だけ★ 作り、today = toJstIso(now).slice(0, 10)、const latestMode = !dateRaw、const date = dateRaw || today。 ★|| の理由のコメント(:49-54)は残し、「?date= は直近モードになる」に直す★: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):115-147):
logsOfDay は ★今までどおり today(= date)の行★。computeScheduleProgress(logsOfDay, …) と data.anomaly は ★変えない★const summaryRows = latestMode ? picked : logsOfDaysummary = computeSummary(summaryRows)problem_logs = capCleanLogs(summaryRows.filter((r) => isProblemGrade(r.grade)), maxDays):66-72 / :85-89 のコメントの「当日」「4つすべてに同じ配列」を、cleanLogRunMeta() を含めて直す(★同じ facilityIds を5本に渡す★)。:82 の「約360行」には「直近モードは8日ぶん」を足す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)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つの顧客を足すと絞らないときと一致)は ★そのまま★ 通ることlib/clean-logs/__tests__/query.test.ts(既存の filterCleanLogs() のテスト)が ★期待値を変えずに★ 通ること(4-3)YAML → 型 → ルート → テストの順でコミット(1つでも分けてもよい)。
components/features/dashboard/DashboardScreen.tsx/api/clean-logs の取得をやめる★(:75-86)。apiFetch<DashboardResponse>("/api/dashboard") だけにし、表には data.problem_logs を使う。
logs の state(:52)・setLogs(:88 / :97)・ListResponse の import は不要になれば消す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本になったので直す: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)が使われなくなれば消す:201-207 のコメント(「当日の清掃ログ行数」)を ★「直近モードでは、7日以内に終わった予定がある清掃エリアの数」★ に直す(sub={\/ ${totalLogsToday}件`}の変数名はtotalLogs` 等に変えてよい):260-273 の要レスキューのコメントの「★数値カード(上)とは母集団が違う★」に「数値カードは直近モード、要レスキューは当日基準」を足すlib/i18n.ts"直近の清掃結果サマリー(各清掃エリアの最後の清掃・{n}日以内)": "Latest cleaning results (last run per area, within {n} days)" を足す(★EN は暫定。ユーザーが後でまとめて見直す★)。★キーの全角括弧・中点を原文と揃える★。重複は lint が落とす。
components/features/detail-log/CleanLogTable.tsx):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)」を足すdateLabel(DashboardScreen.tsx:163)は既に formatMd(row.scheduled_at) なので変えないvitest.config.ts の include は lib/** と app/**)ので段階7 の画面確認で見る★背景★: ダッシュボードのカードと「全項目 →」は /detail-log に移るが、画面は開いたときに date_from = date_to = 今日(todayJst())で API を呼ぶ(components/features/detail-log/DetailLogScreen.tsx の useState(today) 2つ。2026-10-06 時点で :60-61 付近)。 ★昨夜 19:00 の出発失敗はダッシュボードには出るのに、移った先には出ない★。
dateFrom / dateTo の初期値を空文字にし、★空のときは date_from / date_to を URL に付けない★(API は期間なし=全期間を返す。lib/clean-logs/query.ts の既存の挙動。★API は変えない★)。MAX_DATE_RANGE_DAYS、開始 ≦ 終了)は★両方入っているときだけ★行う。既存の検証メッセージ(DetailLogScreen.tsx :167 / :175 付近)の条件をそれに合わせる。gradeFilter = "problem")は変えない★。date_from / date_to が付かないこと、片方だけ入れたとき検証エラーにならず送らない(または両方そろうまで送らない — ★既存の挙動に近いほうを採り、理由をコミットに書く★)、両方入れたときは従来どおり。lib/db/index.ts の cleanLogs()。指示書AC で日付を必須にして避けていた形)。ステージングでは100行程度で問題ないが、本番の想定(約26万行)では重くなる。★本書では対策しない★(リリース前に見直す)。★ログ(db.query 系の所要時間)で観測できることだけ確かめる★。docs/fixtures.md §12(:363-):
:371-372 の「date 省略時は当日=daysBack=0」に「★schedule_progress は省略時も当日。summary と problem_logs は直近モード(D-159)★」を足す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 の画面確認で違えば、どちらが正しいかを報告★)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)。
npx tsc --noEmit -p . — エラー0件npm run lint — 指摘0件(i18n の重複キー・$queryRaw・any・未使用の import が無いこと)npm test — ★全件通過★(表1〜表3 を含む)USE_FIXTURES=true で npm run build★ — 成功(PowerShell なら $env:USE_FIXTURES='true'; npm run build)USE_FIXTURES=true)★: .claude/launch.json の ★d-watch-fixtures(ポート 3100)★ で起動する(Claude Code なら preview_start の d-watch-fixtures)。 ★確かめ終わったら、自分で起動した dev サーバーは必ず止める★
docs/fixtures.md §12.1 の「当日」の行: 予定10・実施9)M/D HH:mm。行を開くとアドバイスが出る・エリア別詳細のモーダルが開く(id は今までと同じ)/api/clean-logs が ★呼ばれない★こと(/api/dashboard の1本だけ)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__
| ファイル | 内容 | 段階 |
|---|---|---|
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() は変えない) |
— |
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 追記)。既定は「期間を指定しない(全期間)」に変えた★status / comment / responded_at / resolved_at)scripts/seed-local-db.ts の接続先とガード(lib/db/local-guard.ts)★本文は暫定のまま正しい(直す箇所は無い)★。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 |
段階ごとに分ける(段階1 / 段階2 / 段階3 / 段階4 / 段階5 / 段階6)。★git add はファイルパスを明示する★(git add -A 禁止)。 メッセージには ★何を変えたか★ と ★なぜ(D-159。ダッシュボードを各清掃エリアの直近の清掃にする)★ を書く。 ★段階1 のコミットは「テストだけ(この時点では落ちる)」と明記する★。 ★PR の作成とマージは daichi がレビューしてから行う★(D-154)。
★実装は 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。
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("…") の二重引用符が落ちている(中身に影響なし。直していない)。
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件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 なしで取得し、前日の行も出る/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 昇順)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)lib/clean-logs/detail-log-date-range.ts★(新規・純粋関数。appendDateRangeParams() / dateRangeError()。テスト20件)<input type="date"> の placeholder は多くのブラウザで出ないので、★両方空のあいだ入力欄の前にも「指定なし(全期間)」を出している★(§段階5b-5 の例より一歩踏み込んだ)npm run build が落ちる★("Symlink [project]/node_modules is invalid")。 ビルドは本体で行った(dev サーバーは .next/dev を使うので :3000 を動かしたままで可)