Newer
Older
client_dwatch / docs / handoff / 指示書AW_エラー一覧ページを作る_20261006.md

指示書AW — エラー一覧ページ(/errors)を作る(robot_error_reports の未対応エラーを一覧し、対応状況と備考を更新する)

★前提: 指示書AV(段階5b を含む)は実装済み(HEAD 79485ce 時点)。★ (AV の実装コミットは 1501173〜939a2c5、段階5b(清掃詳細ログの既定を全期間に)は 137a94b。本書はその上に足す)

日付: 2026-10-06 / 担当: Cursor(★実装は Claude Code(2026-10-06。ユーザー指示「ここで実装する」)。§13★)/ 基点: develop 79485ce 前提: ★D-160(本書の根拠。docs/decisions.md:12612-12622)★ / D-152(ステージングへの4カラム書き込みを解禁。:12325-12356)/ D-156 項3・4(AT の @updatedAt 例外・書き込みの規則。:12475-12489)/ D-158 P-8〜P-10(エラーの3区分・robot_error_reports を正とする。:12523-12581)/ D-105(PrecheckItem.id は string・error_id を Number() に通さない。:5442 以降)/ 指示書AT(4カラム UPDATE)/ 指示書AU 段階0-4(robot_error_reports は select で読む)・段階3b(予定は2表から)/ CLAUDE.md §3・§4.8・§6・§7・§9.3・§9.4・§9.6

★期待値表(§3 の表1)は監督が確定した正。変えないこと★(CLAUDE.md §0-2・§6-2・§9.4)。 ★表2・表3 は本書で表1 と既存のコードから導いたもの★。★監督が確認し正とした(2026-10-06。§11 Q14)★。 ★§11 の質問に回答が来て本書の暫定が変わったら、§11 の各行に書いた「直す場所」を本文と同時に直すこと★ (本文と末尾の決定が食い違ったまま渡さない)。

★行番号はすべて 2026-10-06 の develop 79485ce(AV 実装後)で確かめたもの★ (f424689 以降のコミットは資料だけで、app/ lib/ components/ d-watch-api.yaml prisma/ に差分が無いことを git diff で確かめた)。 ★着手時に develop が進んでいたら、関数名・文で探し直すこと★(特に lib/db/index.ts・lib/types/index.ts・d-watch-api.yaml・lib/i18n.ts)。


0. 何をするか

# やること 段階
1 新しいページ ★/errors「エラー一覧」★(EN 暫定 "Errors")をメニューの「モニタリング」の ★「アラート」の直後★ に足す 段階5
2 ★robot_error_reports の全行のうち、状態が「未解消」「対応中」のもの★を一覧する。★「解消済みも表示」★ で解消も出す 段階2〜5
3 一覧から ★対応状況(未解消 → 対応中 → 解消 → 未解消)と備考★ を更新できる。★書くのは4カラムだけ★(指示書AT の書き込みを、事前チェックの行だけから ★全行★ に広げる) 段階3〜5
4 ★エラーの種類(error_type)・レベル(error_level)・シリアルは生値のまま★出す(区分を作らない。D-077 / D-160 項3) 段階2・5

★変えないもの★(D-160 末尾): ★アラート画面(/alerts。事前チェック+出発失敗)の挙動★/★PATCH /prechecks/{id}/status の挙動(事前チェックの行だけ・他は404)★/ ★D-Sec アラート画面とその取得元(control_error_histories。次フェーズで切り替え)★/★AT の @updatedAt 例外とガードテスト★。


1. 背景

  • ステージングに各店舗のエラー16件(ロボット・制御盤・セキュリティ・清掃開始失敗)が入ったが、★アラート画面は「事前チェックの異常」と「出発失敗」だけを出す作り★なので、 ★本社(事前チェックの予定がある唯一の店舗)しか出なかった★(D-160。2026-10-06 実測・先方投入: 16件すべて本清掃の予定に紐づく)。
    • /alerts が拾うのは ★事前チェック行(precheck ≠ 0)に紐づくエラー行だけ★(lib/db/index.ts:1366 の clean_timetable_id: { in: 事前チェック行の id }、:1393-1405)。 ★本清掃行に紐づくエラー・予定 ID が NULL のエラーは出ない★(:1387-1388 / :1402-1405。D-104)
  • robot_error_reports は ★エラー全般の表★ になった(第11回 P-8〜P-10。D-158): 制御盤エラー=robot_id NULL・serial_number が制御盤の SN/★セキュリティエラーと「清掃を開始できなかった」=robot_id NULL・serial_number = '0'★(error_type で見分ける)。 ★先方の DDL では robot_id が NULL 可、prisma/schema.prisma:318 は NOT NULL のまま★(db pull 待ち)→ ★robot_id / robot を読むと落ちる★(指示書AU 段階0-4。lib/db/robot-error-select.ts:4-12)
  • ユーザー回答(2026-10-06。D-160): 未解消・対応中の全行を一覧/解消は切り替えで表示/このページから対応状況と備考を更新/種類は生値のまま。

2. 決定(Cursor は従うこと)

