Newer
Older
client_dwatch / docs / handoff / 指示書P_ローカルDBで動かす第2歩_DSec.md

指示書P — ローカルDBで動かす第2歩(D-Sec の dsDevices / dsAlerts(disconnected) を実DB経由にする)

項目 内容
実行環境 ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照)
実行状態 ★実行済み★(D-097。段階0〜5すべて完了。origin/develop へ push 済み。着手前確認1件はA案でユーザー回答済みのうえで着手した。着手途中で dwatch_local のスキーマ再同期(指示書Q・D-096)を挟んでいる)
実行順 ★指示書M(D-086)の続き★。★指示書M が作った scripts/seed-local-db.ts / lib/db/prisma.ts / lib/db/local-guard.ts / lib/db/use-fixtures.ts の上に積む作業★です。★指示書M が実装した prismaSource.facilities() / schedules() は1文字も変えません★。★触るコードが重なる未実行の指示書はありません★(未実行は ★指示書I(lib/auth/ / proxy.ts)/指示書J(lib/settings/)★ の2本 — ★着手直前に自分で確認すること★)が、★docs/decisions.md / docs/handoff.md は共通で触ります★。★同時に走らせないこと★
取得する決定番号 ★着手直前に grep -n "^### D-0" docs/decisions.md で最終番号を確認すること★。★作成時点の最終は D-090(docs/decisions.md:3325)なので、最初の空き番号は D-091 の見込み★。★2026-09-07 に D-068 の二重採番が実際に起きています(D-070 冒頭)。2026-09-09 にも D-089 の取り合いが起きかけました(D-090 冒頭)。必ず自分で確認してください★
前提 ★D-090★(docs/decisions.md:3325。本指示書の設計根拠。★未決20件の一覧★) / ★D-086★(:2814。第1歩の実行記録。★Edge Runtime バンドル破壊の地雷を含む★) / ★D-065★(:474。Q1-1 備考 :482 / Q1-2 controls.status :483 / Q1-3 control_error_histories.status :484) / ★D-067 項1★(:604。★2系統の取得元は完全に別★) / ★D-070 D-3★(:1133。controls.status を読むだけでよい。訂正節は :1161) / D-042(:226。書き込みは4カラムのみ) / D-050(:308。通ってからコミット) / D-044(:230。fixtures で開発する) / D-056(:353。schema.prisma を手で書き換えない)
段階数 ★6段階★(段階0〜段階5)
着手前の基準 ★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★(作成時点の参考値は D-089 の「31ファイル / 610テスト / tsc 0 / lint 0」だが、★鵜呑みにしない★)
作成 2026-09-09(作成時点の決定事項最終番号 D-090 / docs/handoff.md の最新は 追記25(docs/handoff.md:2061))

実行後はこの表と、文書末尾の「実行状態」表を更新してください。

★この指示書を読んでいるあなたへ★ .claude/commands/d-watch.md に「実装は Cursor 側、Claude Code は指示書作成まで」とありますが、 これは2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 この指示書を渡されたなら、調査や改訂ではなく実装を行ってください。

★★これは「第2歩」です。うまくいかなければ途中で止めて構いません。★★ ★.env.local の USE_FIXTURES を true に戻すだけで、 完全に元の状態(fixtures で全画面が動く状態)に戻ります★。 ★DB に書いたデータも dwatch_local の中にしかありません。★


★★冒頭に読むべきこと — なぜ security_off を含めないのか★★

★dsAlerts() は disconnected だけを返します。security_off は返しません。★

★注記(2026-09-09 追記。D-091 項3)★: ★ユーザー判断により、security_off は そもそもアラートから外す方向で内部調整中です★(「セキュリティが無効であっても異常ではない」)。 ★したがって本指示書の「disconnected のみ」という範囲は、結果的に最終形に近い可能性があります★。 ★ただし確定ではないので、本指示書の方針(第3歩で security_off を扱う)はそのままにしておきます★。

DsAlert(lib/types/index.ts:312-330)が要求する ★id / device_serial / resolution_status / responded_at / resolved_at / memo の6フィールド★に ★対応するカラムが security_statuses(prisma/schema.prisma:362-377)にも security_status_histories(:379-392)にも存在しません★(D-090 項3-9)。 両テーブルが持つのは id / facility_id / section_id / status / reported_at / created_at だけです。

さらに ★security_statuses.reported_at は @updatedAt★(prisma/schema.prisma:368)なので、 ★detected_at(「いつからセキュリティが解除されたままか」)が取れません★(D-090 項3-10)。

★この6つを埋めるには推測が要ります。それは CLAUDE.md §7「仕様が不明な箇所を推測で実装すること」の 禁止に真正面から当たります。★

一方 disconnected 側は ★取得元が完全に確定しています★:

種別 取得元 根拠
★disconnected★ ★control_error_histories★(制御盤異常のエラー) ★D-067 項1(docs/decisions.md:604)★
security_off security_statuses(状態から導出。エラーではない) 同上。★この2つを同じテーブルから取ろうとしないこと★

★★帰結 — 静かに件数が減ります★★

