robot_error_reports の4カラム UPDATE)日付: 2026-10-02 / 担当: Cursor / 前提: D-042 / D-104 / D-105 / D-152 / D-153 項3 / D-155 / CLAUDE.md §3・§6-1・§7・§9.3 / TeamWorks 第10回回答 N-5・N-6・N-14(docs/handoff/TeamWorks回答_第10回_20261002.md)
★渡す順番: 指示書AR → 指示書AS → 本書AT を一括で渡す★(D-153 項3 / D-155 項6)。 ★分割 PR の作成とマージは、AR と本書の実装が終わってから daichi がレビューして行う★(D-154 項4)。
★★監督の確認は済み(2026-10-02。D-156)★★ — §10 の8項目はすべて推奨どおりで承認。本文のまま実施すること。 (以下は作成時の注記)本書の §3 で ★【要確認】★ と付けた決定は、 推奨案を本文に書き込んである。★承認されればそのまま渡せる★。却下された項目だけ本文を差し替えること。 一覧は §10。★決まったら
docs/decisions.mdに D 番号で記録する(本書の作業者は書かない)★。
/alerts の★事前チェック由来のアラート★について、★対応状況(未解消→対応中→解消)と備考★を画面から更新できるようにする。 書き込み先は ★robot_error_reports の status / comment / responded_at / resolved_at の4カラムだけ★。
| # | 事実 | 場所 |
|---|---|---|
| 1 | ★API 契約はある★。PATCH /prechecks/{id}/status(id は string、status は unresolved/in_progress/resolved、memo は最大400文字)。200 は更新後の PrecheckItem、ほかは 404 / 422 |
d-watch-api.yaml:659-690 |
| 2 | ★実装は無い★。app/api/prechecks/ には route.ts(GET)しか無く、その冒頭に「PATCH は今回作らない」と書いてある |
app/api/prechecks/route.ts:7 |
| 3 | PrecheckItem.id は ★事前チェック由来=robot_error_reports.error_id★、★出発失敗=本清掃行の clean_timetables.id の文字列★ |
lib/db/index.ts:1306 / :1371、d-watch-api.yaml:1771-1773 |
| 4 | ★出発失敗(launch_failed)には robot_error_reports の行が無い★ → ★書き込み先が無い★。resolution_status は unresolved 固定・日時と備考は null 固定 |
D-104 / D-105 申し送り、lib/db/index.ts:1386-1395 |
| 5 | robot_error_reports に ★主キーが無い★。一意なのは ★error_id(varchar(50))だけ★ |
prisma/schema.prisma:307-312 |
| 6 | ★罠★: RobotErrorReport.updated_at に ★@updatedAt★ が付いている |
prisma/schema.prisma:336 |
| 7 | status の 0/1/2 ↔ enum の変換は両方向とも既にある(逆変換は seed 用として作ったもの) |
lib/db/precheck-mapping.ts:32(DB→API)/ :48(API→DB) |
| 8 | ★日時を書くときは toDbJst() を通す★(アダプタは getUTC* で文字列を作るため)。docstring に「4カラムを実装するときは必ず通せ」とある |
lib/format/date.ts:70-83 |
| 9 | 書き込み監査ログの型 DbWriteLogEvent(event: "db.write")は ★定義だけあって一度も出していない★。★record_id: number★ |
lib/logger/events.ts:205-222、設計は docs/logging-design.md:311-339 |
| 10 | ★生SQL は ESLint で禁止★。どうしても要るときはタグ付きテンプレートだけを個別に disable し、理由を decisions に書く、というルール | eslint.config.mjs:41-70(D-122) |
| 11 | 画面は ★バッジを表示するだけ★(クリックできない) | components/features/alerts/AlertsScreen.tsx:295-312 |
| 12 | モックでは ★D-Sec アラートだけ★ がクリックで状態を回す(未対応→対応中→対応済み→未対応)。★事前チェックのアラートには操作が無い★ | d-watch-mock.jsx:191-201 / :961(D-Sec)、:640-645(事前チェック。バッジのみ) |
| 13 | 書き込みの見本(PATCH ルート・画面の備考編集)は既にある。★保存先はメインDBではなく DFA の JSON ストア★ | app/api/ds-devices/[serial_number]/note/route.ts、components/features/ds-logs/DsLogScreen.tsx:245-293 |
| 14 | DB 接続は1アカウント分の環境変数しか無い(DATABASE_URL または DATABASE_HOST/PORT/USER/PASSWORD/NAME) |
lib/db/prisma.ts:38-70、.env.example |
| 15 | ★ステージングへの4カラム UPDATE はユーザーが許可済み★(実データの対応状況が書き換わることを承知のうえ)。★それ以外の列への書き込みは禁止★ | CLAUDE.md §3 の接続先注記 / D-152 |
| 16 | ★ステージングのデータは TeamWorks が入れ替えることがある(時期未定)★。★0行のテーブル(clean_timetables など)には 10/5 中に何らかのデータが入る★ |
第10回 N-6 / D-155 項5 |
| 17 | ★robot_error_reports はエラー全般の表になる予定★(robot_id / serial_number を NULL 可にする DDL 変更待ち。両方 NULL=セキュリティ、robot_id だけ NULL=D-S制御盤) |
第10回 N-5 / D-155 項5 |
| 18 | DB の既定タイムゾーンは 'Asia/Tokyo' に変更済み(先方申告。★当方の再実測は未了★) | 第10回 N-14 / D-155 項4 |
| やる | やらない |
|---|---|
★robot_error_reports の4カラムの UPDATE★(/alerts の事前チェック由来の行だけ) |
★control_error_histories(D-Sec アラート)への書き込み★ — N-5 で D-Sec アラートの出どころが robot_error_reports に移る可能性があるため、★いまは作らない★(PATCH /ds-alerts/{id}/status は契約のまま残す) |
PATCH /prechecks/{id}/status の実装(fixtures / Prisma の両方) |
出発失敗(launch_failed)の対応状況の記録(書き込み先が無い。D-105 申し送りのまま) |
/alerts の画面に「状態の切り替え」と「備考の編集」を足す |
完了率・清掃結果・予定外タスクのコード(★指示書AS の範囲★) |
書き込み監査ログ(db.write)を初めて出す |
4カラム以外の列への書き込み(★権限も無い★。CLAUDE.md §7) |
| 書き込み用の接続を別アカウントにできる器 | スキーマの変更(★§3-1 の @updatedAt 1箇所だけ例外★) |
@updatedAt の罠 — RobotErrorReport.updated_at から @updatedAt を外す★【要確認】何が起きるか(★実測★。出所: 構造 — 当方が Prisma 7.10.0 の生成クライアントに★偽のドライバアダプタ★をつないで、送られる SQL を記録した。★DB には一切接続していない★)
いまのスキーマのまま updateMany で status / comment / responded_at だけを渡すと、Prisma は次の SQL を送る:
UPDATE `robot_error_reports` SET `status` = ?, `comment` = ?, `responded_at` = ?, `updated_at` = ? WHERE `robot_error_reports`.`error_id` = ? -- updated_at の引数は "2026-10-02T11:35:11.700Z"(=アプリの時計の UTC)
update(1件更新)でも同じく updated_at = ? が入る(さらに前後に SELECT とトランザクションが付く)。
| 問題 | 中身 |
|---|---|
| ★5列目の書き込み★ | CLAUDE.md §7 違反。★更新用アカウントは列単位の GRANT★(D-042)なので ★権限エラーで落ちる★ |
| ★値の時刻もずれる★ | アダプタは getUTC* で文字列を作る(node_modules/@prisma/adapter-mariadb/dist/index.js の formatDateTime())ので、★JST の DB に UTC の壁時計値(9時間前)が入る★ |
| ★そもそも不要★ | DDL に ON UPDATE current_timestamp() がある(第10回 N-14 c の回答でも確認)ので、★DB が自分で更新する★ |
検討した案
| 案 | 内容 | 判断 |
|---|---|---|
| ★(a)★ | ★schema.prisma:336 から @updatedAt だけを外す★ |
★採用★ |
| (b) | $executeRaw のタグ付きテンプレートで4列だけの UPDATE を書く |
不採用。ESLint のルール上は個別 disable が可能(eslint.config.mjs:41-50)で、SQL も MySQL 互換の素の UPDATE なので TeamWorks への通知も要らない。だが ★「アプリに生SQLは1件も無い」状態(CLAUDE.md §3)が崩れる★、★列名・型の検査が効かなくなる★、★日時の書式を自前で扱う★ことになる。(a) で足りるので選ばない |
| (c) | updateMany を使えば @updatedAt は付かないのでは |
★不採用(実測で否定)★。上のとおり updateMany でも updated_at = ? が入る |
| (d) | 書き込み専用に別のスキーマ/生成クライアントを持つ | 不採用。生成が2系統になり CI の prisma generate も2本要る。スキーマが2つに割れて食い違う入口が増える |
| (e) | $extends のクエリ拡張で updated_at を引数から抜く |
不採用。updated_at は呼び出し側の引数ではなく、★クエリコンパイラ(wasm)が後から足している★ので抜けない |
(a) を採る理由
SET に渡した列しか出ないことを確かめた★(出所: 構造 — スキーマの複製から生成したクライアント+偽アダプタ。リポジトリのファイルは変更していない):
UPDATE `robot_error_reports` SET `status` = ?, `comment` = ?, `responded_at` = ? WHERE `robot_error_reports`.`error_id` = ?(★
control_error_histories は updated_at 列そのものが無い★ので、もともと SET status = ? だけになる。将来 D-Sec 側を作るときにこの罠は無い)@updatedAt は ★DB 由来の事実ではなく Prisma 固有の属性★で、★db pull は出力しない★。 ★このプロジェクトは db pull のたびに @updatedAt を手で付け外ししている★(D-059 運用 docs/decisions.md:399、指示書Q 2-3 — 実測で HEAD 24箇所 → pull 後 20箇所)。 つまり ★「db pull 後に手で戻す」作業は既に毎回ある★。今回はそこに ★「robot_error_reports.updated_at だけは付けない」という例外を1つ足す★だけnpx prisma generate → npm test で赤くなる★弱点(承知のうえで採る): db pull の手順書・D-059 の運用ルールに「例外1件」が増える。★記録は監督が D 番号で書く★(本書の作業者は docs/decisions.md に触らない)。
updateMany(1文の UPDATE)で、書き込み用の接続から行う★【要確認】getPrisma()(参照用)で行う★getWritePrisma()(新設)で、robotErrorReport.updateMany({ where: { error_id }, data }) の1文だけ★update(1件更新)を使わない理由★: Prisma の update は ★SELECT → UPDATE → 全列の SELECT をトランザクションで包む★(実測)。 ★更新用アカウントに SELECT 権限があるかは分かっていない★(CLAUDE.md §3 は「4カラムのみ UPDATE 可」とだけ書いている)。 ★updateMany なら必要な権限は「4列の UPDATE」+「WHERE で読む error_id の SELECT」だけ★(MySQL は WHERE で読む列に SELECT 権限を要求する)updateMany の戻り値(件数)で存在を判定しない★ — 存在確認は書く前に参照用で行う。件数が 0 なら「確認と書き込みの間に行が消えた」(N-6 の入れ替え)とみなして 404 にする (mariadb ドライバの既定 foundRows: true で「一致した行数」が返る。値が変わらなくても 1。ただしこの既定に依存した分岐は書かない)環境変数(★値は絶対に書かない・コミットしない★)
| 変数 | 意味 |
|---|---|
DATABASE_WRITE_USER |
書き込み用アカウントのユーザー名 |
DATABASE_WRITE_PASSWORD |
同パスワード |
DATABASE_HOST / DATABASE_PORT / DATABASE_NAME を使う★(DATABASE_WRITE_* を使うときはこの3つが必須。URL 形式は設けない — %40 が壊れる問題(lib/db/prisma.ts:29-30)を持ち込まないため)getPrisma() と同じクライアント)で書く★ — ★いまはこれで動く★(ユーザー: 今のアカウントに書き込み権限がある見込み)lib/db/prisma.ts に getWritePrisma() を足す。★globalThis.__dwatchPrismaWrite にキャッシュ★(getPrisma() と同じ理由)。★lib/db/index.ts の先頭で静的 import しない★(lib/db/prisma.ts:3-8 の理由)scripts/check-env.ts が使う lib/config/env-check.ts に検査を足す: ★未設定=warn「書き込みは参照用アカウントで行います」★ / ★片方だけ=error★。★値は出さない★.env.example に2つのキーを★コメントアウト・値なし★で足す/alerts に事前チェック異常として出る行」だけ。それ以外は 404★【要確認】PATCH /prechecks/{id}/status の {id} を robot_error_reports.error_id として引き、★次をすべて満たすときだけ書く★。
error_id = {id} の行があるclean_timetable_id が null でないclean_timetables 行があり、★precheckMinutesFromCode(precheck) が null でない(=事前チェック行)★parent_id が指す本清掃行がある(無ければ prechecks() と同じく★例外★=500。lib/db/index.ts:1290-1301)★1〜3 のどれかを満たさなければ 404★。これは prismaSource.prechecks() が事前チェック行を作る条件(lib/db/index.ts:1265-1285)と同じ。★条件を自前で書き直さず、同じ関数(precheckMinutesFromCode())を使う★。
clean_timetables.id の文字列)は、error_id として引いても当たらない → 404★。★別の分岐を作らない★ (404 のメッセージは「対応状況を記録できるアラートが見つかりません(出発失敗には記録先がありません)」)clean_timetable_id が入らない見込み)も ★2 で 404 になる★ — ★/prechecks から D-Sec 側の行を書き換えられない★。これは意図した性質date_from/date_to)は見ない★(一覧の絞り込みであって、行の性質ではない)lib/fixtures/prechecks.ts の id が一致し、かつ severity === "precheck"」で同じ判定にする★純粋関数★ planPrecheckStatusUpdate(current, request, now) に閉じ込める(新設 lib/prechecks/status-update.ts)。 入力は★API の値★(resolution_status / responded_at / resolved_at は「真の瞬間」の Date か null / note)、 出力は★変える列だけ★を持つパッチ(キーが無い列は書かない)。★toDbJst() は出力をDBに書く直前に1回だけ通す★(この関数の中では通さない)。
不変条件(★この規則で保たれる形★。d-watch-api.yaml:1795-1804 の「対応開始日時(対応中以降で設定)」に合わせた): 未解消 → 両方 null / 対応中 → responded_at のみ / 解消 → 両方あり
now = 2026-10-02 08:15 JST、t1 = 07:00、t2 = 07:30 とする。
| # | 現在 | responded_at |
resolved_at |
要求 | 出力パッチ |
|---|---|---|---|---|---|
| 1 | 未解消 | null | null | 対応中 | status=対応中, responded_at=now |
| 2 | 未解消 | null | null | 解消 | status=解消, responded_at=now, resolved_at=now |
| 3 | 未解消 | null | null | 未解消 | ★空(書かない)★ |
| 4 | 対応中 | t1 | null | 解消 | status=解消, resolved_at=now(★responded_at は変えない★) |
| 5 | 対応中 | ★null★(不正データ) | null | 解消 | status=解消, responded_at=now, resolved_at=now |
| 6 | 対応中 | t1 | null | 対応中 | ★空★ |
| 7 | 対応中 | t1 | null | 未解消 | status=未解消, responded_at=null |
| 8 | 解消 | t1 | t2 | 未解消 | status=未解消, responded_at=null, resolved_at=null(★戻すと両方消す★) |
| 9 | 解消 | t1 | t2 | 対応中 | status=対応中, resolved_at=null(responded_at は変えない) |
| 10 | 解消 | t1 | t2 | 解消 | ★空★(★resolved_at を now で上書きしない★) |
| 11 | 未解消 | ★t1★(先方が入れた等) | null | 対応中 | status=対応中(★既にある responded_at は上書きしない★) |
| 12 | 解消 | t1 | ★null★(不正データ) | 解消 | resolved_at=now |
SELECT … FOR UPDATE を書くには生SQLが要る)note に揃える(memo をやめる)★【要確認】memo、レスポンス(PrecheckItem)が note★(d-watch-api.yaml:682 / :1805)。★同じ DB 列(comment)に2つの名前★があるnote に揃える★。CLAUDE.md §6-1 のとおり ★先に YAML を直す★。理由: 型・fixtures・画面・D-S制御盤の備考 API(/ds-devices/{serial_number}/note)がすべて note。★まだ誰も memo を送っていないので互換の問題は無い★/ds-alerts/{id}/status の memo は変えない★(あちらはレスポンスの DsAlert.memo とも揃っていて食い違いが無い。範囲外)/ds-devices/{serial_number}/note の作法に揃える)| 入力 | 扱い |
|---|---|
note が無い |
★comment は変えない★(状態だけの変更) |
| 文字列以外(null・数値など) | 422 |
| ★401文字以上(トリム前で数える)★ | 422(DEVICE_NOTE_MAX_LENGTH と同じ理由。app/api/ds-devices/[serial_number]/note/route.ts:76-87) |
| 空文字・空白だけ | ★comment = null★(削除) |
| それ以外 | ★前後の空白を除いて保存★ |
| トリム後が現在値と同じ | ★comment は書かない★ |
lib/config/index.ts に PRECHECK_NOTE_MAX_LENGTH = 400 として置く★(ハードコード禁止。YAML の maxLength: 400 と一致させる)status は必須(YAML どおり)。★備考だけ変えるときも画面は現在の status を送る★(#3・#6・#10 の「空」になり、日時は動かない)updated_at・error_type など)が来ても無視する★。★パッチを作るのは planPrecheckStatusUpdate() だけ★なので、本文のキーが DB に届く経路を作らない(テストで固定する)db.write)docs/logging-design.md §6(:311-339)の設計どおり、★成功・失敗の両方で1件ずつ出す★。
DbWriteLogEvent.record_id を number → string に変える★(lib/logger/events.ts:210。error_id は varchar。★Number() を通さない★ — D-105 項2 と同じ理由)。★このイベントはまだ一度も出していないので、壊れる利用者はいない★table: "robot_error_reports" / record_id(=error_id)/ columns(★実際に SET した列だけ★)/ status_old・status_new(API の enum 文字列)/ responded_at_old/new・resolved_at_old/new(JST の ISO 文字列か null)/ ★comment_changed: boolean★ / success / 失敗時の db_error_codecomment / note / memo は禁止キー。docs/logging-design.md:159-172)。★監督の依頼文は「4列の旧値・新値」だが、備考は boolean だけにする★db_error_code は ★Prisma のエラーコードとドライバのエラー番号(例: 列の権限エラー 1143)だけ★。★エラーメッセージは載せない★(DB のユーザー名が入る)docs/logging-design.md §6 の表に「record_id は文字列(robot_error_reports は error_id)」と1行足す/alerts)— 最小限モックの事前チェック画面には操作が無い(§1 #12)。★D-Sec アラートのモックの「クリックで状態を回すボタン」★と、★D-Secログの「備考のインライン編集」★(既に実装済み)を★そのまま借りる★。新しい部品は作らない。【要確認: モックに無い操作を足すこと】
severity === "precheck" の行だけ★:
AlertsScreen.tsx:295-312)を★ボタンにする★。クリックで ★未解消 → 対応中 → 解消 → 未解消★(モック :191-201 と同じ順)。表示は今のバッジと同じ(解消は「解消 (日時)」)t("クリックして備考を編集") を薄く出す)。★DsLogScreen.tsx:245-293 の作法をそのまま★: textarea・onBlur で確定・変えていなければ送らない・失敗時は編集状態のまま残す・maxLength は PRECHECK_NOTE_MAX_LENGTHlaunch_failed の行は今のまま(バッジのみ・編集不可)★PrecheckItem でその1行だけ差し替える★(一覧を取り直さない。DsLogScreen.tsx:252-256 の理由と同じ)。 ★絞り込み(未解消のみ)に合わなくなっても、次に取り直すまで行を消さない★(押し間違いをその場で戻せるように)applyResolutionChange(summary, before, after)★(lib/prechecks/query.ts に追加)で差分だけ反映するlib/i18n.ts): ★"未解消"・"対応中"・"解消" の対訳を足す★(D-105 で「キーが無い」と申し送られていたもの。足せば既存のバッジも英語になる)。 新規は "対応状況の更新に失敗しました": "Failed to update the status" だけ。備考まわりは既存キー(クリックして備考を編集 / 備考を入力 / 保存中… / 保存に失敗しました)を使う。★それ以外の未訳キーには手を出さない★★各段階で npx tsc --noEmit -p . / npx eslint . / npm test が通ってからコミットする★(D-050)。
develop で作業する★(.claude/worktrees/ の中ではない。ワークツリーは古い main 基点)。git pull で最新にするgit log で確かめる(本書は AR → AS → AT の順で渡している)npx prisma generate のあと npm test の件数を記録する(以後の増分の基準)components/features/detail-log/CleanLogTable.tsx)があっても触らない・コミットに含めない★node_modules/next/dist/docs/ の Route Handler の項を読む(CLAUDE.md 末尾)。★既存の app/api/ds-devices/[serial_number]/note/route.ts の形(params は Promise)に合わせる★d-watch-api.yaml を先に直す★(:659-690):
memo → ★note★(§3-5)description を足す: 書き込むのは robot_error_reports の4列だけ / §3-4 の日時の規則(不変条件と「戻すと両方消す」)/ §3-6 の備考の規則 / ★出発失敗と事前チェック以外の行は 404★ / 何も変わらない要求は書かずに現在の値を返す404 の説明に「launch_failed の id もここ」を書くlib/types/index.ts に ★PrecheckStatusUpdateRequest★({ status: PrecheckStatus; note?: string })を足すlib/config/index.ts に ★PRECHECK_NOTE_MAX_LENGTH = 400★ と、パスの error_id の長さ上限 ★PATH_ERROR_ID_MAX_LENGTH = 50★(varchar(50)。PATH_SERIAL_NUMBER_MAX_LENGTH と同じ作法)を足すlib/api/path-params.ts に ★parsePathErrorId()★ を足す(★空文字と51文字以上だけ 422、文字種は絞らない★。parsePathSerialNumber() :66 と同じ作法。テストも同じ形で)@updatedAt を外し、ガードテストで固定する(§3-1)prisma/schema.prisma:336 の ★@updatedAt だけ★を消す。@default(now()) と @db.Timestamp(0) は残す。★他のモデルの @updatedAt は1つも触らない★// ★@updatedAt を付けないこと(DDL に ON UPDATE current_timestamp() はある)★ // アプリがこの表の4カラム(status/comment/responded_at/resolved_at)を UPDATE するため。 // 付けると Prisma が updated_at = <アプリの時刻(UTC)> を SET に足し、 // ①列単位 GRANT の更新用アカウントでは権限エラー ②JST の DB に UTC の値が入る。 // DB が ON UPDATE で自分で更新する。db pull 後に @updatedAt を付け直す作業の★唯一の例外★。 // lib/db/__tests__/robot-error-write-sql.test.ts が固定している(指示書AT §3-1)
:29-33 付近の「db pull 後に移植し直すこと」)にも1行足す★: 「RobotErrorReport.updated_at には @updatedAt を付けない(指示書AT)」npx prisma generatelib/db/__tests__/robot-error-write-sql.test.ts):
@/lib/generated/prisma/client)を、★SQL を記録するだけの偽アダプタ★でつなぐ(型は import type { SqlDriverAdapterFactory } from "@prisma/client/runtime/client"。any は使わない)。★DB には接続しない★SET の列が ★渡したパッチの列と完全に一致し、updated_at を含まない★。4列すべてを渡したときも同じnow = 08:15 JST を渡すと引数は "2026-10-02T08:15:00.000Z"。toDbJst() の付け忘れを検出する)tsx で行い、動くことを確認済み(§3-1)。vitest は environment: "node"(vitest.config :13)なので wasm のクエリコンパイラも動くはず。★動かなければ止まって報告★npx prisma generate のあとに npm test を走らせる★(.github/workflows/ci.yml)のでこのテストは CI でも動く@updatedAt を戻すと落ちることを一度だけ手元で確かめる★(戻して generate → 赤 → 外して generate → 緑)。★戻した状態はコミットしない★CLAUDE.md §6-2)lib/prechecks/__tests__/status-update.test.ts に ★§3-4 の12行と §3-6 の備考の表★をそのまま書く。★先にテストを書いて赤を確かめる★lib/prechecks/status-update.ts に planPrecheckStatusUpdate() を実装する。
{ status?: PrecheckStatus; note?: string | null; responded_at?: Date | null; resolved_at?: Date | null })now は引数で受け取る★(関数の中で new Date() しない)lib/prechecks/query.ts に applyResolutionChange() とテスト(未解消↔対応中は件数が動かない / 解消に入る・出るで unresolved と resolved が1ずつ動く / total は不変)prechecks() の行の組み立て(lib/db/index.ts:1303-1336)を純粋関数に切り出す★ — lib/db/precheck-mapping.ts に precheckItemFromErrorRow(report, precheckMinutes, mainRow)。 ★prechecks() もこの関数を使うように置き換える★(★同じ行の作り方を2箇所に書かない★)。 ★既存の app/api/prechecks/__tests__/route.test.ts が無修正で通ること★=挙動が変わっていない証拠DataSource(lib/db/index.ts:186)に2つ足す:
precheckItemById(id: string): Promise<PrecheckItem | null> — §3-3 の1〜4。★事前チェックでなければ null★updatePrecheckErrorColumns(errorId: string, patch): Promise<{ matched: number }> — ★パッチを DB の列に写して updateMany を1回★
status → robotErrorStatusFromResolutionStatus()(lib/db/precheck-mapping.ts:48。★docstring の「seed 用」を「seed とアプリの書き込み」に直す★)note → commentresponded_at / resolved_at → ★toDbJst()★(null はそのまま null)lib/db/robot-error-write.ts(新設)の関数1つに置き、クライアントを引数で受ける★(段階2 のガードテストが同じ関数を呼べるように)lib/fixtures/prechecks.ts は書き換えず、★モジュール内の上書き表(id → 4項目)★を持つ。fixtureSource.prechecks() と precheckItemById() は上書きを反映して返す。テスト用に resetFixturePrecheckOverrides() を出す。★プロセスを再起動すると消える★(それで良い。fixtures は開発・デモ用。D-045)lib/db/prisma.ts に getWritePrisma()(§3-2)app/api/prechecks/route.ts:7「PATCH は今回作らない」lib/format/date.ts:76-80「アプリからの DB 書き込みはまだ無い」lib/db/index.ts:1392「将来…書き込み先が無い」は ★事実なので残し★、「PATCH は 404 を返す(指示書AT §3-3)」を足すPATCH /prechecks/{id}/status新設 app/api/prechecks/[id]/status/route.ts。順序は ★形式 → 本文 → 実在 → 計画 → 書き込み → 読み直し★(app/api/ds-devices/[serial_number]/note/route.ts と同じ考え方)。
parsePathErrorId() → 失敗は 422status が enum 外 → 422 / note の型・長さ(§3-6)→ 422const now = new Date() を★1回だけ★作るdb.precheckItemById(id) → null なら 404。例外は 500(logApiError)planPrecheckStatusUpdate(before, request, now) → ★空なら書かずに 200 で before を返す★db.updatePrecheckErrorColumns(id, patch):
db.write(success: false)★ と api.error を出して 500matched === 0 → db.write(success: false, db_error_code: "no_matching_row")を出して 404db.precheckItemById(id) で読み直す → ★db.write(success: true)★ → 200 で返す(読み直しで null なら 404。ログは success のまま)withRequestId() で包む★(既存ルートと同じ)components/features/alerts/AlertsScreen.tsx(と .module.css)、lib/i18n.ts。★launch_failed の行の見た目が変わっていないこと★を目で確かめる。
lib/config/env-check.ts と lib/config/__tests__/env-check.test.ts(§3-2)。.env.example。★テストに「出力に値が含まれない」ことを足す★(既存の :210-222 と同じ形)。
dwatch_local)で確かめる.env.local を dwatch_local に向け、USE_FIXTURES=false。npm run seed:local(★lib/db/local-guard.ts がローカル以外を拒否する★)CREATE USER 'dwatch_write_local'@'localhost' IDENTIFIED BY '<手元で決めた値>'; GRANT SELECT (error_id) ON dwatch_local.robot_error_reports TO 'dwatch_write_local'@'localhost'; GRANT UPDATE (status, comment, responded_at, resolved_at) ON dwatch_local.robot_error_reports TO 'dwatch_write_local'@'localhost';
.env.local に DATABASE_WRITE_USER / DATABASE_WRITE_PASSWORD(ローカル専用の値)を入れる。 ★認証方式の違いで接続できなければ、root と同じ方式で作り直す(SELECT user, plugin FROM mysql.user)。それでも駄目なら 8-2 は飛ばして報告★npm run dev → /alerts で事前チェックの行を1件選び、★未解消 → 対応中 → 解消 → 未解消★、★備考を入れる → 空にする★を画面から行う。各回で:
db.write が success: true で出て、columns が実際に変えた列だけ・備考の中身が無い@updatedAt を戻した状態でも一度だけ試し、1143(updated_at への権限エラー)で落ちて db.write が success: false で出ることを確かめる★(§3-1 の実地の裏付け。★戻した状態はコミットしない★)mysql で ★その1行の updated_at を読むだけ★ — 操作の時刻(JST)に更新されていること(★DB 側の ON UPDATE が動いている★)curl 等で。認証 cookie が要る)着手の条件(すべて):
clean_timetables は 10-01 時点で0行。第10回 N-6 で「10/5 中に何らかのデータを入れる」。★/alerts に事前チェックの行が1件も出なければ検証できない★ので、そこで止める手順:
/alerts(全件)から ★事前チェックで、未解消・responded_at/resolved_at/備考がすべて空の行★を1件選ぶ。★無ければ止まって報告(勝手に別の状態の行を選ばない)★error_id と4列(画面・API の値)。★updated_at は読むだけ★(mysql で SELECT error_id, status, comment, responded_at, resolved_at, updated_at FROM robot_error_reports WHERE error_id = ?。★UPDATE / DELETE / INSERT 文を手で打たない★)updated_at だけは DB が更新するので戻らない★ — 値を記録して報告するupdated_at が操作の時刻の JST になっているか★を記録する(第10回 N-14「DB の既定を Asia/Tokyo にした」の再実測を兼ねる。★9時間ずれていても補正しない・報告だけ★)npm run seed:local をステージングに向けない★(テーブルを消して入れ直すので実データが消える)npx tsc --noEmit -p . — 0件npx eslint . — 0件(★eslint-disable を1つも足していないこと★)npm test — 全件通過(段階0 の件数から増えていること)npm run build(USE_FIXTURES=true)が通るgit diff --stat に CLAUDE.md / docs/decisions.md / control_error_histories を書く変更が無い★ことgrep -rn "\$executeRaw\|\$queryRaw" app lib components★ が0件のまま| ファイル | 中身 |
|---|---|
lib/prechecks/__tests__/status-update.test.ts(新) |
§3-4 の12行 / §3-6 の備考の表 / ★出力のキーが4つ以外を含まない★ / now 以外の時刻を使わない |
lib/prechecks/__tests__/query.test.ts |
applyResolutionChange() |
lib/db/__tests__/robot-error-write-sql.test.ts(新) |
★本物の生成クライアント+偽アダプタ★で、SET が渡した列だけ・updated_at を含まない / 日時が壁時計値 / ★4列以外のキーを渡すと例外★ |
lib/db/__tests__/precheck-mapping.test.ts |
precheckItemFromErrorRow()(既存の prechecks() の期待値と同じ行ができる) |
lib/api/__tests__/path-params.test.ts |
parsePathErrorId()(空 / 50文字 / 51文字 / 記号を含む) |
app/api/prechecks/[id]/status/__tests__/route.test.ts(新。getPrisma / getWritePrisma をスタブ。app/api/prechecks/__tests__/route.test.ts:153-170 の作法) |
200(各遷移)/ ★空パッチなら updateMany を呼ばない★ / ★updateMany の data のキーが4列のうちの該当分だけ★ / ★本文に updated_at・error_type を混ぜても data に出ない★ / 422(id・JSON・status・note の型・401文字)/ ★404: 行が無い・clean_timetable_id null・本清掃行を指すエラー・出発失敗の id★ / matched === 0 で 404 / 書き込み例外で 500 / ★db.write が成功・失敗で1件ずつ出て、record_id が文字列・備考の中身を含まない★ / ★responded_at に toDbJst() が通っている★ |
app/api/prechecks/[id]/status/__tests__/route.fixtures.test.ts(新) |
fixtures で PATCH → GET /prechecks に反映される / ★launch_failed の id("4"・"8")は 404★ |
lib/config/__tests__/env-check.test.ts |
書き込み用の変数: 未設定=warn / 片方だけ=error / 値が出力に出ない |
app/api/prechecks/__tests__/route.test.ts |
★無修正で通ること★(段階4-1 の切り出しで挙動が変わっていない) |
/alerts で事前チェックの行の状態と備考を変えられ、★再読み込みしても残っている★(ローカルDB)@updatedAt を戻すとガードテストが落ちる★ことを確かめてあるdb.write が成功・失敗とも出て、備考の中身・認証情報を含まないd-watch-api.yaml が実装と一致している(note、404 の説明、日時の規則)CLAUDE.md・docs/decisions.md★(記録は監督が書く)control_error_histories への書き込み★・PATCH /ds-alerts/{id}/status の実装・D-Sec アラート画面(§2)lib/judge/kpi.ts など)@updatedAt 1箇所とそのコメントだけ例外★。db pull は実行しない$executeRaw / $queryRaw)と eslint-disable の追加★npm run seed:local★・★ステージングで1行より多く書き込むこと★・★mysql からの手動の UPDATE / INSERT / DELETE★lib/fixtures/prechecks.ts の中身★(§9 の食い違いは申し送り)git add -A★ — ファイルパスを明示する段階ごとに分けてよい(例: 段階1 / 段階2 / 段階3 / 段階4〜5 / 段階6〜7 / 段階8〜9 の記録)。 ★段階2 のコミットメッセージには「なぜ @updatedAt を外すか(5列目の書き込み・権限・タイムゾーン)」と「ガードテストの場所」を書く★。
| # | 内容 | 次に動く契機 | |
|---|---|---|---|
| 1 | ★N-5 の DDL 変更(robot_id / serial_number を NULL 可)★のあと db pull すると、precheckItemFromErrorRow() で serial_number が `string \ |
nullになり tsc が止まる。★その時に扱いを決める★(事前チェックの行はロボットのエラーなので NULL は来ない見込み)。★db pull のあとRobotErrorReport.updated_atに@updatedAt` が付いていないこと★も確かめる(ガードテストが見張っている) |
TeamWorks の DDL 変更 |
| 2 | ★D-Sec アラートの対応状況(control_error_histories)★は、N-5 で D-Sec のエラーが robot_error_reports に入るなら書き込み先が変わる。★control_error_histories には updated_at 列が無いので §3-1 の罠は無い★(実測) |
N-5 の食い違いの確認(第11回) | |
| 3 | ★更新用アカウントに SELECT 権限(少なくとも error_id)があるか★ — 無いと updateMany の WHERE で権限エラーになる。★別アカウントを使い始める前に TeamWorks に確認★ |
第11回 / 書き込み用アカウントの発行時 | |
| 4 | lib/fixtures/prechecks.ts の launch_failed(id "4" / "8")が ★対応中・解消で備考あり★になっている。★実DB では常に未解消・備考なし(D-104)★と食い違う |
fixtures の見直し時 | |
| 5 | ★出発失敗の対応状況の記録★(D-105 の2案: DFA 独自ストア / TeamWorks にエラー行を作ってもらう)。N-5 で「エラー全般の表」になるなら後者が通りやすい | 監督の判断 | |
| 6 | 同時操作は後勝ち(§3-4)。問題が出たら WHERE status = <旧値> を足す楽観ロックを検討 |
運用で問題が出たとき |
| # | 項目 | 推奨(本文に書き込み済み) |
|---|---|---|
| 1 | §3-1 RobotErrorReport.updated_at から @updatedAt を外す(D-059 の運用ルールに例外を1つ足す) |
外す+ガードテスト |
| 2 | §3-2 書き込みは updateMany / 書き込み用の環境変数 DATABASE_WRITE_USER・DATABASE_WRITE_PASSWORD(未設定なら参照用で書く・片方だけなら例外) |
そのとおり |
| 3 | §3-3 /prechecks から書けるのは事前チェック異常の行だけ(出発失敗・本清掃行のエラー・予定 ID が NULL の行は 404) |
そのとおり |
| 4 | §3-4 日時の規則。特に ★戻すと responded_at / resolved_at を消す★・★解消済みを再度「解消」にしても日時を上書きしない★ |
そのとおり |
| 5 | §3-5 リクエストの memo を note に改名(YAML を先に直す) |
改名する |
| 6 | §3-8 モックに無い操作(状態ボタン・備考編集)を事前チェックの行に足す | D-Sec のモックと D-Secログの作法を借りて足す |
| 7 | 段階9 ステージング検証 — 今は接続できず事前チェック行も無い。★ローカル検証だけで AT を完了扱いにし、ステージングは条件が揃ってから★でよいか | よい(完了条件 7) |
| 8 | 段階9 で updated_at だけは元に戻らない(DB が更新する)ことを受け入れるか |
受け入れる(ユーザーは実データの書き換えを承知済み。D-152) |