# 決定 理由
D1 ★対象は robot_error_reports の全行★(error_type を問わない。事前チェック・本清掃・予定 ID が NULL・robot_id が NULL の行すべて)。★既定は status が 未解消(0)・対応中(1)★、include_resolved=true で 解消(2) を足す D-160 項1
D2 ★状態コード ↔ enum は既存の lib/db/precheck-mapping.ts の2関数だけ★(resolutionStatusFromRobotErrorStatus() :33 / robotErrorStatusFromResolutionStatus() :53)。★0/1/2 を直書きしない★ — 絞り込みのコードも ["unresolved", "in_progress"].map(robotErrorStatusFromResolutionStatus) で作る 変換を1箇所に(AT §1 #7)
D3 ★検知時刻 = errored_at ?? created_at★。★既存の detectedAtFromError()(lib/db/precheck-mapping.ts:171)を使い、jstIsoFromDb() に通す★ /alerts と同じ規則(D-105。指示書S 3-4)
D4 ★紐づく予定の4区分★: clean_timetable_id が NULL →「予定なし」(none)/★本表にも履歴にも無い id★ →「予定不明」(unknown)/precheckMinutesFromCode(precheck) が null →「本清掃」(main)/それ以外 →「事前チェック(N分前)」(precheck。N は ★生の分数★) D-160 / 監督の既定。分数は D-105 項1 と同じく生値(設定の 60/30 に無い値でも出す)
D5 ★予定は2表から引く★: 一覧は findTimetablesByChunks()(lib/db/timetable-source.ts:255)、1件は findTimetableById()(:285)。★同じ id は本表が勝つ★ 指示書AU 段階3b。表1 E6
D6 ★robot_error_reports の読み取りは select で列を絞る。robot_id / robot を読まない★。新しい select は lib/db/robot-error-select.ts に定数で置き、lib/db/__tests__/robot-error-select.test.ts:18-23 の一覧に足す 指示書AU 段階0-4(NULL の行で落ちる)
D7 ★並びは検知時刻の降順、同時刻は error_id の昇順(文字列の辞書順)★ 監督の既定。sortPrechecks() と同じ(D-105 項2 #4。error_id が数値である保証が無い)
D8 ★SQL に降ろすのは status IN・facility_id IN・検知時刻の期間★。★並べ替えとページ分けは「キーだけの一覧」をアプリで行う★(2段階で読む。下の「D8 の理由」) ★errored_at ?? created_at の順は Prisma のクエリビルダで書けない★(COALESCE の orderBy が無い。生SQL は禁止)。★§11 Q1★
D9 ★種類(error_type)の絞り込みは部分一致・大小文字を区別しない★(includesFoldPlain()。lib/format/search-text.ts:190)。★SQL の LIKE にしない(アプリ側で行う)★ 機種・シリアルと同じ照合(指示書AG D-2)。LIKE は % _ のエスケープと DB の照合順序で fixtures と結果が割れる。★§11 Q4★
D10 ★施設名の絞り込みは既存の resolveFacilityIdsByName()(lib/facilities/resolve-by-name.ts:78)★、分類は resolveFacilityIdsByMasters()(lib/facilities/resolve-by-master.ts:88)、★2本は intersectFacilityIds()(lib/facilities/intersect-ids.ts:45)で AND★ /clean-logs と同じ(app/api/clean-logs/route.ts の取得部分)。★null = 絞らない / [] = 0件★(罠1)
D11 ★更新は新しい PATCH /errors/{error_id}/status★。★robot_error_reports に行があれば書ける(無ければ404)★。★規則は AT のまま★: パッチは planPrecheckStatusUpdate()(lib/prechecks/status-update.ts:50)、本文の検証は parsePrecheckStatusUpdateBody()(:100)、書き込みは writeRobotErrorColumns()(lib/db/robot-error-write.ts:121)を ★そのまま使う★。★改名しない★(§11 Q7) D-160 項2。★同じ列を2つの規則で書かない★
D12 ★PATCH /prechecks/{id}/status は変えない★(事前チェックの行だけ・他は404)。★ルートの共通化はしない★(新しいルートを並べて置く) D-160「変えないもの」。AT のルートテストを巻き込まない
D13 ★fixtures は「DB の行の形」で持つ★(日時は toDbJst() を通した壁時計値の Date、id は bigint)。★fixtures と Prisma は同じ純粋関数(行 → API の値・絞り込み・並べ替え)を通す★ CLAUDE.md §9.5(判定ロジックは切替の外側)。USE_FIXTURES で件数が変わらない(指示書AC 禁止事項6)
D14 ★fixtures の書き込みは新しい上書き表★(lib/db/fixture-error-report-overrides.ts)。★パッチは robotErrorColumnsFromPatch()(lib/db/robot-error-write.ts:70)で DB の列の値に写してから上書きする★ Prisma 版と同じ歯止め・同じ値(toDbJst() 済みの日時)。★AT の fixture-precheck-overrides.ts は触らない★(§11 Q8)
D15 ★メニューは常に出す★(/alerts と同じ)。★AppShell.tsx には契約(facilities.contract)でメニューを出し分ける仕組みが無い★(components/layout/AppShell.tsx:8-34・:126-158。全メニューを無条件に描く)。★本書でも作らない★ 監督の既定「/alerts と同じ規則」。実測した規則は「無条件」。★§11 Q2★
D16 ★列の見出しは「施設名」★(監督の依頼文は「店舗」) CLAUDE.md §8「施設(旧称店舗)」。★既存キー 店舗 は EN が "Stores"(複数形。lib/i18n.ts:390 のレポートの粒度タブ)で列見出しに合わない★。★§11 Q5★
D17 ★対応状況の巡回順(未解消 → 対応中 → 解消 → 未解消)は純粋関数1本★: AlertsScreen.tsx:80-89 の nextResolutionStatus() を lib/prechecks/status-cycle.ts(新規)へ移し、★/alerts と /errors の両方がそれを import する★ ★2画面で巡回順が食い違わないため★。/alerts の挙動は変わらない(関数を移すだけ)。★§11 Q10★
D18 ★レスポンスに clean_timetable_id を載せる(画面には出さない)★ 「予定不明」の行を調べるときに要る。数値(Number())で返す — clean_timetables.id は BigInt(PrecheckItem の facility_id と同じ作法)。★§11 Q9★

D8 の理由(「キーだけの一覧」をアプリで並べる)

  • 並びの基準は ★errored_at ?? created_at★(D3)。Prisma の orderBy は ★列ごとの並べ替えしか書けず COALESCE を書けない★。 [{ errored_at: { sort: "desc", nulls: "last" } }, { created_at: "desc" }] にすると ★errored_at が NULL の行(表1 E7)は検知時刻に関係なく最後に回る★ — 表1 の並びと食い違う(例外もテスト失敗も出ない)。
  • ★生SQL($queryRaw)は禁止★(CLAUDE.md §3。ESLint でも禁止。D-122)。
  • そこで ★2段階で読む★:
    1. ★キーだけ★(error_id / clean_timetable_id / facility_id / error_type / errored_at / created_at / status)を、★SQL で status IN・facility_id IN・期間を絞って★ 全件読む
    2. アプリで ★絞り込みの「正」(純粋関数)→ 並べ替え → ページ分け★ をして、★そのページの error_id だけ★ 列を足して読み直す(最大 PAGINATION.maxPerPage = 200件。lib/config/index.ts:421-424)
  • ★既存の一覧も take / skip を SQL に降ろしていない★(lib/types/index.ts:588 付近「page / per_page … ★take / skip は SQL に降ろさない★(指示書AC D-3)」)。本書の形はそれより軽い(1段目はキーだけ)。
  • ★性能の見積もり★: 既定(未解消+対応中)は対応待ちの件数しか読まない。★「解消済みも表示」は保持期間ぶん全部のキーを読む★(ステージングは 188行。D-158 2026-10-06 実測・先方投入)。★本番の行数は不明★ → §10 の範囲外・リリース前に見直す。

3. ★期待値表(正)★

表1(★監督確定・正・変えない★)

# 行 既定(解消を含めない)で出るか 「解消済みも表示」で出るか 紐づく予定の表示 検知時刻
E1 status 0・本清掃の予定に紐づく 出る 出る 本清掃 errored_at
E2 status 1・事前チェック(precheck=30)に紐づく 出る 出る 事前チェック(30分前) errored_at
E3 status 2・本清掃 出ない 出る 本清掃 errored_at
E4 status 0・clean_timetable_id NULL 出る 出る 予定なし errored_at
E5 status 0・clean_timetable_id が両方の予定表に無い id 出る 出る 予定不明 errored_at
E6 status 0・予定が clean_timetable_histories にだけある 出る 出る 本清掃(履歴の予定も引く) errored_at
E7 status 0・errored_at NULL 出る 出る (予定どおり) created_at
E8 status 0・robot_id NULL・serial_number '0'・error_type SECURITY_ON 出る 出る (予定どおり) errored_at。シリアル欄は「0」、種類は「SECURITY_ON」のまま
E9 status 0・precheck=10 の予定に紐づく 出る 出る 事前チェック(10分前) errored_at(設定の 60/30 に無い値でも生の分数で出す)

並び順: 検知時刻の降順、同時刻は error_id の昇順。

PATCH: AT §3-4 の状態遷移と日時の規則(12行の表)をそのまま適用する。★事前チェック以外の行(E1・E4・E5・E8)にも書ける★こと、★存在しない error_id は404★、★書く列は4列以内(ガードテスト)★。

★テストの書き方の注意(表1 の値を変えずに、テストを確かにするための指定)★:

  • ★E7 の created_at は9行の中で一番新しく、E7 の errored_at 以外の値より後★にする(例: 他は 10/5 19:00〜22:00、E7 は created_at = 10/6 08:00)。 → ★E7 が先頭に来ること★を確かめる。nulls: "last" の並べ替え(D8 の理由)だと最後に来るので、取り違えを検出できる
  • ★2行を同じ検知時刻にして error_id の昇順★を確かめる(例: E4 = "E-10"・E5 = "E-9" を同時刻にし、★"E-10" が先★ — 辞書順。数値順なら逆になる)
  • E2 の 30 は ★PRECHECK_MINUTES_BEFORE[1](lib/config/index.ts:244)から読む★。★E9 の 10 は設定に無い値であることが本題なので直書きする★(lib/fixtures/prechecks.ts の 20 と同じ扱い。コメントで理由を書く)
  • E6 は ★履歴の表(cleanTimetableHistory)にだけ★ 予定を置く。★本表の findMany が E6 の id を返さない★ことをスタブで確かめる
  • E8 の行は ★robot_id: null★ を持たせ、guardNullRobotId()(lib/db/__tests__/robot-error-null-guard.ts)をスタブに入れる(robot_id を読むと落ちる)
  • E7・E8 の「(予定どおり)」は ★テストを書く人が予定を決めてよい★(例: 両方とも本清掃)
  • 日時は ★+09:00 付きの ISO 文字列から toDbJst(new Date(...))★ で DB の壁時計値を作る(スタブが返す Date は DB と同じ形にする)

表2(一覧の絞り込み・ページ。★本書で導いたもの。監督確認済み・正★)

入力は表1 の9行(facility_id は E1〜E5 = 1、E6〜E9 = 2 とする)。

# 入力 期待 確かめること
F1 既定(パラメータなし) E3 以外の8行・meta.total = 8 D1
F2 include_resolved=true 9行 D1
F3 include_resolved=yes(true / false 以外) 422 入力の検証(黙って false に倒さない)
F4 error_type=secur(大小文字違い・部分) E8 だけ D9
F5 error_type=(空)/空白だけ F1 と同じ 空は「絞らない」
F6 date_from = date_to = E7 の created_at の日付 E7 が入る(errored_at が NULL でも created_at の日付で当たる) D3。★SQL 側も errored_at が NULL の行を created_at で絞る★
F7 date_from > date_to 422 parseCleanLogQuery() と同じ文言(lib/clean-logs/query.ts の「date_fromはdate_to以前の日付を指定してください」)
F8 分類で絞って施設が0件(facility_ids = []) 0件(★全件にならない★) 罠1
F9 施設名が当たり分類も当たるが積集合が空 0件 D10(AND)
F10 per_page=3&page=2(既定の8行) 並び順の4〜6行目・meta.total = 8 D8
F11 page=99 items: []・meta.total = 8 404 にしない(一覧の作法)
F12 status = 3 の行が混ざる ★既定でも「解消済みも表示」でも出ない・例外にならない★ SQL の status IN で外れる。純粋関数も生の数値で比べるので resolutionStatusFromRobotErrorStatus() の例外は起きない。★§11 Q3★

表3(ラベル errorScheduleLabel()。★本書で導いたもの。監督確認済み・正★)

# schedule_link / precheck_minutes JP EN(暫定)
S1 main / null 本清掃 Main run
S2 precheck / 30 事前チェック(30分前) Pre-check (30 min before)
S3 precheck / 10 事前チェック(10分前) Pre-check (10 min before)
S4 none / null 予定なし No schedule
S5 unknown / null 予定不明 Unknown schedule
S6 precheck / ★null★(API 契約では起きない) 事前チェック Pre-check

S6 は precheckSeverityLabel()(lib/prechecks/label.ts)と同じ考え方 — ★黙って「null分前」を出さない★。


4. ★落とし穴(全段階で読むこと)★

# 罠 どう避けるか
4-1 ★robot_error_reports を select なし・include で読む/robot_id・robot を選ぶ★ ★robot_id が NULL の行(E8・NOT_STARTED・制御盤)で読み取りが落ちる★(指示書AU 段階0-4)。★robot-error-select.ts の定数だけを使う★。findUnique も同じ
4-2 ★facility_ids の null と [] を混同する★ !== null で分岐する(罠1。lib/db/index.ts:1320-1328 のコメントと同じ)。★?.length で書かない★
4-3 ★期間の条件を errored_at だけにかける★ ★errored_at が NULL の行は created_at で絞る★(OR: [{ errored_at: 範囲 }, { errored_at: null, created_at: 範囲 }])。片方だけだと E7 が期間指定で消える(表2 F6)
4-4 ★期間の Date を自分で作る★ ★dbDateRange()(lib/db/date-range.ts:50)の1箇所★。DB の値は JST の壁時計値(CLAUDE.md §3)。★9時間を足さない・引かない★
4-5 ★errored_at ?? created_at を SQL の orderBy で近似する★ ★しない★(D8 の理由)。並べ替えは純粋関数1本
4-6 ★take / skip を findTimetables*() に渡す★ 型で禁止されている(lib/db/timetable-source.ts:57-70)。予定は id で引くだけ
4-7 ★error_id を Number() に通す★ ★文字列のまま★(varchar(50)。D-105 項2)。React の key も文字列のまま
4-8 ★findUnique({ error_id }) の結果をそのまま採る★ ★DB の照合順序は大小文字を区別しない(ci)★。rer-1 で RER-1 が当たる → ★report.error_id !== id なら null(404)★(precheckItemById() lib/db/index.ts:1517-1521 と同じ。D-4)
4-9 ★clean_timetables.id の照合を bigint の === で書く★ bigint は === で比べられるが Map のキーにはならない → ★.toString() をキーにする★(lib/db/index.ts:1331 の key() と同じ)
4-10 ★予定を本表だけから引く★ ★E6 が「予定不明」になる★。findTimetablesByChunks() / findTimetableById() を使う(D5)
4-11 ★状態コードを直書きする(status: { in: [0, 1] })★ D2。robotErrorStatusFromResolutionStatus() から作る
4-12 ★planPrecheckStatusUpdate() を通さずにパッチを作る/本文を data に渡す★ ★パッチを作るのは planPrecheckStatusUpdate() だけ★(AT §3-6)。本文の updated_at・error_type が DB に届く経路を作らない
4-13 ★update(1件更新)を使う★ ★writeRobotErrorColumns()(updateMany の1文)だけ★(AT §3-2)。★書き込み用の接続(getWritePrisma())で書く★
4-14 ★prisma/schema.prisma の RobotErrorReport.updated_at に @updatedAt を付ける★ ★付けない★(AT §3-1・D-156 項3)。lib/db/__tests__/robot-error-write-sql.test.ts が見張っている
4-15 ★1段目と2段目の間に行が消える★(ステージングのデータ入れ替え。第10回 N-6) 2段目で見つからない error_id は ★飛ばす★(★例外にしない★ — 一覧全体が500になる)。meta.total は1段目の件数のまま(1件ずれうる。承知のうえ)。★コメントで理由を書く★
4-16 ★/alerts の挙動を変える★ AlertsScreen.tsx で変えてよいのは ★nextResolutionStatus() を import に置き換えることだけ★(D17)。★prechecks() / precheckItemById() / /prechecks のルートは触らない★
4-17 ★JP/EN の切替で列幅や位置が動く★ ★table-layout: fixed + <colgroup>★(components/features/detail-log/CleanLogTable.module.css:45・CleanLogTable.tsx:80)。★状態ボタンは固定幅★(「対応中」と "In progress" で幅が違う)。ツールバーの入力欄も固定幅
4-18 ★画面の文字列を t() に通さない/キーの全角括弧がずれる★ 新しい文言はすべて t()(lib/i18n.ts)。★{n} は replace★("{n}分前" の前例)。★既存キーの EN と衝突しないか確かめる★("{n}件" は "{n} facilities"(lib/i18n.ts:274)なので件数表示に使えない。店舗 は "Stores")
4-19 ★生SQL($queryRaw / $executeRaw)・eslint-disable を足す★ ★禁止★(CLAUDE.md §3)
4-20 ★git add -A★ ★禁止★(CLAUDE.md §7)。ファイルパスを明示する。★作業ツリーに自分の作業でないファイルがあっても含めない★

段階0 — 着手前

  1. ★develop で作業する★(.claude/worktrees/ の中ではない)。git pull で最新にし、★AV の実装(段階5b を含む)が入っていること★を git log で確かめる(79485ce 以降・lib/dashboard/latest-run.ts と lib/clean-logs/detail-log-date-range.ts があること)。
  2. ★作業ツリーに自分の作業でない変更があっても触らない・コミットに含めない★(git status で控える)。
  3. npx prisma generate → npx tsc --noEmit -p . → npm test が通ることを確かめ、★テストの件数を記録する★(以後の増分の基準)。通らなければ着手前に報告。
  4. ★Next.js は版が新しい★。ルートを書く前に node_modules/next/dist/docs/ の Route Handler の項を読む(CLAUDE.md 末尾)。★動的セグメントの params は Promise★(app/api/prechecks/[id]/status/route.ts:88-95 に合わせる)。
  5. 次の grep で ★robot_error_reports の読み取りがすべて定数の select を使っている★ことを確かめる(本書で足す読み取りも同じにする):
    grep -rn "robotErrorReport\." lib app --include=*.ts | grep -v __tests__

段階1 — 期待値表をテストにする(実装より先)

★この時点ではテストが落ちる(関数・型が無い)のが正しい★。コミットはテストだけで1つ(「テストだけ。この時点では落ちる」と明記)。

  1. ★lib/db/__tests__/error-report-mapping.test.ts(新規)★:
    • 冒頭コメントに表1・表3 を写し、「★表1 は監督確定(D-160 / 指示書AW §3)。変えないこと★」「表2・表3 は指示書AW で導いたもの」と書く
    • errorReportItemFromRow(): E1〜E9 の各行 → schedule_link / precheck_minutes / detected_at / serial_number / error_type / error_level が表1 どおり(it.each)
    • errorScheduleLink(): NULL → none/予定が undefined → unknown/本清掃 → main/PRECHECK_MINUTES_BEFORE[1] → precheck・その分数/10 → precheck・10/★予定の id と clean_timetable_id が違う → 例外★(呼び出しの誤り)
    • errorReportKeyFromRow(): detected_at が E7 で created_at、それ以外で errored_at(+09:00 の ISO。★壁時計値のまま★ — toDbJst(new Date("2026-10-05T19:05:00+09:00")) → "2026-10-05T19:05:00+09:00")
    • ★status に 3 が入った行でも errorReportKeyFromRow() は例外にならない★(キーは生の数値を持つ。表2 F12)
  2. ★lib/error-reports/__tests__/query.test.ts(新規)★:
    • filterSortErrorReportKeys(): 表2 F1・F2・F4・F5・F6・F8・F12 と、★表1 の並び順★(E7 が先頭・同時刻は "E-10" が "E-9" より先)
    • errorReportStatusCodes(false) = 未解消・対応中のコード、(true) = それに解消を足したもの(★期待値も robotErrorStatusFromResolutionStatus() から作る★。0/1/2 を直書きしない)
    • parseErrorReportQuery(): F3(422)・F7(422)・日付の形式(422)・group_id=0(422。parseMasterIdFilters() に任せる)・page / per_page の丸め(parsePrecheckQuery() と同じ)・error_type / facility_name は前後の空白を除く
  3. ★lib/error-reports/__tests__/label.test.ts(新規)★: 表3 S1〜S6(t は恒等関数と EN 辞書の2通り)
  4. ★lib/prechecks/__tests__/status-cycle.test.ts(新規)★: 未解消 → 対応中 → 解消 → 未解消(3件)
  5. lib/db/__tests__/robot-error-select.test.ts: SELECTS(:18-23)に段階2-3 の2つの定数を足す(★robot_id / robot を含まない・clean_timetable_id を含む★がそのまま効く)

段階2 — 型・純粋関数

2-1. lib/types/index.ts

PrecheckDbQuery(:592 付近)の後に足す。★YAML のスキーマ名と揃える★(CLAUDE.md §9.2):

/** エラー一覧の「紐づく予定」(指示書AW D4)。`d-watch-api.yaml` の `ErrorReportItem.schedule_link` */
export type ErrorScheduleLink = "main" | "precheck" | "none" | "unknown";

/** `GET /errors` の1行(`robot_error_reports` の1行。指示書AW)。`d-watch-api.yaml` の `ErrorReportItem` */
export interface ErrorReportItem {
  /** `robot_error_reports.error_id`(★string のまま。`Number()` を通さない。D-105 項2★) */
  id: string;
  /** `errored_at ?? created_at`(`detectedAtFromError()`)。ISO8601(+09:00) */
  detected_at: string;
  facility_id: number;
  facility_name: string;
  /** ★生値★(`'0'` もそのまま。D-077) */
  serial_number: string;
  /** ★生値★(区分を作らない。D-077 / D-160 項3) */
  error_type: string;
  /** ★生値★ */
  error_level: string;
  /** ★画面には出さない★(「予定不明」の調査用。D18) */
  clean_timetable_id: number | null;
  schedule_link: ErrorScheduleLink;
  /** `schedule_link === "precheck"` のときだけ数値(★生の分数★)。それ以外は null */
  precheck_minutes: number | null;
  /** ★`PrecheckStatus` を使い回す★(名前は歴史的なもの。§11 Q7) */
  resolution_status: PrecheckStatus;
  responded_at: string | null;
  resolved_at: string | null;
  note: string | null;
}

/** `DataSource.errorReports()` に渡す取得条件(指示書AW)。★引数は必須★ */
export interface ErrorReportDbQuery {
  /** false = 未解消・対応中だけ / true = 解消も含める */
  include_resolved: boolean;
  /** ★`null` = 施設で絞らない / `[]` = 0件★(罠1) */
  facility_ids: number[] | null;
  /** JST の `YYYY-MM-DD`(検知時刻の日付)。★下限なしは `null`★ */
  date_from: string | null;
  /** JST の `YYYY-MM-DD`(★その日を含む★)。★上限なしは `null`★ */
  date_to: string | null;
  /** 種類の部分一致(大小文字を区別しない)。★`""` = 絞らない★ */
  error_type: string;
  page: number;
  per_page: number;
}

★PrecheckStatusUpdateRequest / PrecheckStatusPatch(:542 / :556)は ★そのまま使う★(新しい型を作らない。D11)。

2-2. lib/prechecks/status-cycle.ts(新規)

components/features/alerts/AlertsScreen.tsx:76-89 の nextResolutionStatus() を ★docstring ごと★ 移し、export する。★中身は1文字も変えない★。

2-3. lib/db/robot-error-select.ts — 定数を2つ足す(:51-64 の後)

/**
 * エラー一覧の1段目(キーだけ。指示書AW D8)。絞り込み・並べ替えに要る列だけ。
 * ★`clean_timetable_id` は並べ替えには使わないが、select の共通テスト(紐づけのキーを含む)のために読む★
 */
export const ROBOT_ERROR_LIST_KEY_SELECT = {
  error_id: true,
  clean_timetable_id: true,
  facility_id: true,
  error_type: true,
  errored_at: true,
  created_at: true,
  status: true,
} as const satisfies Prisma.RobotErrorReportSelect;

/**
 * エラー一覧の2段目・PATCH の前後の読み取り(`errorReportItemFromRow()`)に要る列。
 * ★`errorReports()` と `errorReportById()` が同じ定数を使う★(行の作り方を1本にする。AT 段階4-1 と同じ理由)
 */
export const ROBOT_ERROR_LIST_ROW_SELECT = {
  error_id: true,
  clean_timetable_id: true,
  serial_number: true,
  facility_id: true,
  facility: { select: { facility_name: true } },
  error_type: true,
  error_level: true,
  errored_at: true,
  created_at: true,
  status: true,
  responded_at: true,
  resolved_at: true,
  comment: true,
} as const satisfies Prisma.RobotErrorReportSelect;

★robot_id / robot / section を足さない★(4-1)。

2-4. lib/db/error-report-mapping.ts(新規)— ★純粋関数のみ★(lib/db/precheck-mapping.ts と同じ作法)

/** 1段目の行(`ROBOT_ERROR_LIST_KEY_SELECT` の結果がそのまま入る形) */
export interface ErrorReportKeySource extends PrecheckDetectedAtSource {
  error_id: string;
  facility_id: bigint;
  error_type: string;
  status: number;
}
/** 絞り込み・並べ替えに使うキー(★`status` は生の数値★。表2 F12) */
export interface ErrorReportKey {
  error_id: string;
  facility_id: number;
  error_type: string;
  /** `jstIsoFromDb(detectedAtFromError(row))` */
  detected_at: string;
  status: number;
}
/** 2段目の行(`ROBOT_ERROR_LIST_ROW_SELECT` の結果がそのまま入る形) */
export interface ErrorReportRowSource extends PrecheckDetectedAtSource {
  error_id: string;
  clean_timetable_id: bigint | null;
  serial_number: string;
  facility_id: bigint;
  facility: { facility_name: string };
  error_type: string;
  error_level: string;
  status: number;
  responded_at: Date | null;
  resolved_at: Date | null;
  comment: string | null;
}
/** 予定(本表・履歴のどちらか。`findTimetables*()` の `select: { id: true, precheck: true }`) */
export interface ErrorTimetableRef { id: bigint; precheck: number }

export function errorReportKeyFromRow(row: ErrorReportKeySource): ErrorReportKey
export function errorScheduleLink(
  cleanTimetableId: bigint | null,
  timetable: ErrorTimetableRef | undefined
): { schedule_link: ErrorScheduleLink; precheck_minutes: number | null }
export function errorReportItemFromRow(
  row: ErrorReportRowSource,
  timetable: ErrorTimetableRef | undefined
): ErrorReportItem
export function errorReportListWhere(query: ErrorReportDbQuery): Prisma.RobotErrorReportWhereInput
  • errorScheduleLink(): ①cleanTimetableId === null → none ②timetable === undefined → unknown ③★timetable.id !== cleanTimetableId → 例外★(呼び出しの誤り)④precheckMinutesFromCode(timetable.precheck)(lib/db/precheck-mapping.ts:97)が null → main ⑤それ以外 → precheck・その値。 ★CLEAN_TIMETABLE_PRECHECK.main を自分で比べない★(precheckMinutesFromCode() の1本)
  • errorReportItemFromRow():
    • id: row.error_id(★string のまま★)/detected_at: jstIsoFromDb(detectedAtFromError(row))
    • facility_id: Number(row.facility_id)/clean_timetable_id: row.clean_timetable_id === null ? null : Number(row.clean_timetable_id)(★BigInt を残すと NextResponse.json が例外★)
    • serial_number / error_type / error_level は ★そのまま★
    • resolution_status: resolutionStatusFromRobotErrorStatus(row.status)(★3以上は例外★ — 1段目で外れているので一覧では来ない。PATCH の1件読みで来たら500。fail-loud)
    • responded_at / resolved_at は jstIsoFromDb()(null はそのまま)/note: row.comment
    • ★precheckItemFromErrorRow()(:202-238)と同じ書き方で揃える★(日時・BigInt の扱い)
  • errorReportListWhere()(★SQL 側の写し。正は 2-5 の純粋関数★):
    const range = dbDateRange(query.date_from, query.date_to);
    return {
      status: { in: errorReportStatusCodes(query.include_resolved) },
      ...(query.facility_ids !== null
        ? { facility_id: { in: query.facility_ids.map((id) => BigInt(id)) } }
        : {}),
      ...(range.gte || range.lt
        ? { OR: [{ errored_at: range }, { errored_at: null, created_at: range }] }
        : {}),
    };
    ★error_type は SQL に降ろさない★(D9)。★page / per_page も降ろさない★(D8)

2-5. lib/error-reports/query.ts(新規)

export interface ErrorReportQuery {
  dateFrom: string | null;   // ★空文字は null に正規化★(`parsePrecheckQuery()` の注記と同じ理由)
  dateTo: string | null;
  includeResolved: boolean;
  facilityName: string;      // trim 済み。"" = 絞らない
  errorType: string;         // trim 済み。"" = 絞らない
  groupId: number | null;
  customerId: number | null;
  page: number;
  perPage: number;
}
export function parseErrorReportQuery(searchParams: URLSearchParams): QueryParseResult<ErrorReportQuery>
export function errorReportStatusCodes(includeResolved: boolean): number[]
export function compareErrorReportKeys(a: ErrorReportKey, b: ErrorReportKey): number
export function filterSortErrorReportKeys(keys: readonly ErrorReportKey[], query: ErrorReportDbQuery): ErrorReportKey[]
  • parseErrorReportQuery(): 日付の形式は parseDate()(lib/clean-logs/query.ts)、★date_from > date_to は 422(表2 F7)★、★期間の日数の上限は設けない★(/prechecks と同じ。§11 Q13)/ include_resolved は ★無い・空・"false" → false、"true" → true、それ以外 → 422★(表2 F3)/分類は parseMasterIdFilters()(lib/api/master-id.ts:93)/ page / per_page は parsePrecheckQuery()(lib/prechecks/query.ts)と同じ丸め(PAGINATION)。★エラーに入力値をエコーしない★
  • errorReportStatusCodes(): (includeResolved ? ["unresolved", "in_progress", "resolved"] : ["unresolved", "in_progress"]).map(robotErrorStatusFromResolutionStatus)(D2)
  • compareErrorReportKeys(): ★Date.parse(b.detected_at) - Date.parse(a.detected_at)★、0 なら ★a.error_id < b.error_id ? -1 : a.error_id > b.error_id ? 1 : 0★(localeCompare を使わない — 環境で順が変わる)
  • filterSortErrorReportKeys() — ★絞り込みの「正」★(SQL はその写し。指示書AC C-5 と同じ考え方):
    1. status が errorReportStatusCodes(query.include_resolved) に入る(★生の数値で比べる★)
    2. query.facility_ids !== null なら facility_id が入る(罠1)
    3. ★detected_at.slice(0, 10)★ が date_from 以上・date_to 以下(★文字列で比べる★。filterFixturePrechecks() lib/db/fixture-prechecks.ts:33-38 と同じ)
    4. query.error_type !== "" なら includesFoldPlain(key.error_type, query.error_type)
    5. compareErrorReportKeys() で並べる(★入力の配列を書き換えない★。コピーしてから sort)

2-6. lib/error-reports/label.ts(新規)

errorScheduleLabel(t: Translate, item: Pick<ErrorReportItem, "schedule_link" | "precheck_minutes">): string — 表3。 Translate は lib/prechecks/label.ts の型を import する。{n} は replace(precheckSeverityLabel() と同じ方式A)。

段階1 のテストが通ること。コミット。


段階3 — データ層

3-1. DataSource(lib/db/index.ts:205。updatePrecheckErrorColumns() の宣言 :300-308 の直後)

/**
 * エラー一覧(`GET /errors`。指示書AW)。★`robot_error_reports` の全行★が対象(事前チェック・本清掃・予定なし・`robot_id` NULL を問わない)。
 * ★2段階で読む★(D8): キーだけを SQL で `status IN`・`facility_id IN`・期間で絞って読み、
 *   `filterSortErrorReportKeys()` で絞り込み・並べ替え → ページの `error_id` だけ列を足して読む。
 * ★`total` は絞り込み後・ページ分け前の件数★(`meta.total`)。
 */
errorReports(query: ErrorReportDbQuery): Promise<{ items: ErrorReportItem[]; total: number }>;
/**
 * `PATCH /errors/{error_id}/status` の前後の読み取り。★`error_id === id` の行があれば返す(事前チェックかどうかを問わない)★。
 * ★大小文字は JS の `===` で決める★(D-4。`precheckItemById()` と同じ)。★期間・施設・状態では絞らない★
 */
errorReportById(id: string): Promise<ErrorReportItem | null>;
/**
 * 4カラムを書く(`writeRobotErrorColumns()` の1本。★書き込み用の接続★)。
 * ★`matched` で存在を判定しないこと★ — 存在確認は書く前に `errorReportById()` で行う
 */
updateErrorReportColumns(errorId: string, patch: PrecheckStatusPatch): Promise<{ matched: number }>;

3-2. fixtures のデータ — lib/fixtures/error-reports.ts(新規)

  • ★DB の行の形★(D13)で持つ: FixtureErrorReportRow(= ErrorReportRowSource + ★robot_id: bigint | null(読まない。実データの形を示すためだけ)★)。 日時は ★toDbJst(new Date(at(n, "HH:MM")))★(at() は lib/fixtures/_helpers.ts)。施設名は facilities(lib/fixtures/facilities.ts)から id で引いて入れる(★施設名を直書きしない★)
  • 予定は errorReportTimetables: ErrorTimetableRef[](★fixtures では本表・履歴を分けない★。E6 相当の行にはコメントで「実DBでは履歴にだけある想定」と書く)
  • ★入れる行(監督の指定+表1)★。id は "FX-ERR-01" 〜(★lib/fixtures/prechecks.ts の id と重ならない★)。値は ★ダミー★(実在の店名・住所を入れない。CLAUDE.md §7):| id | 内容 | status | 予定 | 備考 | |---|---|---|---|---| | FX-ERR-01 | ロボットのエラー(例 EmergencyStop / ERROR)・本清掃に紐づく | 0 | 本清掃 | E1 | | FX-ERR-02 | ロボットのエラー・事前チェック(PRECHECK_MINUTES_BEFORE[1])に紐づく | 1(responded_at あり) | 事前チェック | E2 | | FX-ERR-03 | 解消済み・本清掃(responded_at / resolved_at / 備考あり) | 2 | 本清掃 | E3 | | FX-ERR-04 | clean_timetable_id NULL | 0 | なし | E4 | | FX-ERR-05 | clean_timetable_id が予定に無い id | 0 | 不明 | E5 | | FX-ERR-06 | 予定が履歴にだけある想定・本清掃 | 0 | 本清掃 | E6 | | FX-ERR-07 | errored_at NULL(created_at だけ) | 0 | 本清掃 | E7 | | FX-ERR-08 | ★robot_id NULL・serial_number '0'・error_type SECURITY_ON★ | 0 | 本清掃 | E8 | | FX-ERR-09 | 事前チェック(★10 を直書き。設定に無い値★) | 0 | 事前チェック | E9 | | FX-ERR-10 | ★robot_id NULL・serial_number = 制御盤の SN(lib/fixtures/ds-devices.ts の DS-001)・error_type D-S_CONTROL_FAILURE★ | 0 | 本清掃 | P-8 a | | FX-ERR-11 | ★robot_id NULL・serial_number '0'・error_type NOT_STARTED・error_level CRITICAL★ | 0 | 本清掃 | ステージング実測の形(D-158 2026-10-06 実測・先方投入) |★error_level の値(CRITICAL 以外)と SECURITY_ON / D-S_CONTROL_FAILURE のレベルは実データ未受領のダミー★ — ファイル冒頭のコメントに書く。
  • lib/fixtures/index.ts に export * from "./error-reports"; を1行足す

3-3. fixtures の上書き表 — lib/db/fixture-error-report-overrides.ts(新規)

lib/db/fixture-precheck-overrides.ts と同じ形で、★値は DB の列の形★(D14):

const overrides = new Map<string, RobotErrorColumnsData>();
export function resetFixtureErrorReportOverrides(): void
/** 上書きを反映した行(★元の配列の要素は書き換えない★)。`comment` は `status` と同じく上書きのキーがあるときだけ差し替える */
export function withFixtureErrorReportOverride<T extends FixtureErrorReportRow>(row: T): T
/** `robotErrorColumnsFromPatch(patch)` で DB の列に写し、既存の上書きに重ねる。★`error_id === id`★ が無ければ `matched: 0` */
export function applyFixtureErrorReportPatch(rows: readonly FixtureErrorReportRow[], id: string, patch: PrecheckStatusPatch): { matched: number }
  • ★robotErrorColumnsFromPatch() を通す★ので、4つ以外のキー・空パッチは Prisma 版と同じく例外になり、日時は ★toDbJst() 済み★(読み出しは jstIsoFromDb() で戻る)
  • ★プロセスを再起動すると消える★(AT と同じ。D-045)
  • ★AT の fixture-precheck-overrides.ts とは状態を共有しない★(id が重ならないので食い違いは起きない。§11 Q8)

3-4. fixtures 版(fixtureSource。lib/db/index.ts:548-563 の事前チェックの3メソッドの後)

async errorReports(query: ErrorReportDbQuery) {
  const rows = errorReportRows.map(withFixtureErrorReportOverride);
  const keys = filterSortErrorReportKeys(rows.map(errorReportKeyFromRow), query);
  const start = (query.page - 1) * query.per_page;
  const pageIds = keys.slice(start, start + query.per_page).map((k) => k.error_id);
  // 以下、Prisma 版と同じ: id で行を引き、予定を id で引いて errorReportItemFromRow()
  ...
  return { items, total: keys.length };
},
async errorReportById(id) { /* `===` で探し、上書きを反映して errorReportItemFromRow() */ },
async updateErrorReportColumns(errorId, patch) { return applyFixtureErrorReportPatch(errorReportRows, errorId, patch); },

★予定の引き当て(clean_timetable_id → ErrorTimetableRef)は .toString() をキーにした Map★(4-9)。 ★ページの切り出し・行の組み立てを Prisma 版と同じ関数にしたければ、非公開の関数(例 pageOfErrorReports())に切り出して2本で共有してよい★(★規則を2箇所に書かない★)。

3-5. Prisma 版(prismaSource。updatePrecheckErrorColumns() lib/db/index.ts:1556-1562 の後)

async errorReports(query: ErrorReportDbQuery) {
  const { getPrisma } = await import("./prisma");
  const prisma = await getPrisma();
  // 1段目: キーだけ(★SQL で status / 施設 / 期間を絞る。並べ替え・ページ分けはしない★。D8)
  const keyRows = await prisma.robotErrorReport.findMany({
    where: errorReportListWhere(query),
    select: ROBOT_ERROR_LIST_KEY_SELECT,
  });
  const keys = filterSortErrorReportKeys(keyRows.map(errorReportKeyFromRow), query);
  const start = (query.page - 1) * query.per_page;
  const pageIds = keys.slice(start, start + query.per_page).map((k) => k.error_id);
  if (pageIds.length === 0) return { items: [], total: keys.length };   // ★0件なら2段目を撃たない★
  // 2段目: そのページの行だけ列を足して読む
  const rows = (await Promise.all(
    chunkForInClause(pageIds).map((ids) =>
      prisma.robotErrorReport.findMany({ where: { error_id: { in: ids } }, select: ROBOT_ERROR_LIST_ROW_SELECT })
    )
  )).flat();
  // 予定は2表から id で(D5)。★id が0件ならクエリを撃たない★(findTimetablesByChunks が塊0個で返す)
  const timetableIds = [...new Set(rows.flatMap((r) => (r.clean_timetable_id === null ? [] : [r.clean_timetable_id.toString()])))].map(BigInt);
  const timetables = await findTimetablesByChunks(prisma, chunkForInClause(timetableIds), (ids) => ({
    where: { id: { in: ids } },
    select: { id: true, precheck: true },
  }));
  ...(rowById / timetableById を `.toString()` / `error_id` で引き、★pageIds の順★に並べて errorReportItemFromRow()。見つからない error_id は飛ばす(4-15))
  return { items, total: keys.length };
},
  • ★errorReportById()★: findUnique({ where: { error_id: id }, select: ROBOT_ERROR_LIST_ROW_SELECT }) → ★report.error_id !== id なら null★(4-8)→ clean_timetable_id があれば findTimetableById(prisma, report.clean_timetable_id, { id: true, precheck: true })(lib/db/timetable-source.ts:285)→ errorReportItemFromRow()
  • ★updateErrorReportColumns()★: updatePrecheckErrorColumns()(:1556-1562)と ★同じ中身★(getWritePrisma() → writeRobotErrorColumns())。 ★同じ3行を2回書きたくなければ、非公開の関数に切り出して2本から呼んでよい★(updatePrecheckErrorColumns() の挙動は変えない)
  • import は lib/db/index.ts:97-127 の並びに足す(ROBOT_ERROR_LIST_*_SELECT・error-report-mapping・fixture-error-report-overrides・errorReportRows / errorReportTimetables)

3-6. テスト

  1. ★lib/db/__tests__/error-report-mapping.test.ts★ に errorReportListWhere() を足す: 既定 → status: { in: [未解消, 対応中のコード] } だけ/facility_ids: [] → facility_id: { in: [] }(★キーが消えない★)/null → facility_id のキーが無い/ 期間あり → OR の2要素で、gte / lt が ★壁時計値★(date_from = "2026-10-05" → gte = 2026-10-05T00:00:00.000Z、date_to = "2026-10-05" → lt = 2026-10-06T00:00:00.000Z)/期間なし → OR のキーが無い
  2. ★lib/db/__tests__/fixture-error-reports.test.ts(新規)★: fixtures の11行が ★errorReportKeyFromRow() / errorReportItemFromRow() を例外なく通る★/FX-ERR-03 は既定で出ない/ applyFixtureErrorReportPatch() → errorReports() に反映される/resetFixtureErrorReportOverrides() で戻る/存在しない id・大小文字違いは matched: 0/★4つ以外のキーは例外★

コミット。


段階4 — API(YAML → 型 → ルート)

4-1. d-watch-api.yaml(★先に直す。CLAUDE.md §6-1★)

  1. tags(:39-62)の prechecks(:46-47)の後に - name: errors / description: エラー一覧(robot_error_reports) を足す
  2. /prechecks/{id}/status(:690-755)の後・# ---- reports(:757)の前に ★/errors★ と ★/errors/{error_id}/status★ を足す:
    • GET /errors: description に ★対象は robot_error_reports の全行(事前チェック・本清掃・予定なし・制御盤・セキュリティ・清掃開始失敗を問わない)★/既定は未解消・対応中、include_resolved=true で解消も/ ★並び順は detected_at(= errored_at、無ければ created_at)の降順、同値は id(error_id)の昇順(文字列の辞書順)★/★error_type・error_level・serial_number は DB の値のまま(D-077)★//alerts との違い(あちらは事前チェック+出発失敗だけ)。 パラメータ: date_from / date_to(★検知時刻の日付★。date_from > date_to は422)/ $ref GroupId / $ref CustomerId / facility_name(部分一致。記号を無視)/ error_type(部分一致・大小文字を区別しない)/ include_resolved(type: boolean、default: false。true / false 以外は422)/ $ref Page / $ref PerPage。 200 は ErrorReportListResponse、422 は ValidationError
    • PATCH /errors/{error_id}/status: ★/prechecks/{id}/status の description(:694-713)の日時・備考の規則をそのまま写す★。違いは ★「robot_error_reports に error_id の行があれば書ける(事前チェックかどうかを問わない)。無ければ404」★だけ。 パスパラメータは /prechecks/{id}/status と同じくインラインの { type: string, maxLength: 50 }(★共有の PathId(integer)を参照しない★。:715-717 のコメントと同じ理由)。 リクエストは /prechecks/{id}/status と同じ(status 必須・note は maxLength: 400)。200 は ErrorReportItem、404 は Error、422 は ValidationError
  3. components/schemas の PrecheckListResponse(:1882 付近)の後に ★ErrorReportItem★(2-1 の型と同じ。schedule_link は enum: [main, precheck, none, unknown] と4区分の説明・precheck_minutes は「schedule_link=precheck のときだけ。生の分数」)と ★ErrorReportListResponse★(items + meta: $ref Meta。★summary は無い★)を足す

4-2. GET /errors — app/api/errors/route.ts(新規)

形は app/api/clean-logs/route.ts(動く見本)と app/api/prechecks/route.ts に揃える:

  1. parseErrorReportQuery() → 失敗は 422
  2. ★try の中で★ Promise.all([resolveFacilityIdsByName(q.facilityName), resolveFacilityIdsByMasters(q)]) → intersectFacilityIds()(D10)→ db.errorReports({ include_resolved, facility_ids, date_from, date_to, error_type, page, per_page })
  3. 例外は 500(logApiError。文言「データの取得に失敗しました」)
  4. ListResponse<ErrorReportItem>({ items, meta: { total, page, per_page } })を返す。withRequestId() で包む

4-3. PATCH /errors/{error_id}/status — app/api/errors/[error_id]/status/route.ts(新規)

★app/api/prechecks/[id]/status/route.ts(全173行)と同じ順序・同じ部品★(形式 → 本文 → 実在 → 計画 → 書き込み → 読み直し)。違いだけ書く:

箇所 /prechecks /errors
ROUTE /api/prechecks/[id]/status ★/api/errors/[error_id]/status★
params { id } ★{ error_id }★
実在・読み直し db.precheckItemById() ★db.errorReportById()★
書き込み db.updatePrecheckErrorColumns() ★db.updateErrorReportColumns()★
404 の文言 対応状況を記録できるアラートが見つかりません(出発失敗には記録先がありません) ★「エラーが見つかりません」★
監査ログの before の型 PrecheckItem ★ErrorReportItem★(4項目の名前は同じ)
  • 同じものを使う: parsePathErrorId()(lib/api/path-params.ts:89)/parsePrecheckStatusUpdateBody()/planPrecheckStatusUpdate()/isEmptyPatch()/columnsForPatch()(lib/db/robot-error-write.ts:135)/logDbWrite() / dbErrorCode()(lib/logger/db-write.ts:53 / :22)
  • ★const now = new Date() は1回だけ★。★空パッチなら書かずに 200 で before・db.write も出さない★。★matched === 0 は 404・db.write(success: false, db_error_code: "no_matching_row")★。★書き込みの例外は 500・db.write(success: false)★。★成功時の db.write は書き込み直後に1件★
  • ★監査ログに備考の中身を載せない★(comment_changed: boolean だけ。docs/logging-design.md の禁止キー)
  • ★auditBase() 等を /prechecks のルートと共有するための書き換えはしない★(D12)。同じ形の小さな関数を新しいルートに置く

4-4. テスト

  1. ★app/api/errors/__tests__/route.test.ts(新規。Prisma 経路)★ — app/api/prechecks/[id]/status/__tests__/route.test.ts:1-110 の作法(vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: false }))・@/lib/db/prisma をスタブ):
    • スタブの robotErrorReport.findMany は ★guardNullRobotId() を通し★、★select を見て返す列を絞る★(1段目と2段目で返す形が違う)。where は ★status.in / facility_id.in / OR(期間)/ error_id.in を評価★する
    • cleanTimetable.findMany / cleanTimetableHistory.findMany を別の配列から返す(E6 は履歴だけ)。facilityNameIndex 等、施設名・分類の解決に要るものは既存の app/api/__tests__/master-filter.test.ts の作法を見て用意する
    • ★表1 の9行で★: 既定は E3 以外の8行・並びが表1 のとおり(E7 が先頭・同時刻は "E-10" が先)・各行の schedule_link / precheck_minutes / detected_at / serial_number / error_type が表1 どおり。include_resolved=true で9行
    • 表2 F3・F4・F6・F7・F8・F10・F11・F12
    • ★1段目の where が errorReportListWhere() の戻りと toEqual★(ルートが別の条件を組み立てていない)
    • ★1段目の select が ROBOT_ERROR_LIST_KEY_SELECT、2段目が ROBOT_ERROR_LIST_ROW_SELECT★/★0件のとき2段目と予定のクエリを撃たない★
  2. ★app/api/errors/[error_id]/status/__tests__/route.test.ts(新規。Prisma 経路)★ — /prechecks の同名テストの describe を写し、行を表1 にする:
    • ★200: E1(本清掃)・E4(予定なし)・E5(予定不明)・E8(robot_id NULL)・E2(事前チェック)★ に書ける。updateMany の data が該当の列だけ
    • AT §3-4 の代表(#1・#2・#4・#8・#9)・備考だけ・備考を空に・★空パッチなら updateMany を呼ばない・db.write も出さない★
    • ★本文に updated_at・error_type・memo を混ぜても data に出ない★
    • ★404: 行が無い/★大小文字違い(e-1 で E-1)★/matched === 0★
    • 422: id(空・51文字)・JSON・status・note の型・401文字(PRECHECK_NOTE_MAX_LENGTH + 1)
    • ★書き込みの例外 → 500。db.write(success: false)と api.error が1件ずつ・コードだけ★
    • ★db.write: route が /api/errors/[error_id]/status・record_id が文字列・columns が実際に SET した列だけ・備考の中身を含まない★
    • ★参照用の接続で updateMany を呼ばない★(スタブの参照用は updateMany で例外。/prechecks のテスト :96-98 と同じ)
  3. ★app/api/errors/__tests__/route.fixtures.test.ts(新規)★: fixtures で GET(既定で FX-ERR-03 が出ない・include_resolved=true で11行)→ PATCH → GET に反映される/存在しない id は404。beforeEach で resetFixtureErrorReportOverrides()
  4. ★既存のテストが無修正で通ること★: app/api/prechecks/__tests__/route.test.ts・app/api/prechecks/[id]/status/__tests__/*.test.ts・lib/db/__tests__/robot-error-write-sql.test.ts(★/prechecks の挙動と書き込みの SQL が変わっていない証拠★)

YAML → ルート → テストの順でコミット(1つでも分けてもよい)。


段階5 — 画面

5-1. メニュー — components/layout/AppShell.tsx

MENU_MONITORING(:8-23)の ★/alerts の行(:10)の直後★ に足す:

// ★エラー一覧(D-160 / 指示書AW)★ — robot_error_reports の未対応エラーを全部出す(/alerts は事前チェック+出発失敗だけ)。
//   ★契約によるメニューの出し分けは無い(このファイルの全メニューと同じ。指示書AW D15)★
{ href: "/errors", id: "errors", label: "🚨 エラー一覧" },

★ラベルとアイコンは暫定(§11 Q6)★。モックにこのページは無い(★モックは書き換えない★)。

5-2. ページ — app/(dashboard)/errors/page.tsx(新規)

app/(dashboard)/alerts/page.tsx(5行)と同じ形で <ErrorsScreen /> を返す。

5-3. components/features/errors/ErrorsScreen.tsx + .module.css(新規)

★見た目の作法は既存の画面から借りる。新しい共通部品は作らない★(AT §3-8 と同じ考え方)。

  • 見出し: t("エラー一覧")、副題 t("未対応のエラー(ロボット・制御盤・セキュリティ・清掃開始失敗)")。件数は t("全{n}件").replace("{n}", String(total))(★"{n}件" は使わない★。4-18)
  • ツールバー(★1操作=1確定。「検索」ボタンは置かない★。AlertsScreen.tsx:146-155 の判断と同じ):
    • 期間2つ(<input type="date">。★初期値は空=期間なし★。空の側は URL に付けない。両方空のあいだは t("指定なし(全期間)") を出す — 清掃詳細ログ(AV 段階5b。137a94b)と同じ見せ方。lib/i18n.ts:363 の既存キー)
    • 分類のセレクト(AlertsScreen.tsx:358-410 をそのまま写す。useClassificationFilter(t)・appendClassificationParams()(lib/masters/options.ts:218)。★option のラベルを t() に通さない★)
    • 施設名・種類のテキスト入力(★300ms の debounce★。DetailLogScreen.tsx:110-117 の作法。placeholder は t("施設名") / t("種類"))
    • チェックボックス t("解消済みも表示")(既定オフ)
    • ★どれを変えても setPage(1) をその場で★(AlertsScreen.tsx:162-170 の注記)
  • 表(★table-layout: fixed + <colgroup>★。CleanLogTable.module.css:30-50 の作法で、列幅を CSS 変数にし min-width を合計にする。★狭い画面では表を横スクロールの箱に入れる★)。列と幅の目安(★幅は実装者が調整してよい。ただし固定にする★):| 列 | 中身 | 幅の目安 | |---|---|---| | t("検知時刻") | formatMd + formatHm(例 10/5 19:05。AlertsScreen.tsx:42-44) | 96px | | t("施設名") | facility_name | 残り(最小 140px) | | t("シリアル") | serial_number(★0 もそのまま★) | 140px | | t("種類") | error_type(★生値。t() に通さない★。長い値は折り返す) | 170px | | t("レベル") | error_level(★生値。t() に通さない★) | 90px | | t("紐づく予定") | errorScheduleLabel(t, item) | 170px | | t("対応状況") | ★ボタン★(下) | 150px | | t("備考") | ★インライン編集★(下) | 220px |★error_type / error_level / serial_number を t() に通さない理由★: 辞書に同じ綴りのキーがあると誤って訳される(AlertsScreen.tsx:363-364 の「屋号名まで通すと辞書の対訳に誤って当たる」と同じ)
  • 対応状況のボタン: AlertsScreen.tsx:46-72(statusBadgeClass() / StatusBadgeText)と :503-511 の見た目を写す。クリックで ★nextResolutionStatus()(lib/prechecks/status-cycle.ts)★ の状態を送る(★備考は送らない★)。★ボタンは固定幅★(4-17)
  • 備考: AlertsScreen.tsx:97-140(PrecheckNote)の作法を写す — クリックで textarea・★onBlur で確定★・★変えていなければ送らない★・★失敗時は編集状態のまま★・maxLength={PRECHECK_NOTE_MAX_LENGTH}(lib/config/index.ts:613)・★備考だけ変えるときも現在の status を送る★
  • 更新: PATCH /api/errors/${encodeURIComponent(item.id)}/status。★成功したらレスポンスでその1行だけ差し替える★(一覧を取り直さない)。★「解消済みも表示」がオフでも、解消にした行は次に取り直すまで消さない★(押し間違いをその場で戻せる。AlertsScreen.tsx:242-251 と同じ)。 ★保存中はその行のボタン・入力を無効★。保存中・失敗の表示は ★その行の直後に <tr><td colSpan={8}> で★ 出す(t(rowError.message)。サーバーの文言は辞書に無ければ原文)
  • 空・読み込み・失敗: t("該当するエラーはありません") / t("読み込み中…") / サーバーの文言(AlertsScreen.tsx:445-450 の作法。★「読み込み中…」は t() に通す★)
  • ページ送り: Pagination(AlertsScreen.tsx:532-537)。★meta.total を使う★。存在しないページに取り残されたら最終ページへ(AlertsScreen.tsx:219-223。★pages > 0 の判定を落とさない★)

5-4. /alerts の巡回順を共有する — components/features/alerts/AlertsScreen.tsx

★変えてよいのはここだけ★(D17・4-16): :76-89 の nextResolutionStatus() を削り、lib/prechecks/status-cycle.ts から import する。★他は1文字も変えない★。

5-5. lib/i18n.ts

足すキー(★EN は暫定。ユーザーが後でまとめて見直す★)。★キーは画面の原文と1文字も違わないこと(全角括弧・中点)★。重複は lint が落とす:

// ---- エラー一覧(D-160 / 指示書AW)----
"🚨 エラー一覧": "🚨 Errors",
エラー一覧: "Errors",
"未対応のエラー(ロボット・制御盤・セキュリティ・清掃開始失敗)": "Open errors (robots, control panels, security, start failures)",
"全{n}件": "{n} total",
検知時刻: "Detected at",
種類: "Type",
レベル: "Level",
紐づく予定: "Schedule",
対応状況: "Status",
備考: "Note",
本清掃: "Main run",
"事前チェック({n}分前)": "Pre-check ({n} min before)",
事前チェック: "Pre-check",
予定なし: "No schedule",
予定不明: "Unknown schedule",
解消済みも表示: "Show resolved",
該当するエラーはありません: "No matching errors",
エラーが見つかりません: "Error not found",

★既にあるキーは足さない★: 施設名・シリアル・未解消・対応中・解消・クリックして備考を編集・備考を入力・保存中…・保存に失敗しました・対応状況の更新に失敗しました・読み込み中…・指定なし(全期間)・ホールディングス・顧客(lib/i18n.ts で確かめてから足す。AV の実装でキーが増えていないかも見る)。

5-6. 確認

★画面のテストは vitest で走らない★(vitest.config.ts:14)ので段階7 の画面確認で見る。★巡回順・ラベルは純粋関数のテスト(段階1)で固定してある★。


段階6 — 資料(本書で直すのは次の2つだけ)

  1. docs/fixtures.md: ★§13「エラー一覧(error-reports)」を新設★(§12.3 :439- の後)。3-2 の表(11行・表1 との対応)と、 ★既定で10行(FX-ERR-03 以外)・「解消済みも表示」で11行★、★robot_id は読まない(select で絞る)ので fixtures でも値は使わない★、★error_level 等はダミー★ を書く
  2. ★d-watch-api.yaml / lib/types/index.ts / ルートの冒頭コメント★(段階4 で書いたもの)

★CLAUDE.md / docs/decisions.md / docs/code-map.md / docs/spec.md は監督が直す★。


段階7 — 全体の確認

  1. npx tsc --noEmit -p . — 0件
  2. npm run lint — 0件(★eslint-disable を1つも足していない★・i18n の重複キー・$queryRaw・any・未使用の import が無い)
  3. npm test — ★全件通過★(段階0 の件数から増えていること)
  4. ★USE_FIXTURES=true で npm run build★ — 成功(PowerShell なら $env:USE_FIXTURES='true'; npm run build)。★ワークツリーではなく本体で行う★(AV §13-4。Turbopack が node_modules のジャンクションを拒否する)
  5. ★画面で確かめる(USE_FIXTURES=true)★: .claude/launch.json の ★d-watch-fixtures(ポート 3100)★ で起動する。★確かめ終わったら自分で起動した dev サーバーは必ず止める★
    • ★メニュー★: 「モニタリング」の「🔔 アラート」の直後に「🚨 エラー一覧」。EN で "🚨 Errors"
    • ★一覧★: 既定で10行・並びが検知時刻の降順。FX-ERR-08 のシリアルが「0」、種類が「SECURITY_ON」。FX-ERR-09 が「事前チェック(10分前)」、FX-ERR-04 が「予定なし」、FX-ERR-05 が「予定不明」
    • ★「解消済みも表示」★で11行。★絞り込み★(施設名・種類 secur・期間・分類)で件数が変わり、1ページ目に戻る
    • ★状態ボタン★: 未解消 → 対応中 → 解消 → 未解消。★解消にした行が既定表示でも消えない★(取り直すまで)。★備考★を入れる → 空にする
    • ★/alerts★ の状態ボタンの巡回順が今までと同じ(D17)
    • ★JP/EN 切替で表の列幅・ボタンの位置が動かない★。375px 幅で表が横スクロールになり、ページ全体が横にはみ出さない(★崩れていたら直さずに報告★。AV §13-4 のサイドバーの件は既知)
  6. ★ローカルDB(dwatch_local)で確かめる★(USE_FIXTURES=false。npm run seed:local は ★lib/db/local-guard.ts がローカル以外を拒否する★):
    • /errors が出る。★シードの事前チェックのエラー行★が「事前チェック(N分前)」で出る
    • 1行を 未解消 → 対応中 → 解消 → 未解消、備考を入れる → 空にする。サーバーのログに db.write が success: true で出て、route が /api/errors/[error_id]/status・columns が実際に変えた列だけ・備考の中身が無い
    • ★AT 段階8-2 の列単位 GRANT のローカル専用ユーザーがあれば★、それで書いて ★1143(権限エラー)が出ない★ことも見る(★パスワード・ユーザー名を文書・報告に書かない★)
  7. 最後の grep(★0件になること★):
    grep -rn "\$queryRaw\|\$executeRaw" app lib components
    grep -rn "status: { in: \[" lib app --include=*.ts | grep -v __tests__
    grep -rn "robot_id: true\|robot: true\|robot: {" lib/db/robot-error-select.ts

段階8 — ステージングで確かめる(★条件付き。満たさなければ止まって報告★)

着手の条件(すべて): ステージングに接続できる(★別の認証情報を探したり試したりしない★)//errors に ★未解消・responded_at / resolved_at / 備考がすべて空★ の行がある。

手順(AT 段階9 と同じ。★ステージングへの4カラムの書き込みはユーザーが許可済み。D-152★):

  1. ★書き込むのは1行だけ★。★事前チェック以外の行(本清掃に紐づく・robot_id NULL の行など)★から選ぶ(★本書で新しく書けるようになった行で確かめる★)
  2. ★操作前の値を記録する★: error_id と4列(画面・API の値)。★mysql で読むだけ★(★UPDATE / DELETE / INSERT を手で打たない★)
  3. 未解消 → 対応中 → 解消 → 未解消、備考を入れる → 空にする
  4. ★最後に4列が操作前と完全に一致すること★(updated_at は DB が更新するので戻らない — 値を記録して報告。AT §10 #8 で受け入れ済み)
  5. ★robot_id が NULL の行が一覧に出て、読み取りで落ちない★ことを確かめる(D-158 時点で2行)
  6. ★途中でデータが入れ替わったら止めて報告★(第10回 N-6)。★他の行に書いて代わりにしない★
  7. ★npm run seed:local をステージングに向けない★(テーブルを消して入れ直すので実データが消える)

8. テスト一覧(本書で足す・直すもの)

ファイル 内容 段階
lib/db/__tests__/error-report-mapping.test.ts(新規) 表1 E1〜E9 の行の組み立て・errorScheduleLink()・errorReportKeyFromRow()・errorReportListWhere() 1・3
lib/error-reports/__tests__/query.test.ts(新規) 表2・表1 の並び順・parseErrorReportQuery()・errorReportStatusCodes() 1
lib/error-reports/__tests__/label.test.ts(新規) 表3 1
lib/prechecks/__tests__/status-cycle.test.ts(新規) 巡回順 1
lib/db/__tests__/robot-error-select.test.ts SELECTS に2つ足す 1
lib/db/__tests__/fixture-error-reports.test.ts(新規) fixtures の11行・上書き表 3
app/api/errors/__tests__/route.test.ts(新規) GET(Prisma 経路。表1・表2・2段階の読み方) 4
app/api/errors/[error_id]/status/__tests__/route.test.ts(新規) PATCH(Prisma 経路。全行に書ける・404・422・500・db.write) 4
app/api/errors/__tests__/route.fixtures.test.ts(新規) fixtures で GET → PATCH → GET 4
★無修正で通ること★ app/api/prechecks/**・lib/prechecks/__tests__/status-update.test.ts・lib/db/__tests__/robot-error-write-sql.test.ts・lib/db/__tests__/precheck-mapping.test.ts —

9. 触ってはいけないもの

  • CLAUDE.md / docs/decisions.md / docs/code-map.md / docs/spec.md(監督が直す)
  • d-watch-mock.jsx(このページはモックに無い。モックは書き換えない)
  • ★prisma/schema.prisma / lib/generated/prisma/**★(スキーマは変えない。★RobotErrorReport.updated_at に @updatedAt を付けない★。db pull は実行しない)
  • ★control_error_histories への書き込み・D-Sec アラート(/ds-alerts・PATCH /ds-alerts/{id}/status・DsAlertsScreen.tsx・lib/ds-alerts/**)★(取得元の切り替えは次フェーズ。D-158 項5 / D-160)
  • ★/alerts の挙動★: prismaSource.prechecks() / precheckItemById() / updatePrecheckErrorColumns() の中身、app/api/prechecks/**、lib/db/fixture-precheck-overrides.ts、lib/fixtures/prechecks.ts。★AlertsScreen.tsx は 5-4 の置き換えだけ★
  • planPrecheckStatusUpdate() / parsePrecheckStatusUpdateBody() / isEmptyPatch() の中身(lib/prechecks/status-update.ts)、lib/db/robot-error-write.ts の中身(★書いてよい列を増やさない★)
  • detectedAtFromError() / precheckMinutesFromCode() / resolutionStatusFromRobotErrorStatus() / robotErrorStatusFromResolutionStatus() の中身(lib/db/precheck-mapping.ts)
  • findTimetables() / findTimetablesByChunks() / findTimetableById() / mergeTimetableRows() の中身(lib/db/timetable-source.ts)
  • AV で入れたもの(lib/dashboard/latest-run.ts・/api/dashboard・DashboardScreen.tsx)
  • scripts/seed-local-db.ts とガード(lib/db/local-guard.ts)
  • ★4カラム以外への書き込み・生SQL・eslint-disable の追加★
  • ★認証情報(パスワード・接続文字列・DB のユーザー名)を文書・コード・テスト・コミット・作業報告に書くこと★(D-154 項3)
  • ★git add -A★

10. 範囲外(本書ではやらない。監督が扱う)

  • ★エラーの区分(ロボット/制御盤/セキュリティ/清掃開始失敗)を作ること★ — P-8 の規則で作れるがユーザー判断で生値のみ(D-160 却下案)。★error_type にコードを付けるのは次フェーズ★
  • ★D-Sec アラートの取得元を robot_error_reports に切り替えること★(P-10。次フェーズ)
  • ★出発失敗(レポート行が無い予定)の対応状況の記録★(D-105 申し送り。「清掃開始できなかった」行 NOT_STARTED との関係は次フェーズで整理)
  • サマリのカード(未解消◯件 等)・メニューのバッジ(モックの /alerts にある未解消件数のバッジ。d-watch-mock.jsx:372)・CSV 出力
  • ★契約(facilities.contract)によるメニュー・データの出し分け★(D15。§11 Q2)
  • ★「解消済みも表示」の性能★(保持期間ぶんのキーを読む。D8 の理由)— リリース前に本番の行数を見て見直す
  • 関数名・定数名の一般化(planPrecheckStatusUpdate → 例 planResolutionUpdate、PRECHECK_NOTE_MAX_LENGTH → 例 ERROR_NOTE_MAX_LENGTH、PrecheckStatus → 例 ResolutionStatus)(§11 Q7)
  • /alerts の備考・状態ボタンの部品を /errors と共通の部品にすること(本書は作法を写すだけ)

11. 監督への質問(★回答で暫定が変わったら「直す場所」を同時に直すこと★)

★監督の決定(2026-10-06): Q1〜Q14 はすべて下の表の「本書の暫定」どおりで確定★。本文は暫定のとおり書いてあるので、直す場所は無い。 ★Q1 の補足★: 依頼文の「SQL の take/skip でページ分け」は、errored_at ?? created_at の順を Prisma で書けないため取り下げる(監督の依頼文の誤り)。 ★Q2 の補足★: 契約によるメニューの出し分けが未実装であることは、本書の範囲外として監督が別に扱う(docs/decisions.md D-160 追記)。

# 論点 本書の暫定 直す場所
Q1 ★依頼文の「ページ分けは SQL(take/skip)で」と食い違う★。検知時刻(errored_at ?? created_at)の順は Prisma のクエリビルダで書けない ★キーだけを SQL で絞って全件読み、アプリで並べてページ分け → ページの行だけ読み直す★(D8)。対案: orderBy: errored_at desc nulls last, created_at desc で SQL ページ分け(★E7 の並びが表1 と食い違う★ので採らない) §2 D8 / 2-4 / 3-4 / 3-5 / 4-4
Q2 ★依頼文の「メニューは /alerts と同じ規則(D-Watch の契約)」★ — ★実測では AppShell.tsx に契約による出し分けが無い★(全メニューを無条件に描く。CLAUDE.md §4.6 の「契約のないセクションはメニューごと非表示」は未実装) ★/alerts と同じく常に出す★。データも契約で絞らない(/prechecks も絞っていない) §2 D15 / 5-1 / §10
Q3 status が 0/1/2 以外(UnsignedTinyInt なので3以上が入りうる)の行 ★一覧には出さない(SQL の IN で外れる)・例外にもしない★(表2 F12)。PATCH の1件読みで来たら500(fail-loud) 表2 F12 / 2-4 / 2-5
Q4 種類の絞り込み ★部分一致・大小文字を区別しない(includesFoldPlain())をアプリ側で★。依頼文の候補「存在する値から選ぶセレクト」は採らない §2 D9 / 2-5 / 5-3
Q5 列見出し ★「施設名」★(依頼文は「店舗」。CLAUDE.md §8 の用語と、既存キー 店舗 の EN が "Stores" のため) §2 D16 / 5-3 / 5-5
Q6 メニューのアイコンと EN ★🚨 エラー一覧 / 🚨 Errors(暫定)★ 5-1 / 5-5
Q7 planPrecheckStatusUpdate / PRECHECK_NOTE_MAX_LENGTH / PrecheckStatus / PrecheckStatusPatch の名前 ★改名しない(そのまま使う)★。改名は AT のテスト・ルートを巻き込むので別作業(§10) §2 D11 / 2-1 / §10
Q8 fixtures の上書き表を AT と共有するか ★別のモジュール(状態を共有しない)★。fixtures の id が重ならないので /alerts と /errors の食い違いは起きない §2 D14 / 3-3
Q9 レスポンスに clean_timetable_id を載せるか ★載せる(画面には出さない)★ §2 D18 / 2-1 / 4-1
Q10 /alerts の nextResolutionStatus() を共有の純粋関数に移すか(AlertsScreen.tsx に import の置き換えだけが入る) ★移す★(2画面の巡回順が食い違わないため。挙動は変わらない) §2 D17 / 2-2 / 5-4 / 4-16
Q11 段階8(ステージングで1行書いて戻す)を行うか ★条件が揃えば行う★(D-152 で許可済み。事前チェック以外の行で確かめる) 段階8
Q12 サマリのカード(未解消◯件)・メニューのバッジ ★作らない★(件数は「全◯件」だけ) §10 / 5-3
Q13 期間の日数の上限(MAX_DATE_RANGE_DAYS) ★設けない★(/prechecks と同じ。date_from > date_to だけ422) 2-5 / 4-1
Q14 表2(F1〜F12)・表3(S1〜S6)・fixtures の11行と「既定10行・全11行」を正にしてよいか 本書で導いた値 §3 / 3-2 / 段階6 / 段階7

12. コミット

段階ごとに分ける(段階1 / 段階2 / 段階3 / 段階4 / 段階5 / 段階6)。★git add はファイルパスを明示する★(git add -A 禁止)。 メッセージには ★何を変えたか★ と ★なぜ(D-160。未対応のエラーを一覧し、対応状況と備考を更新できるようにする)★ を書く。 ★段階1 のコミットは「テストだけ(この時点では落ちる)」と明記する★。 ★D8(2段階で読む)を入れるコミットには、COALESCE の並べ替えを Prisma で書けないことと、nulls last を採らない理由(表1 E7)を書く★。 ★PR の作成とマージは daichi がレビューしてから行う★(D-154)。


13. 実装の記録(2026-10-06)

★実装は Cursor ではなく Claude Code★(ユーザー指示 2026-10-06。本書の扱いを聞いた質問に「ここで実装する」と回答。段階8 も条件がそろえば行うと確認済み)。 監督がサブエージェント2本(★バックエンド: 段階1〜2 → 段階3・4・6★ / ★画面: 段階5★)に分け、ワークツリーで実装 → develop に cherry-pick。 ★期待値表(§3 表1)は変えていない★。記録の正は docs/decisions.md D-160 の「実装の記録」と docs/handoff.md 追記62。

13-1. コミット(develop。★すべて未 push★。origin/develop は 19dbe38)

段階 コミット
1(テストだけ。この時点では落ちる) fc9b3cf
2(型・純粋関数) 499b23f
5-4(/alerts の巡回順を lib/prechecks/status-cycle.ts から import) 5ceb30f
5(画面・メニュー・i18n) 388172f
3(データ層。fixtures と Prisma、2段階で読む) e56b734
4-1(YAML) 75a0115
4(ルート・テスト) 9bdb8e2
6(docs/fixtures.md §13) f02eb1d

13-2. 確認(監督。f02eb1d 時点)

  • npx tsc --noEmit -p . 0件 / npm run lint 0件 / vitest ★119ファイル・2,790件 通過★(着手前 111・2,601)/ USE_FIXTURES=true npm run build 成功(/errors・/api/errors・/api/errors/[error_id]/status が出る)
  • 段階7-7 の grep: status: { in: [ と select 定数の robot_id / robot は ★0件★。$queryRaw|$executeRaw は既存のコメント3か所(「使わない」と書いたもの)だけ
  • ★fixtures の API(当方の fixtures 由来)★: 本番ビルドを next start -p 3100、2026-10-06 16:0x JST。 既定 ★10件★・include_resolved=true で ★11件★/並びは検知時刻の降順(先頭は errored_at が NULL の FX-ERR-07=created_at 10/6 06:40)/ error_type=secur で FX-ERR-08 の1件/include_resolved=yes は422/FX-ERR-08 はシリアル「0」・種類「SECURITY_ON」/ PATCH で 対応中 → 解消 → 未解消 の日時の入り方と消え方が §3(AT §3-4)どおり/大小文字違い・存在しない id は404/日本語の備考が UTF-8 で往復一致//prechecks は200
  • ★画面(当方の fixtures 由来)★: Edge ヘッドレスで撮影。メニューは「🔔 アラート」の直後に「🚨 エラー一覧」(EN "🚨 Errors")/10行/ 予定の表示(事前チェック(10分前)・(30分前)・予定なし・予定不明・本清掃)/状態ボタンを押すと対応中になり、読み直しても保たれる/ ★JP/EN で表の列幅は動かない★/1440px で横スクロールなし
  • ★ローカルDB(dwatch_local。当方のシード由来)★: next start を環境変数でローカルに向けて起動、2026-10-06 16:10 JST。 ★49行★(本清掃・事前チェック30/60・予定なし・予定不明がすべて出る)。RER-1008-main(本清掃に紐づく)で 対応中 → 解消 → 未解消・備考を入れて空に。 db.write は route が /api/errors/[error_id]/status・columns は変えた列だけ・備考の中身なし(comment_changed のみ)。最後は元の値に戻した。 ★段階7-6 の列単位 GRANT のローカル専用ユーザーでの確認は、していない★(.env.local に書き込み用アカウントの設定が無い)

13-3. ステージング(段階8。先方投入データ)

  • ユーザーの dev サーバー :3000 経由、2026-10-06 16:11 JST。前後の値は mysql の SELECT だけで読んだ(★UPDATE を手で打っていない★)
  • 一覧: ★16行すべて本清掃・未解消★。★シリアル「0」の行6行(SECURITY_ON 5・NOT_STARTED 1)も落ちずに読めた★(robot_id が NULL の行。§8-5)
  • 書いた行は ★1000013(SECURITY_ON・シリアル0・本清掃)の1行だけ★: 対応中 → 解消 → 未解消・備考を入れて空に。★権限エラーなし★
  • ★4列は操作前と完全に一致★(status 0・comment / responded_at / resolved_at は NULL)。updated_at は 15:09:52 → 16:11:10(DB が更新。AT §10 #8 で受け入れ済み)

13-4. 実装担当の判断(本書と違う・本書が決めていなかったところ)

# 判断 理由・場所
1 ★include_resolved は小文字の true / false だけ★(TRUE も422) 黙って倒さない(表2 F3 の延長)。d-watch-api.yaml にも明記
2 GET のルートテストは db.facilityNameIndex()・facilityGroupIndex() を vi.spyOn で差し替えた ★vitest でモックした @/lib/db/prisma を同時に動的 import すると、片方に本物が返る罠★(テスト環境だけ)。★/clean-logs の Prisma 経路テストで施設名と分類を同時に試すと同じ罠を踏む★
3 ページの切り出しと行の組み立ては lib/db/index.ts の非公開2関数(errorReportPageIds() / errorReportItemsInPageOrder())で fixtures と Prisma が共有 3-4 の「規則を2箇所に書かない」
4 updateErrorReportColumns() は updatePrecheckErrorColumns() と同じ3行を ★共通化せず並べた★ updatePrecheckErrorColumns() の挙動を変えないため(D12 と同じ考え方)
5 fixtures の error_type は LowBattery 等のダミー 3-2 の表の例以外は実データ未受領(docs/fixtures.md §13)
6 画面は eslint-disable を足さず、一覧の結果を {query, items, total, error} で持ち、読み込み中を result.query !== queryString で判定 4-19。effect の中で同期的に setState しない(react-hooks/set-state-in-effect の抑止コメントを足さずに済ませる。ErrorsScreen.tsx:230-231)
7 列幅は §5-3 の目安から調整(★表の最小幅 1156px★) ErrorsScreen.module.css。固定幅は守っている(4-17)
8 期間の検証は dateRangeError(from, to, Infinity) 上限を設けない(§11 Q13)。lib/clean-logs/detail-log-date-range.ts を流用
9 一覧の取得失敗の文言を t() に通した 4-18

13-5. 残したもの・副作用

  • ★副作用: i18n に「備考」→ "Note" を足したので、D-Secログ(DsLogScreen.tsx の t("備考"))の備考列の見出しが EN で "NOTE" になる★(これまでは EN でも「備考」のまま)。直していない(誤訳ではない)
  • ★ツールバーの並びは言語で変わる★: JP では「解消済みも表示」が2行目に回り、EN では1行に収まる(★表の列幅は動かない★)。直していない
  • ★375px★ はサイドバーが畳まれない既知の作り(指示書AV §13-4)で、右側が切れる。直していない(段階7-5「崩れていたら直さずに報告」)
  • ★「解消済みも表示」の性能★(保持期間ぶんのキーを読む。D8 の理由)— §10 のとおりリリース前に見直す
  • 新しい文言の EN(§5-5)は暫定(ユーザーが後でまとめて見直す)
  • 運用上の知見: ★Bash ツールはヒアドキュメントの中でも \\ を \ に縮める★(スクリプトに正規表現の \S 等を書くときは Write ツールで書くか、バックスラッシュを使わない)