★USE_FIXTURES=false にすると、以下が起こります。これは想定内であり、バグではありません★:

画面 変化 理由
★/ds-alerts★ ★12件 → 5件 に減る★ ★security_off 7件が返らなくなる★
★/dashboard のセキュリティバナー★ ★出なくなる(空配列)★ ★computeSecurityBanner()(lib/dashboard/aggregate.ts:113-125)が alert_type === "security_off" で絞っているため★

★★「静かに減る」ので、段階4 の確認項目に件数の突き合わせを必ず入れてあります。飛ばさないでください。★★ ★差がちょうど7件であることを数えること★(fixtures の内訳は security_off 7件 / disconnected 5件 — lib/fixtures/ds-alerts.ts を実測して確かめること)。

★security_off は第3歩で扱います★(D-090 項2 の表)。


★★着手前にユーザーへ確認すること(1件)★★

★施設12(フレッシュタウン湖畔店)の DS-012 をどう扱うか★

事実(★すべて現物で裏を取ってから使うこと★):

  • 施設12 は ★ロボット0台★(lib/fixtures/facilities.ts:213 の robots: []) → ★シードは「ロボット1台につき section を1件」作る★(scripts/seed-local-db.ts:207-224)ので ★施設12 には sections が0件★になる
  • 施設12 は ★d_security_enabled: false★(lib/fixtures/facilities.ts:212)
  • しかし fixtures には ★DS-012(lib/fixtures/ds-devices.ts:109-115)と 対応するアラート(lib/fixtures/ds-alerts.ts:140-152。id: 10)★ がある
  • ★controls.section_id は NOT NULL の sections への FK★ (prisma/schema.prisma:182 / :193)。★そのままではシードできず、FK 違反で止まる★

選択肢:

案 内容 帰結
★A(推奨)★ ★DS-012 とそのアラートをシードから除外する★ ★指示書M で施設12 のスケジュールを除外したのと同じ扱い★(D-086 / scripts/seed-local-db.ts:128-149)。★d_security_enabled: false の施設が D-Sec デバイスを持つのは、そもそも fixtures 側の矛盾★。★/ds-logs が 12台 → 11台 になります★(異常5→4)
B 施設12 に sections を1件作る(ロボットは無いまま) ★「1清掃エリア = 1ロボット」(CLAUDE.md §8)の前提に反する section が DB に生まれる★
C DS-012 を別の施設に付け替える ★fixtures と DB で施設が変わる。段階4 の突き合わせが読みにくくなる★

★A を採る場合、/ds-logs の台数が 12 → 11 に変わります。段階4 の期待値を書き換えること。★ ★回答を得るまで段階1 に入らないこと★(CLAUDE.md §0-1「仕様が不明なら止まる」)。


この指示書の位置づけ

★どこまでやるか — 残り10メソッド中2つだけ★

lib/db/index.ts の prismaSource(:149-257)は、指示書M で facilities()(:162-198)と schedules()(:199-238)の2本が実装済みです。★残り10本は notImplemented() のまま★。

本指示書で実装するのは ★そのうち2本だけ★:

メソッド 現在地 使われる画面
★dsDevices()★ lib/db/index.ts:156-158 ★F-06 D-Secログ★(/ds-logs)。app/api/ds-devices/route.ts:42 / app/api/ds-devices/[serial_number]/note/route.ts
★dsAlerts()★(★disconnected のみ★) lib/db/index.ts:159-161 ★F-07 D-Secアラート★(/ds-alerts)+ ★F-01 ダッシュボード★。app/api/ds-alerts/route.ts:43 / app/api/dashboard/route.ts:59

★この2本を選んだ理由★(D-090 項2):

  • ★取得元が D-065 / D-067 / D-070 で確定している★(唯一、推測なしで書ける組み合わせ)
  • ★judge も集計も連続日数も不要★ — 変換だけで済む
  • ★影響画面が F-06 / F-07 に閉じており、失敗しても被害が小さい★

★今回の成果になるもの★

★fixtures の施設名の不整合が、DB 経由にすることで構造的に消えます★(D-090 項4):

# fixtures の値 施設マスタの実名 該当箇所
1 ★「メガバリューマート緑野第二店」★(facility_id: 12) ★フレッシュタウン湖畔店★(lib/fixtures/facilities.ts:204) lib/fixtures/ds-devices.ts:111 / lib/fixtures/ds-alerts.ts:145
2 ★「バリューマート城南店」★(facility_id: 10) ★フレッシュタウン大和店★(lib/fixtures/facilities.ts:169) lib/fixtures/ds-devices.ts:103 / lib/fixtures/ds-alerts.ts:132

★#1 は既知(D-086 項1 / 項15)、#2 は D-090 で初めて記録された新発見★です。

★なぜ DB にすると消えるのか★: controls / control_reports / control_error_histories は いずれも facilities への FK を持つ(prisma/schema.prisma:192 / :333 / :353)ので、 ★施設名は結合で取ることになり、手書きの文字列を入れる余地が構造的に無くなる★からです。 ★段階1 で facility_name をシードに渡さないこと(後述 1-6)が、この成果の前提です。★


