Newer
Older
client_dwatch / docs / handoff / 指示書AV_ダッシュボードを直近の清掃にする_20261006.md

指示書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)の ★直後★ に足す:

/**
 * ダッシュボード(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 に書く:

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(新規)— ★純粋関数のみ★

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() の直後に足す:

/**
 * ★清掃ログの 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() の直後)

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() を足す:
    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 を動かしたままで可)