★調査済みの事実(★現物で裏を取ってから使うこと★)★

1. ★対象テーブルの列(prisma/schema.prisma の実測)★

テーブル model 行 ★deleted_at★ 備考
controls Control :174-198 ★ある★(:188) serial_number(:176)/ facility_id(:180)/ ★section_id NOT NULL FK★(:182, :193)/ status(:183)/ inactive(:184)
control_reports ControlReport :323-340 ★無い★ control_id(:327)/ serial_number(:328)/ ★reported_at★(:329)
control_error_histories ControlErrorHistory :342-360 ★無い★ control_id(:346)/ ★status★(:347)/ ★comment★(:348)/ ★occurred_at★(:349)/ responded_at(:350)/ resolved_at(:351)。★created_at / updated_at も無い★
sections Section :142-172 ある(:151) ★section_name★(:145)

★deleted_at が無いテーブルに where: { deleted_at: null } を書くと型エラーになります(=安全側)★。 ★型エラーが出たら「このテーブルには論理削除が無い」というサインです。無理に回避しないこと。★

2. ★コード値の変換表(★これが実装の中身そのものです★)★

変換元 変換先 根拠
★controls.status = 0★ ★DsDevice.connection_status = "ok"★ ★D-065 Q1-2(docs/decisions.md:483)★
★controls.status = 1★ ★DsDevice.connection_status = "error"★ 同上。★3状態目は存在しない★(lib/types/index.ts:284-290)
★control_error_histories.status = 0★ ★DsAlert.resolution_status = "pending"★ ★D-065 Q1-3(docs/decisions.md:484)★
★= 1★ ★"in_progress"★ 同上
★= 2★ ★"done"★ 同上

★判定の再現はしないこと★: 通信断のしきい値(link_schedules.intervals)から自分で 異常を計算し直す必要はありません。★TeamWorks 側のサーバーが判定結果を controls.status に 格納するので、DFA は読むだけです★(D-070「★通信断判定についての訂正★」。docs/decisions.md:1161)。

3. ★挿入順序(外部キーの依存)★

★既存のシード(scripts/seed-local-db.ts:152-158)が入れている7テーブルの後に足します★:

(既存)groups → customers → devices → facilities → sections → robots → clean_schedules
(追加)        → controls → control_reports → control_error_histories

★削除は挿入の逆順★(scripts/seed-local-db.ts:151-158 と同じ流儀)。

4. ★備考(note)はメインDBに無い★

★DsDevice.note に対応するカラムは controls にありません★(D-065 Q1-1。docs/decisions.md:482)。 ★実値は app/api/ds-devices/route.ts:75-78 が JSON ストア(data/settings/ds-device-notes.json)から マージします★。★prismaSource 側は何もしません(常に null を返す)★。


★禁止事項(この指示書の全段階に効きます)★

# ★してはいけないこと★ 理由
1 ★★security_off 側を推測で実装すること★★ ★対応カラムが存在しない★(D-090 項3-9 / 3-10)。★CLAUDE.md §7「仕様が不明な箇所を推測で実装すること」の禁止★。★件数が減るのは想定内。埋めようとしないこと★
2 ★★メインDB(TeamWorks 側)に接続・書き込みすること★★ CLAUDE.md §2 / §7 / D-042。★作業は dwatch_local の中だけで完結します★。★接続先ホスト名は未受領なので、そもそも繋がりません★
3 ★dwatch_ddl に書き込むこと★ ★参照専用★(受領した実DDL の姿を保つため)
4 ★prisma migrate / prisma db push / prisma db pull を使うこと★ ★CLAUDE.md §7 / D-056 / D-059★。★dwatch_local には余分な4列があり db push は DROP しようとします★(D-086 項3)。★テーブルは既にあるので不要★
5 ★prisma/schema.prisma を手で書き換えること★ ★D-056★。★db pull の生成物が正★
6 ★lib/db/prisma.ts / lib/db/local-guard.ts / lib/db/use-fixtures.ts を変更すること★ ★指示書M で完成しており、そのまま効きます★。★特に use-fixtures.ts の切り出しは Edge Runtime バンドル破壊の修正そのものです★(D-086。553b3cc)。★戻すと /api/reports/* が404になります★
7 ★prismaSource.facilities() / schedules() を変更すること★ ★指示書M の成果物★。★本作業は「その隣に2本足す」だけです★
8 ★lib/ds-devices/query.ts / lib/ds-alerts/query.ts / API ルート2本 / 画面2枚を変更すること★ ★本作業はデータ層の差し替えです★。★これらが1文字も変わらないまま画面が実DBで動くことが、変換が正しいことの証明になります★
9 ★lib/db/ にフィルタ・ページング・ソートを持ち込むこと★ ★lib/db/index.ts:59-63 の設計方針★(「フィルタや集計はこの層では行わず、APIルート側で組み立てます」)。★DataSource は引数を取らない契約(:74-82)★
10 ★inactive で絞り込むこと★ ★第1歩と同じ方針(D-086 / lib/db/index.ts:165-167)★。★inactive は「無効化」であって削除ではなく、画面が見せ分ける可能性がある。仕様が未確定なので絞らない側に倒す★
11 ★lib/fixtures/ を削除・改変すること★ ★CLAUDE.md §9.5★(DB接続後も残す)。★施設名の不整合も「直さない」— DB 経由で消えることを実測するのが今回の成果です★
12 ★テストを DB 依存にすること★ ★遅く・不安定になり、DB が空の環境で落ちます★。★テストは fixtures と純粋関数だけで書く★
13 ★シードに実在の住所・電話番号・社名・人名を入れること★ CLAUDE.md §7 / D-043。★fixtures の架空値をそのまま使えば安全★
14 ★git add -A を使うこと★ CLAUDE.md §7。★過去に別作業の途中ファイルを巻き込む事故が起きています。必ずファイルパスを明示★
15 ★.env.local をコミットすること★ CLAUDE.md §9.3。★git add でパスを明示すれば入ってしまいます★。★確認後は USE_FIXTURES=true に戻すこと★
16 ★DB のユーザー名・パスワードを git 管理下の文書やコードに書くこと★ CLAUDE.md §9.3。★.env.local にのみ置く★

作業の順序(★崩さないこと★)

段階 内容 コミット
段階0 前提の確認(★読むだけ★) コミットなし
段階1 シードに D-Sec の3テーブルを足す 段階1で1コミット
段階2 prismaSource.dsDevices() を実装する 段階2で1コミット
段階3 prismaSource.dsAlerts()(disconnected のみ)を実装する 段階3で1コミット
段階4 ★動作確認(ここが本題)★ コミットなし
段階5 記録と push 段階5で1コミット + push

★段階ごとに npx tsc --noEmit / npm run lint / npm test を実行し、通ってからコミットすること★ (D-050。docs/decisions.md:308)。


段階0 — 前提の確認(★読むだけ・コミットなし★)

読むもの

# 対象 見る点
1 ★D-065 Q1-2 / Q1-3★(docs/decisions.md:483-484) ★controls.status は 0/1 の2値★ / ★control_error_histories.status は 0/1/2★
2 ★D-067 項1★(docs/decisions.md:604) ★2系統の取得元は完全に別★。★同じテーブルから取ろうとしないこと★
3 ★D-070 D-3★(docs/decisions.md:1133)+ ★訂正節★(:1161) ★controls.status を読むだけでよい★。★intervals から判定を再現する必要はない★
4 ★D-090★(docs/decisions.md:3325) ★本指示書の設計根拠と未決20件★
5 ★指示書M の facilities() / schedules()★(docs/handoff/指示書M_ローカルDBで動かす第1歩.md:529-720) ★雛形として読む★。★遅延 import・変換の書き方・テストの作り方をそのまま踏襲する★
6 ★lib/db/prisma.ts★(全40行) ★シングルトン・★遅延 import が必須★な理由(:4-8)★
7 ★lib/db/local-guard.ts★(全31行) ★dwatch_local 以外を弾く仕組み。★そのまま効きます★★
8 ★scripts/seed-local-db.ts★(全276行) ★冪等の作り(:152-158)★ / ★スキップの作法(:128-149)★ / ★id を明示する方針(:14-16)★
9 ★D-086 の「段階5で発見した想定外の不具合」★(docs/decisions.md:2912-2930) ★Edge Runtime バンドル破壊★。★404 が出たらこれを疑う★

★接続先の確認(★これを飛ばさないこと★)★

★.env.local の DATABASE_URL が dwatch_local を指していることを確認してください。★ ★値そのものを画面・ログ・文書に出さないこと★(CLAUDE.md §9.3)。 ★確認は「ローカルを向いているか」だけで十分です★(lib/db/local-guard.ts:10-20 と同じ判定)。

段階0の完了条件

  • 上記9点を読んだ
  • ★着手前確認(施設12 の DS-012)の回答を得た★
  • ★着手前の基準値(npm test のファイル数・テスト数 / tsc / lint)を自分で実測して控えた★

段階1 — シードに D-Sec の3テーブルを足す

1-1. 作るもの

★scripts/seed-local-db.ts に追記する★(新規ファイルを作らない)。 ★ガード(assertLocalDwatchDatabaseUrl。:40)はそのまま効きます★。

★新たにシードするテーブル(挿入順)★: controls → control_reports → control_error_histories

1-2. ★controls★

  • ★lib/fixtures/ds-devices.ts の dsDevices を import して変換する★
  • ★section_id は sections に実在する id を渡すこと★(prisma/schema.prisma:182 / :193 の NOT NULL FK)
    • ★既存シードは sections.id をロボットの id と同じにしている★(scripts/seed-local-db.ts:217-224)
    • ★施設ごとに section を引ける対応表を作ること★(★facility_id → その施設の section の1つ★)
  • ★★施設12(フレッシュタウン湖畔店)は sections が0件なので、DS-012 をそのままシードすると FK 違反で止まります★★ → ★着手前確認の回答に従うこと★
    • ★A案を採る場合は、既存のスケジュール除外(scripts/seed-local-db.ts:128-149)と同じ書き方にすること★: ★facility_id: 12 を名指ししない★。★「section が存在しない施設のデバイス」を構造的に弾き、 スキップ件数が想定と違ったら例外で止める★
  • ★status は connection_status から逆変換★("ok" → 0 / "error" → 1)
  • ★password は NOT NULL(prisma/schema.prisma:177)★ — ★fixtures に無いのでダミー値を入れる★。 ★実在しそうな文字列を入れないこと★(CLAUDE.md §7 / D-043)
  • ★id は明示する★(scripts/seed-local-db.ts:14-16 の方針。AUTO_INCREMENT に任せない)

1-3. ★control_reports★

  • ★DsDevice.last_reported_at が reported_at の元になります★
  • ★★デバイスごとに「最新1件」が正しく引けるデータを入れること★★
    • ★1件だけ入れるのでは「最新を選ぶロジック」が検証できません★。 ★少なくとも数台については、より古い行をもう1件足すこと★
    • ★古い行の reported_at は last_reported_at より確実に前にすること★
  • ★facility_id / section_id / control_id / serial_number はすべて NOT NULL★ (prisma/schema.prisma:325-328)

1-4. ★control_error_histories★

  • ★lib/fixtures/ds-alerts.ts の dsAlerts のうち ★alert_type === "disconnected" の行だけ★ を変換する★
    • ★security_off の行はシードしない★(DB に置き場所が無い。禁止事項1)
    • ★スキップ件数を数えて出力すること★(★fixtures の security_off の件数と一致するはず★ — 自分で数えること)
  • ★status は resolution_status から逆変換★("pending" → 0 / "in_progress" → 1 / "done" → 2)
  • ★comment ← memo★ / ★occurred_at ← detected_at★ / responded_at / resolved_at はそのまま
  • ★control_id は serial_number から引くこと★(device_serial → controls.id の対応表)

1-5. ★冪等にすること★

★deleteMany を挿入の逆順で先頭に足す★(scripts/seed-local-db.ts:151-158 の既存ブロックに、 ★D-Sec の3本を先頭側(= clean_schedules より前)に★ 追加する):

control_error_histories → control_reports → controls → (既存)clean_schedules → robots → sections → ...

★2回続けて実行しても結果が同じであることを実際に確かめること★。

1-6. ★★facility_name はシードに渡さないこと★★

★fixtures の facility_name(ds-devices.ts / ds-alerts.ts の文字列)を使ってはいけません。★ ★シードに渡すのは facility_id だけ★です。施設名は ★段階2 / 段階3 で facilities を結合して取ります★。

★これが「城南店」「緑野第二店」の不整合が構造的に消える仕組みそのものです★(D-090 項4)。

★同様に section_name(ds-alerts.ts のダミー値。lib/fixtures/ds-alerts.ts:15-17 に 「暫定のダミー値」と明記)も渡しません。★sections.section_name を結合で取ります★。

1-7. ★検算★

★シード後にテーブル件数を数えること★。

# ★mysql.exe は PATH に無い。フルパスで呼ぶこと★
# ★接続アカウントは `.env.local` の DATABASE_URL に書いてあるものを使う★
#   (★この文書にユーザー名・パスワードを書かないこと★。CLAUDE.md §9.3)
SELECT (SELECT COUNT(*) FROM controls) c,
       (SELECT COUNT(*) FROM control_reports) cr,
       (SELECT COUNT(*) FROM control_error_histories) ceh;

★期待値は自分で fixtures を数えて立てること★(★作成時点の参考: controls は fixtures の デバイス数から施設12 の分(A案なら1件)を引いた数 / control_error_histories は disconnected の件数★ — ★鵜呑みにせず実測すること★)。

段階1の完了条件

  • ★2回続けて実行しても結果が同じであること★
  • ★スキップされたデバイス/アラートの件数と内容が出力されていること★
  • ★ガードが効くこと★(★DATABASE_URL を一時的に別の値にして中断を1回だけ確かめ、必ず元に戻す★)
  • ★prisma/schema.prisma に差分が無いこと★(git diff --stat prisma/ が空)
  • ★lib/fixtures/ に差分が無いこと★(git diff --stat lib/fixtures/ が空)
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★ここで1コミット★

段階2 — prismaSource.dsDevices() を実装する

2-1. 作るもの

★lib/db/index.ts:156-158 の notImplemented("dsDevices") を実装に置き換える★。 ★facilities()(:162-198)と同じ形にすること★(遅延 import → findMany → map で変換)。

2-2. ★結合★

controls + control_reports(★最新1件★)+ facilities

  • ★facilities は facility_name を取るため★(★fixtures の手書き文字列は使わない★)
  • ★control_reports は last_reported_at を取るため★。 ★デバイスごとに reported_at が最大の1件を選ぶこと★ (Prisma の include で orderBy + take: 1 を使うのが素直。★段階1-3 で古い行を入れたのは ここを検証するためです★)

2-3. ★変換★

出力(DsDevice。lib/types/index.ts:295-302) 入力
serial_number controls.serial_number
facility_id controls.facility_id(★Number() で変換。BigInt のまま返さない★ — facilities() の :180 と同じ)
★facility_name★ ★facilities.facility_name(結合)★
connection_status ★controls.status 0 → "ok" / 1 → "error"★(D-065 Q1-2)
last_reported_at control_reports.reported_at(最新1件)。★ISO 8601(JST, +09:00)★(CLAUDE.md §9.3)
★note★ ★常に null★(下記 2-4)

2-4. ★note は常に null を返すこと★

★実値は app/api/ds-devices/route.ts:75-78 が JSON ストアからマージします★(D-065 Q1-1)。 ★prismaSource 側では何もしません。★ ★理由をコードコメントに残すこと★(後任が「実装漏れ」と誤解して埋めにいかないように)。

2-5. ★deleted_at の扱い★

  • ★controls には deleted_at があります★(prisma/schema.prisma:188)→ ★where: { deleted_at: null } で絞る★ (facilities() の lib/db/index.ts:169 と同じ)
  • ★control_reports には deleted_at がありません★ → ★書くと型エラーになります(=安全側)★。 ★型エラーが出たら「このテーブルには無い」というサインなので、無理に回避しないこと★

2-6. ★inactive では絞らないこと★

★禁止事項10★。第1歩と同じ方針(D-086 / lib/db/index.ts:165-167)。 ★理由をコメントに残すこと★。

2-7. ★テスト★

★DB 依存のテストは書かないこと★(禁止事項12)。 ★変換の中に純粋関数として切り出せる部分(コード値 → 文字列)があれば、そこにテストを書く★。 ★期待値表を先に作ること★(CLAUDE.md §9.4)。

段階2の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★lib/ds-devices/query.ts / app/api/ds-devices/ / 画面に差分が無いこと★(禁止事項8)
  • ★ここで1コミット★

段階3 — prismaSource.dsAlerts() を実装する(★disconnected のみ★)

3-1. 作るもの

★lib/db/index.ts:159-161 の notImplemented("dsAlerts") を実装に置き換える★。

3-2. ★結合★

control_error_histories + controls + sections + facilities

  • ★controls は device_serial を取るため★
  • ★sections は section_name を取るため★(★fixtures ではダミー値だった。 lib/fixtures/ds-alerts.ts:15-17★)
  • ★facilities は facility_name を取るため★

3-3. ★変換★

出力(DsAlert。lib/types/index.ts:312-330) 入力
id control_error_histories.id(★Number() で変換★)
★alert_type★ ★固定で "disconnected"★(control_error_histories は制御盤異常しか持たない。D-067 項1)
device_serial controls.serial_number(結合)
facility_id control_error_histories.facility_id(★Number()★)
★facility_name★ ★facilities.facility_name(結合)★
★section_name★ ★sections.section_name(結合)★
detected_at control_error_histories.occurred_at(★ISO 8601 / +09:00★)
★resolution_status★ ★status 0 → "pending" / 1 → "in_progress" / 2 → "done"★(D-065 Q1-3)
responded_at / resolved_at 同名カラム(nullable のまま)
★memo★ ★control_error_histories.comment★(★書き込み許可4カラムの1つ。D-042★)

3-4. ★★security_off を返さないことを docstring に明記すること★★

★メソッドの直上に、次の3点をコメントで残すこと★:

  1. ★このメソッドは disconnected しか返さないこと★
  2. ★理由: security_statuses / security_status_histories に id / device_serial / resolution_status / responded_at / resolved_at / memo に 対応するカラムが無い(D-090 項3-9)。detected_at も @updatedAt のため取れない(同 3-10)★
  3. ★帰結: /ds-alerts の件数が減り、/dashboard のセキュリティバナーが出なくなる。 これは意図した挙動であり、実装漏れではない。第3歩で扱う★

★★「実装漏れ」ではなく「意図的な未実装」だと後任が一目で分かるようにすること★★。

3-5. ★deleted_at / inactive★

  • ★control_error_histories には deleted_at がありません★(prisma/schema.prisma:342-360)→ ★絞らない★
  • ★controls の deleted_at は結合側で絞ってよい★
  • ★inactive では絞らない★(禁止事項10)

段階3の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★docstring に「security_off を返さない」と理由が書かれていること★
  • ★lib/ds-alerts/query.ts / app/api/ds-alerts/ / lib/dashboard/ / 画面に差分が無いこと★(禁止事項8)
  • ★ここで1コミット★

段階4 — ★★動作確認(ここが本題)★★

★ここまでの3段階は準備です。この段階で「本当に DB から出ているのか」を確かめます。★

4-1. 起動

  • ★.env.local の USE_FIXTURES を false にする★ (★lib/db/use-fixtures.ts は process.env.USE_FIXTURES !== "false" なので、 ★文字列 false 以外はすべて fixtures 扱い★です。FALSE や 0 では切り替わりません)
  • ★npm run dev を Bash で起動しないこと。Browser pane の preview_start を使う★ (★.claude/launch.json に d-watch(npm run dev / port 3000)があります★ — ★着手時に確認すること★)
  • ★Browser pane が黒画面 / 0×0 になる既知の問題★: ★preview_start に url を渡して開き直す★ (指示書O :183 の前例)
  • ★環境変数を変えたら dev サーバを再起動し、反映されていることを確認すること★

4-2. ★★確認1 — /ds-logs(F-06)★★

# 項目
1 ★台数が期待どおりであること★ — ★fixtures は 12台(正常7 / 異常5)★。★A案(施設12 除外)を採った場合は 11台(正常7 / 異常4)★。★着手前確認の回答に応じて期待値を確定させること★
2 ★★施設名が「バリューマート城南店」ではなく「フレッシュタウン大和店」で出ること★★(DS-011 / 施設10) ← ★これが今回の成果★
3 ★★施設12 を残した場合、「メガバリューマート緑野第二店」ではなく「フレッシュタウン湖畔店」で出ること★★(DS-012)
4 ★last_reported_at が正しい(最新の control_reports の値である)こと★ — ★段階1-3 で古い行を足したデバイスで、古いほうが出ていないことを確認する★
5 ★備考が消えていないこと★ — ★JSON ストアに保存済みの値があれば、それが表示されること★(app/api/ds-devices/route.ts:75-78 のマージが効いている証拠)
6 ★ソートが「異常 → 正常」であること★(サーバ側ソート。lib/ds-devices/query.ts)

4-3. ★★確認2 — /ds-alerts(F-07)★★ ← ★件数の突き合わせを飛ばさないこと★

# 項目
1 ★★件数を数えること★★ — ★fixtures は 12件(security_off 7 + disconnected 5)★、★DB 経由では 5件★(disconnected のみ)。★A案で DS-012 を除外しても、DS-012 のアラートは security_off なので disconnected の5件は変わらない★ — ★自分で fixtures を数えて確かめること★
2 ★★差がちょうど7件(= security_off の件数)であること★★。★これ以外の差が出たら変換のバグです★
3 ★section_name が実データで出ること★ — ★fixtures のダミー値(「1F売り場」「2F衣料品」「1Fレジ前」「B1食品」)ではなく、シードが入れた sections.section_name が出ること★
4 ★施設名が施設マスタの実名で出ること★(確認1-2 と同じ趣旨)
5 ★resolution_status の3値がすべて出ること★(pending / in_progress / done — ★fixtures の disconnected には done が無い(lib/fixtures/ds-alerts.ts:182-185 に「意図的に作っていない」と明記)ので、done が0件なのが正しい★)
6 ★memo が comment から出ていること★

4-4. ★★確認3 — /dashboard★★

# 項目
1 ★★セキュリティバナーが出なくなること★★(★security_off が返らないため。lib/dashboard/aggregate.ts:113-125★)。★これは想定内。記録すること★
2 ★ただし cleanLogs が未実装なので /api/dashboard 自体が 500 になるはず★(app/api/dashboard/route.ts:57 が db.cleanLogs() を呼ぶ)。★バナーの確認は 500 の内側なので目視できない可能性が高い★ → ★その場合は「確認できなかった」と正直に記録すること★

4-5. ★★確認4 — 他画面が落ちることを「確認して記録する」★★

★これは失敗ではありません。第2歩の想定どおりの状態です。★

画面 呼んでいる未実装メソッド 期待
/dashboard cleanLogs / prechecks ★500★
/detail-log cleanLogs ★500★
/alerts prechecks / cleanLogs ★500★
/reports reportKpis / facilityPerformance / continuousProblems / errorBreakdown ★500★
★/facilities / /schedules★ ★(実装済み。指示書M)★ ★200。回帰していないこと★
★/ds-logs / /ds-alerts★ ★(本作業で実装)★ ★200★
  • ★エラーメッセージが [db] xxx のPrisma実装はまだありません。USE_FIXTURES=true で開発してください。 であること★(lib/db/index.ts:143-147)
  • ★★404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと★★ (D-086 の「段階5で発見した想定外の不具合」。docs/decisions.md:2912-2930)。 ★500 と 404 は意味が違います★ — ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★

4-6. ★USE_FIXTURES=true に戻す★

  • ★.env.local を true に戻し、dev サーバを再起動する★
  • ★全画面が従来どおり動くことを確認する★(★特に /ds-logs 12台 / /ds-alerts 12件 / /dashboard のセキュリティバナーが出ること★)
  • ★これを確認して初めて「壊していない」と言えます★

4-7. ★ログの確認★

  • ★read_console_messages でブラウザ側のエラー0件★
  • ★preview_logs でサーバ側のエラーを確認★(★A Node.js module is loaded が出ていないこと★ ← 4-5 の404対策)

段階5 — 記録と push

docs/decisions.md に ★D-091★ を追記

★着手直前に grep -n "^### D-0" docs/decisions.md で最終番号を確認すること★。 ★D-089 は指示書O が、D-090 は本指示書の設計根拠が既に取得済みです★。★空き番号は自分で確かめること★。

★記録すること★:

  1. ★実装した2メソッドと、それぞれの結合・変換★
  2. ★★security_off を含めなかった理由★★(対応カラムが無い。D-090 項3-9 / 3-10)と、 ★その帰結(/ds-alerts が5件になり、/dashboard のバナーが出なくなる)★
  3. ★★fixtures の不整合が構造的に消えたことの実測★★ — ★「城南店」→「フレッシュタウン大和店」/「緑野第二店」→「フレッシュタウン湖畔店」が 実際に画面で確認できたか★。★section_name がダミー値でなくなったか★
  4. ★★施設12 の DS-012 をどう扱ったか★★(着手前確認の回答と、その結果の台数)
  5. ★段階4 で確認できなかった項目があれば、その理由★(★ブラウザが使えなかった場合の代替手段と限界を明記★。 指示書N・指示書O の前例)
  6. ★第3歩に残したもの★(dsAlerts の security_off / cleanLogs ほか。D-090 項2 の表を引くこと)
  7. ★実測値★(npm test のファイル数・テスト数 / tsc / lint。★着手前の基準値との差★)

docs/handoff.md に追記

★最新は 追記25(docs/handoff.md:2061)★。次は ★追記26★ の見込み。★着手直前に自分で確認すること★。

push

  • ★git add は必ずファイルパスを明示。git add -A は禁止★(禁止事項14)
  • ★.env.local をコミットしないこと★(禁止事項15)。★USE_FIXTURES=true に戻したことを確認してから★
  • git push origin develop(★main へは push しない★)

段階5の完了条件

  1. docs/decisions.md に決定番号を追記した(★上記7項目すべて記録★)
  2. docs/handoff.md に追記した
  3. ★origin/develop へ push 済み★
  4. ★文書末尾の「実行状態」表を更新した★

★やらないこと(第3歩以降)★

★本指示書の範囲外です。手を出さないこと★(D-090 項2 の分割表):

対象 いつ
★dsAlerts の security_off 側★ 第3歩
cleanLogs 第3歩
prechecks 第4歩
reportKpis / facilityPerformance / errorBreakdown 第4歩
continuousProblems 第5歩
★点検記録2本(inspections / inspectionDetail)★ ★最後★(D-072 項7。docs/decisions.md:1052)

★D-090 項3 の未決20件も本指示書では解決しません★。★「未決である」と分かっている状態を保つのが正しい★。


完了条件(全体)

  1. ★着手前確認1件(施設12 の DS-012)の回答を得てから段階1 に入った★
  2. 段階0〜5 をすべて実施したこと(★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★)
  3. npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  4. ★USE_FIXTURES=false で /ds-logs と /ds-alerts が 200 で表示されること★
  5. ★★施設名が施設マスタの実名(フレッシュタウン大和店 / フレッシュタウン湖畔店)で出ること★★
  6. ★★/ds-alerts の件数が fixtures より7件(= security_off の件数)少ないことを実測で確認したこと★★
  7. ★section_name が実データ(sections.section_name)で出ること★
  8. ★dsAlerts() の docstring に「security_off を返さない」と理由が書かれていること★
  9. ★USE_FIXTURES=true に戻して全画面が従来どおり動くこと★
  10. ★prisma/schema.prisma / lib/fixtures/ / lib/ds-devices/query.ts / lib/ds-alerts/query.ts / API ルート / 画面 に差分が無いこと★
  11. ★lib/db/prisma.ts / lib/db/local-guard.ts / lib/db/use-fixtures.ts / prismaSource.facilities() / prismaSource.schedules() に差分が無いこと★
  12. docs/decisions.md / docs/handoff.md の更新
  13. ★origin/develop へ push 済み★
  14. 文書末尾の「実行状態」表を更新した

実行状態

段階 状態 コミット
★着手前の確認★ ★回答済み(A案。施設12のDS-012をシードから除外)★ —
段階0 前提の確認 完了 コミットなし
段階1 シードに D-Sec の3テーブルを足す 完了 9b39847
段階2 prismaSource.dsDevices() 完了 7811572
段階3 prismaSource.dsAlerts()(disconnected のみ) 完了 f05b522
段階4 動作確認 完了(Browser pane無し。curlで代替。限界は D-097 §5 参照) コミットなし
段階5 記録と push 完了 本コミット

実行日: 2026-09-09 取得した決定番号: D-097(docs/decisions.md:4223)。途中の寄り道で D-095(dwatch_local作り直し)・ D-096(指示書Q。prisma/schema.prisma再同期)も取得 着手前の実測値: 33ファイル / 631テスト、tsc 0、lint 0 施設12 の DS-012 の扱い: A案採用(シードから除外)。ユーザー事前回答どおり /ds-logs の台数: 12台 → 11台(正常7 / 異常4) /ds-alerts の件数(fixtures / DB): 12件 → 5件(disconnectedのみ。差7件 = security_off件数と一致) 完了後の実測値: 33ファイル / 631テスト(変化なし)、tsc 0、lint 0 詳細: docs/decisions.md D-097 を参照