Newer
Older
client_dwatch / docs / 変更点まとめ_20260914.md

D-Watch 変更点まとめ — モック・仕様書から実際に変えたこと

作成日: 2026-09-14(★最終更新 2026-09-17★) 対象: ブランチ develop 読み手: 非エンジニアを含むプロジェクト関係者 出典: docs/decisions.md(★D-001〜D-150★)、docs/handoff.md、TeamWorks 確認事項 第1〜9回、d-watch-mock.jsx、 docs/データの流れ_画面1-5_20260917.md、docs/データの流れ_画面6-10_20260917.md

★反映範囲(2026-09-17 更新)★

本文は ★D-001〜D-150 の全件★ を反映しています。 2026-09-14 の初版は D-001〜D-134 までで、★D-135〜D-150 の16件は第4章に追補★しました (初版の本文はそのまま残し、古くなった箇所には印を付けてあります)。 ★以前ここにあった「6件が未反映」という注記は解消済み★です。

★コミット数などの「規模の数字」は本文から外しました★ — main..develop の件数は日々変わるため、 必要なときは git log --oneline main..develop | wc -l で数えてください。

★この資料には「技術的な裏付け」が併記されています★(2026-09-17 追加)

各項目の下にある ★折りたたみ(「技術的な裏付け」)★ を開くと、 ★DBのどの列から来て / どの関数を通って / どんな型で / どの画面に / どんな値で出るか★ が ★file:line 付き★ で書いてあります。★読まなくても本文だけで意味が通ります★。

★付ける/付けないの方針★: ★数字・判定・データ形式が関わる項目にだけ付けています★。 ★見た目・文言・体制・手続きだけの項目には付けていません★(裏付けるべき数値やデータ形式が無いため)。

★もっと詳しく知りたいときは★、下の2つの文書に ★64本以上の「縦線」(DBの列 → 取得 → 変換 → 型 → 画面 → 値の例)★ が書いてあります。★どの項目がどの章に対応するかは第9章の対応表★を見てください。

文書 対象画面
docs/データの流れ_画面1-5_20260917.md ダッシュボード / 清掃詳細ログ / アラート / レポート / 清掃面積
docs/データの流れ_画面6-10_20260917.md D-Secログ / D-Secアラート / 施設ロボットマスタ / スケジュール / システム管理

★数字の出所について★(CLAUDE.md §6-6): 本資料に出てくる具体的な値には ★「シード由来」(当方が作ったサンプルデータ)/「構造由来」(型・設計図・設定値から読み取れる値)/ 「記録済み実測」★ のいずれかの印を付けています。★印の無い数字を「実測」とは呼びません★。 ★★本番DB(133.125.238.144)にいま入っている行は、1行残らず当方のダミーです★★ (D-145 / D-148)。★規模や実勢の根拠には使えません★。


この資料の読み方

D-Watch は「モック(画面デザイン案)」「要件定義書」「UI仕様書」「テーブル定義書(DBの設計図)」という 4種類の資料をもとに作り始めました。開発を進めるなかで、これらの資料どおりに作れなかった/作らない方が良かった 箇所が出てきています。この資料はその全件を、なぜそうしたかとセットでまとめたものです。

変更は勝手に行っていません。すべて docs/decisions.md に決定番号(D-001 など)を付けて記録してあり、 「なぜそう決めたか」を後から誰でも遡れるようにしています。表の右端の決定番号がその参照先です。


第1章 — 変更が起きた5つの理由

変更の中身はバラバラに見えますが、原因をたどると次の5つに集約されます。

# 理由 どういうことか 代表例
① 作り方そのものを変えた 外注前提の技術で書かれていた資料を、自社で作れる技術に置き換えた 開発言語の変更、サーバー種別の変更
② DBを作る会社(TeamWorks)の実物に合わせた 資料は「DFA側の想定」であり、実際のDBとは違っていた。集計を作るのは先方なので、こちらが合わせるしかない 週の数え方、率の小数化、集計値を再計算しない
③ 「推測で作らない」方針を貫いた 分からないものを想像で埋めると、エラーも出ないまま間違った情報が画面に出る。分からないものは分からないまま出す エラー種別を翻訳しない、バッテリー残量を空欄にする
④ 実際に動かして/実データに近づけて初めて分かった 手元の小さなテストデータでは絶対に再現しない不具合が複数見つかった 時刻の9時間ズレ、エラー率の分子が逆、画面を開くとサーバーが落ちる
⑤ 使いやすさ・安全性のために足した モックに無くても、実運用で必要になるものを追加した ページ送り、検索の表記ゆれ対応、連続日数の表示上限

③が繰り返し出てくる理由 このプロジェクトで最も警戒しているのは「静かに壊れる」ことです。 プログラムが停止するエラーは誰でも気づきますが、「エラーも出ない/テストも通る/でも画面の数字が間違っている」 という壊れ方は、翌朝これを見て現場に指示を出す運用担当が、そのまま誤った判断をしてしまいます。 契約フラグの誤読(全施設のメニューが消える)、エラー種別の推測変換(誤った故障原因を現場に伝える)などが この類型で、いずれも実装前に発見して潰しています。


第2章 — 特に影響の大きかった変更 10件

1. 開発言語と作る主体を丸ごと変えた(D-001 / D-003)

  • 変更前: TeamWorks が PHP / Laravel で実装し、さくらのレンタルサーバで動かす想定
  • 変更後: DFA が内製で Next.js(JavaScript系)を使い、さくらのクラウド仮想サーバで動かす
  • なぜ: 非エンジニア2名体制で2言語を管理するのは現実的でなく、AI支援の精度もこの構成が最も高いため。 またレンタルサーバは PHP 向けの共有環境で、Next.js が必要とする「ずっと起動し続けるプログラム」を動かせません。
  • 注意: 要件定義書には今も「PHP / Laravel」と書かれています。この記述は古いものとして扱ってください。

2. 画面の時刻が9時間ズレていた(D-114 / D-115)★最も危険だった不具合★

  • 症状: DBに入っている 02:00 が、画面には 11:00 と表示されていた
  • 原因: DBの日時を「世界標準時」とみなして読み、その上でさらに9時間足していた(二重のズレ)
  • なぜ表示崩れで済まないか: 深夜2時の清掃が「まだ来ていない未来の予定」と判定され、 翌朝8時に運用担当が画面を開くとその行が一覧から消えていました。 「夜間の清掃結果を翌朝確認する」という本プロダクトの運用そのものが成立しない状態でした。
  • なぜ今まで気づかなかったか: 開発用のテストデータが9時間引いた値で書かれており、 読み出し時の+9時間と往復で辻褄が合っていたためです。実データに繋いだ瞬間に全画面がズレるところでした。
技術的な裏付け 【DBの列】 clean_timetables.start_at DateTime @db.DateTime(0)(prisma/schema.prisma:589) 中身は「日本時間の壁時計値」(TeamWorks 第8回 A2。2026-09-11 受領) 【何が起きるか】 @prisma/adapter-mariadb は `new Date(`${value}Z`)` で ★UTC とみなす★(設定変更不可) → JS の Date は「真の瞬間より9時間先」を指す 【補正】 lib/format/date.ts:95-97 jstIsoFromDb(date: Date): string = toJstIso(fromDbJst(date)) lib/format/date.ts:67-69 fromDbJst(date: Date): Date = new Date(date.getTime() - DB_DATETIME_OFFSET_MINUTES * 60_000) lib/config/index.ts:193 DB_DATETIME_OFFSET_MINUTES = 540(9 × 60) lib/format/date.ts:37-46 toJstIso(date: Date): string → "YYYY-MM-DDTHH:mm:ss+09:00" ★この2段を1つの式に潰さないこと★(lib/format/date.ts:88-90)— 潰すと「DBのタイムゾーン」という設定値が式から消え、設定を変えても効かなくなる 【逆向き】 lib/db/date-range.ts:50-67 dbDateRange(from, to): { gte?: Date; lt?: Date } ★to は「その日を含む」ので翌日 00:00 未満(lt)にする★。 ★lte で 23:59:59 にしない★ — 桁が増えたとき 23:59:59.5 が静かに落ちる 【型】 CleanLogItem.scheduled_at: string(ISO8601 +09:00)(lib/types/index.ts:209) 【画面】 components/features/detail-log/CleanLogTable.tsx:188(日付)/ :189(予定) lib/format/date.ts:8-12 formatMd(iso) / :15-17 formatHm(iso) ★Date に通さず文字列を切る★ので実行環境のタイムゾーンに影響されない 【値の例】 ★構造由来★ DB `2026-09-11 02:00:00` → API `"2026-09-11T02:00:00+09:00"` → 画面「9/11 02:00」 ★記録済み実測(★当方のシード由来★。指示書AC C-6)★: clean_timetables 1,044行の start_at は 0時:210 / 1時:516 / 2時:288 / 3時:30 で ★15時以降が0件★ → ★9時間ずらしても日付が変わらないので、補正を忘れても件数が1件も変わらない★。 だから lib/db/__tests__/date-range.test.ts に「壁時計20:00の清掃」の合成データがある。 ★このテストを消さないこと★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(d) / docs/データの流れ_画面6-10_20260917.md §0.5。

3. 清掃が実施されたかどうかの判定を作り直した(D-106 / D-107)

  • 変更前: 「清掃の履歴データが1行も無ければ未実施」というこちらの推測で判定していた
  • 変更後: DBが持っている結果コード(正常/清掃未実施/セキュリティ有効/制御盤異常/ロボット異常)を読む
  • なぜ: 「清掃中に起きたエラーがどの画面にも出ない」という指摘をきっかけに定義書を読み直したところ、 最初から1つの項目で表現されていたことが分かりました。推測で判定していた状態を、根拠のある値を読む状態に置き換えています。
技術的な裏付け 【DBの列】 clean_status_histories.clean_error 型: Int @default(0) @db.UnsignedTinyInt(NOT NULL)(prisma/schema.prisma:487) 意味: 0=正常 / 1=異常(清掃未実施) / 2=異常(セキュリティ有効) / 3=異常(D-S制御盤異常) / 4=異常(清掃ロボット異常)(lib/config/index.ts:263-274) ★unsigned tinyint なのでコード表に無い 5〜255 も入りうる★ 【判定】 lib/db/clean-log-mapping.ts:152-157 notExecutedFromHistory(history: CleanResultLike | undefined): boolean 履歴の行が無い(undefined) → true(=出発失敗) clean_error === 1 → true 0 / 2 / 3 / 4 / 5以上 → false(=出発した扱い) ★★`!== 0` なら異常、と書かないこと★★ — `=== 1` の形だから ★コード表に無い5以上が自動的に「未実施ではない」側に落ちる★ lib/db/clean-log-mapping.ts:184-192 completionRateFromHistory(h): number | null 行が無い / clean_error===1 → null それ以外 → clean_ratio の生値(丸めない) ★★`history ? history.clean_ratio : null` に戻さないこと★★(lib/db/index.ts:915-917)— 未実施行の clean_ratio は既定値 0 なので ★null が「0%で実施した」に化ける★。 ★判定はどちらも × なので画面もテストも赤くならない★(D-107 落とし穴1) 【取得】 lib/db/index.ts:909 departed: !notExecutedFromHistory(history) lib/db/index.ts:918 completion_rate: completionRateFromHistory(history) 【型】 CleanLogItem.departed: boolean(lib/types/index.ts:222) CleanLogItem.completion_rate: number | null(:247) 【画面】 components/features/detail-log/CleanLogTable.tsx:200-202 {r.departed ? "✓" : "✗"} 【値の例】 ★構造由来★ 0 → ✓ / 1 → ✗ / 2・3・4 → ✓(未実施とは別扱い。ユーザー回答 2026-09-10) ★構造由来★ 履歴の行が無い → ✗(D-107 設計3-C。過剰検知は意図して選んだ側) ★シード由来★ fixtures は departed が入力で clean_error が導出(lib/fixtures/clean-logs.ts:66) → ★fixtures に clean_error === null の行は存在しない(null は DB 経由でだけ出る)★ 詳しくは docs/データの流れ_画面1-5_20260917.md §1.2 / §2.3(f)。

4. エラー発生率の分子が逆だった(D-118)

  • 変更前: 清掃前の点検(事前チェック)で起きたエラーだけを数えていた
  • 変更後: 本番の清掃で起きたエラーを数える
  • なぜ: TeamWorks の回答「事前チェックは本清掃を円滑に実施するためのものなので、集計には含めないのが正しい」による。 それまでは数えるべきでないものだけを数え、数えるべきものを捨てていた状態でした。
  • 数字の変化: 今週のエラー率 50% → 10.7%、内訳件数 14件 → 3件 (★この数字は当方のシード由来です★。本番の実勢ではありません。D-145)
技術的な裏付け 【計算式】 CLAUDE.md §4.2 エラー発生率 = エラー発生回数 ÷ 清掃実施回数 × 100 【DBの列】 robot_error_reports.error_type String @db.VarChar(100)(prisma/schema.prisma:328) clean_timetables.precheck Int @default(0) @db.UnsignedTinyInt(:596) ★0 = 本清掃。0以外は「本清掃開始の何分前か」の分数そのもの★(D-112) 【どこで分岐するか】 当期(進行中の週・月): lib/db/index.ts:2431〜 ★本清掃行(precheck = 0)を指す robot_error_reports★ だけを期間で絞って error_type ごとに数える ★以前は事前チェック行(precheck ≠ 0)のエラーを数えていた=分子が逆だった★ 確定期間: facility_error_summaries を読む(lib/db/index.ts:2397〜) facility_error_summaries.error_type String @db.VarChar(100)(prisma/schema.prisma:775) facility_error_summaries.error_count Int @db.UnsignedSmallInt(:777) 【変換】 lib/db/report-mapping.ts:759-771 errorBreakdownItemsFromCounts(counts: ReadonlyMap<string, number>): ErrorBreakdownItem[] type = label = ★生値★(7分類にも日本語にも変換しない。D-077 / D-088) rate = total === 0 ? 0 : count / total * 100 ★丸めない生値★ ★分母はこの Map 内の全件数の合計(reduce で出す。定数に置かない)★ 【型】 ErrorBreakdownItem(lib/types/index.ts:640-654) { type: string; label: string; count: number; rate: number } 【画面】 components/features/reports/ReportsScreen.tsx:679 / :690 / :693 {Math.round(item.rate)}% ★表示専用の丸め★ 【値の例】 ★シード由来★(lib/fixtures/reports.ts:483-496。合計 58件) EmergencyStop 18件 → 31.034…% → 表示 "31%" LostLocalization 12件 → 20.689…% → "21%" ""(空文字)1件 → ラベルは "(値なし)"(lib/format/raw-label.ts:21-26) 【未解決】 ★確定期間の稼働率とエラー発生率は、いまサマリから出せません★ — 分母(清掃実施回数)に相当する列がサマリに無いため(第9回 Q6-2 で依頼中。D-147) 詳しくは docs/データの流れ_画面1-5_20260917.md §4.4。

5. エラー種別を日本語7分類に翻訳するのをやめた(D-077)

  • 変更前: ロボットの故障種別を「ハンドルアップ/LostLocation/オフライン/ブラシエラー/緊急停止/充電不足/その他」の 日本語7分類に変換して表示する設計
  • 変更後: DBに入っている値をそのまま表示する(画面には EmergencyStop のような英語の識別子がそのまま出ます)
  • なぜ: ロボットメーカーのAPIから実際に何が返るか不明で、推測で対応表を作ると 現場に誤った故障原因を伝えることになるため。実際、TeamWorks から受領した24個の値のうち、 7分類の「ハンドルアップ」「オフライン」「ブラシエラー」に該当するものは1つもありませんでした。
  • 見た目の副作用: 日本語の文字と完全一致させて色分けしていたため、この列は全部グレー表示になります。これは意図した挙動です。
技術的な裏付け 【DBの列】 robot_error_reports.error_type String @db.VarChar(100)(prisma/schema.prisma:328) robot_error_reports.errored_at DateTime? @db.DateTime(0)(:332。★nullable★) 【変換】 lib/db/clean-log-mapping.ts:54-60 robotStatusFromErrors(errors: readonly RobotErrorLike[]): string 0件 → ROBOT_STATUS_STANDBY = "Standby."(lib/config/index.ts:322。★末尾のピリオド込みが原文★) 1件以上 → errored_at が最新の1件の error_type を ★生値のまま★ ★7分類への変換は行わない★(CLAUDE.md §4.8 / D-077) 【型】 CleanLogItem.status_1h / status_30m: string(lib/types/index.ts:215-217) ★実DB上の取得元・値の形式は未確定★(D-077 項3)。日本語6値は fixtures 由来の暫定値 【画面】 CleanLogTable.tsx:192 / :197 <Pill text={r.status_1h} color={statusColor(theme, r.status_1h)} /> lib/theme-status.ts:24-29 statusColor(theme: ThemeName, status: string): string ★Object.hasOwn(ROBOT_STATUS_TONES, status) でないとグレー(c.txD)にフォールバック★ ROBOT_STATUS_TONES(lib/config/index.ts:676-683)= ★日本語6語だけ★ 待機中:"ok" / 充電中:"pr" / ハンドルアップ:"ng" / 緊急停止:"ng" / オフライン:"ng" / セキュリティ有効:"ac" 【値の例】 ★シード由来★ "待機中" → 緑 / "オフライン" → 赤 ★構造由来★ "EmergencyStop"(実データで来うる英語識別子)→ ★表に無いのでグレー★ ★構造由来★ "Standby."(エラー0件のときの代替値)→ ★あえて表に足していないのでグレー★ 【なぜ Object.hasOwn を先に通すか】 lib/theme-status.ts:14-22 — `constructor` / `toString` / `__proto__` のような名前が status として来ると、 ★添字アクセスがプロトタイプ側の関数を拾い★、Pill が `background: undefined22` を出して ★グレーにすらフォールバックしない★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(e) / §3.4(b)。

6. 月をまたぐ週の数え方を全面的に作り直した(D-071)

  • 変更前(DFA案): 「その月の1日を含む週は、まるごとその月の第1週」。週は必ず7日間、週番号は1〜5
  • 変更後(TeamWorks方式): 月の境界で週を切る。第1週は必ず1日から、最終週は必ず末日で終わる。 週の長さは1〜7日で変わり、週番号は1〜6
  • 具体例(2026年): 8月第1週=08-01の1日だけ / 8月第6週=08-30〜08-31の2日だけ / 9月第1週=09-01〜09-05の5日
  • なぜ: 週次の集計表を作るのは TeamWorks 側で、DFAはそれを読むだけです。DFAが別のルールを持つと、 確定済みの週(集計表から読む)と進行中の週(DFAがその場で計算)で「週」の意味が変わり、同じ画面の中で数字が食い違います。
技術的な裏付け 【帰属ルールの置き場所】 lib/format/period.ts:126 weekOfMonth(date): { year: number; month: number; week: number } ★★帰属ルールを知っているのはこの関数だけ★★(CLAUDE.md §4.7)— ★画面側で年月週を自前計算しない★(ReportsScreen.tsx:85-86 の initialPeriod もこれを呼ぶ) 【期間の検証】 lib/reports/query.ts:138 parseReportQuery(searchParams) year は REPORT_YEAR.min(2000)〜max(2100)(lib/config/index.ts:421-426) ★★week は 1 以上のみ受け付ける★★ — week=0 はサマリ4テーブルで「月次」を表す予約値(lib/types/index.ts:757-762) 【粒度の判別】 lib/db/report-mapping.ts:72 periodOfSummaryRow(week: number): Period ★week === 0 なら monthly、それ以外は weekly★ (month=0 かつ week=0 が年次。★P1 では読まない★。D-072 項4) 【期間の移動】 ReportsScreen.tsx:245-260 navigatePeriod(delta) → shiftPeriod(...) ★monthly の shiftPeriod は week に 0 を返す(D-072)★ので、 週次へ戻すときは clampWeek() でその月の最大週へ丸める(:253-255) 【値の例】 ★構造由来★(CLAUDE.md §4.7 の2026年の例) 8月第1週 = 08-01 のみ(★1日★) 8月第6週 = 08-30〜08-31(★2日★) 9月第1週 = 09-01〜09-05(5日) 9月第2週 = 09-06〜09-12(7日) ★構造由来★ year の範囲外(例 year=-1)は 422。 ★記録済み実測(2026-09-14。develop 3e48d30)★: 以前は `period=monthly&year=-1&month=9` が 200 を返していた(lib/config/index.ts:392-399) 詳しくは docs/データの流れ_画面1-5_20260917.md §4.1。

7. 一覧画面にページ送りを新設した(D-120)★モックに無い機能★

  • 変更前: モックにはページ送りが1つもなく、全画面が「最大200件を一度に表示するだけ」。 201件目以降を画面に出す手段が存在しませんでした(しかも切れたことすら表示されない)
  • 変更後: 一覧6画面すべてにページ送りを追加。実際に存在するページだけをボタンに出す
  • なぜ: 本番想定は1日あたり約360行。当日1日を表示しただけで切れる状態でした ★→【2026-09-17 D-144 で訂正。「1日約360行」は施設200件を前提にした外挿で、 実際のロボット台数は30台未満(ユーザー申告)。★ただし「切れる」という結論は変わりません★】★
技術的な裏付け 【設定値】 lib/config/index.ts:325-341 PAGINATION = { defaultPerPage: 50, maxPerPage: 200, maxVisiblePages: 7 } ★構造由来★ 【API の約束】 一覧APIは必ず meta を返す(CLAUDE.md §9.3) Meta = { total: number; page: number; per_page: number }(lib/types/index.ts:20-24) ListResponse<T> = { items, meta }(:84-87) SummaryListResponse<TItem, TSummary> = { summary, items, meta }(:89-93) 【切り出しの順序】 app/api/clean-logs/route.ts :98 filterCleanLogs(rows, query) ← ★規則の「正」はここ。SQL はその写し★ :105 ページ切り出し → capCleanLogs() ← ★表示上限は最後にかける★ :107-110 { items, meta: { total, page, per_page } } 【★事故りやすい★】 ★`summary.total` をページャに渡さないこと★ app/api/prechecks/route.ts:81-84 / components/features/ds-logs/DsLogScreen.tsx:168-175 meta.total = 絞り込み後の件数(★ページング計算用★) summary.total = 日付・分類で絞った全件(★対応状況フィルタの影響を受けない★) ★取り違えると `summary.total > meta.total` になり「存在しないページのボタン」が出る★ (部品側にも同じ警告: components/ui/Pagination.tsx:33-39) 【値の例】 ★シード由来★ D-Secログの fixtures は12件(error 5件 / ok 7件。lib/fixtures/ds-devices.ts) → status=all のとき summary = { total: 12, ok: 7, error: 5 } / meta.total = 12 → status=error に切り替えると ★summary は不変・meta.total だけ 5★ 詳しくは docs/データの流れ_画面6-10_20260917.md §6.5 / docs/データの流れ_画面1-5_20260917.md §3.3。

8. 絞り込みをDB側に降ろした(D-131)

  • 変更前: DBから全部読み込んでから、プログラム側で絞り込んでいた
  • 変更後: DBに条件を渡して、絞り込んだ結果だけ受け取る
  • 効果(実測): ダッシュボード1日分 1,657行 → 116行 / 清掃詳細ログ1日 1,631行 → 90行 (★出所: 当方のシード由来★。★本番の行数ではありません★。D-145)
  • 重要: 画面に出る数字は1つも変わっていません(216ケースで変更前後の完全一致を確認済み)。速さだけの改善です
技術的な裏付け 【どこまでSQLに降りたか】 ★日付と施設IDまで★。 ★機種・シリアル・判定のフィルタと、ページの切り出し(slice)は今もメモリのまま★ (app/api/clean-logs/route.ts:98-105)。★これは意図した決定★ — SQLでページングすると ★meta.total が「判定フィルタ後の件数」と食い違う★(D-131) 【★最大の罠★】 ★`null` と `[]` は別物★ null = 絞らない / [] = 照合したが1件も当たらなかった(=0件) lib/db/index.ts:729-736 / :1165-1173 / :1808-1813 app/api/clean-logs/route.ts:75-77 / lib/types/index.ts:155-163 ★★Prisma の `in: undefined` は条件が消えて `WHERE 1=1`(全件)になる★★ — ★「0件ヒット」が「全件表示」に化ける。例外もテスト失敗も出ない★ ★`query.facility_ids?.length ? ... : {}` と書かないこと★(lib/db/index.ts:1808-1813) 【施設IDへの解決】 lib/facilities/resolve-by-master.ts:88-93 resolveFacilityIdsByMasters() lib/facilities/intersect-ids.ts intersectFacilityIds(nameIds, masterIds) ★AND(積集合)。片方を捨てない。積が空なら [] を返し null にしない★ lib/facilities/filter-by-ids.ts:48-55 filterRowsByFacilityIds() ★DataSource が引数を取らない4本(ds-devices / ds-alerts / facilities / schedules)は SQLに降ろさずルート側でメモリフィルタしている★(D-138 項3) 【母集団を揃える】 app/api/dashboard/route.ts:66-72 — ★数値カード・スケジュール進捗・セキュリティバナー・要レスキューの4つに同じ facilityIds を渡す★。 ★1つでも渡し忘れると「この屋号の昨夜の成績」と「要レスキュー」が別集団の話になる★。 ★例外もテスト失敗も出ない★ので app/api/__tests__/master-filter-dashboard-reports.test.ts で固定してある 【上限】 lib/config/index.ts:464 DB_IN_CLAUSE_MAX_ITEMS = 1000 ★構造由来★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.2 / 付録A #3。

9. 実データを入れた日に初めて出る不具合を潰した(D-133)

3件とも、手元の小さなテストデータでは絶対に再現しないものでした。

不具合 実データで何が起きるか
D-Secログ画面が制御盤の全通信履歴を読んでいた 本番想定1,100万〜7,000万行。画面を開いた瞬間にサーバーが落ちる
備考の保存でも同じ全件読みが走っていた 同上
顧客サマリを顧客で絞っていなかった 2社目のデータが入るとKPIが別会社の数字になる(エラーは出ない)

「テストが全部通っている」ことが何の保証にもなっていなかった、という教訓として記録しています。

技術的な裏付け 【① D-Secログが全通信履歴を読んでいた】 【DBの列】 control_reports.reported_at DateTime @default(now()) @db.Timestamp(0)(prisma/schema.prisma:365) 【直し方】 lib/db/index.ts:1440-1444 prisma.controlReport.groupBy({ by: ["control_id"], where: { control_id: { in: controls.map(c => c.id) } }, _max: { reported_at: true }, }) ★行は運ばない(制御盤ごとの最大値だけ取る)★(:1437-1439) ★`orderBy: { id: "desc" }, take: 1` に書き換えないこと★(:1421-1424)— ★`id` の大小順と `reported_at` の大小順が一致する保証がどこにも無い★。 さらに ★`take: 1` は SQL に降りない★(D-133 の発見そのもの) 【突合の罠】 lib/db/index.ts:1448 new Map(latest.map(g => [String(g.control_id), g._max.reported_at])) ★★キーは String() で揃える★★ — BigInt をそのままキーにすると `1n` / `"1"` / `1` が別キーになり、 ★全制御盤が「心拍0件」に見えて例外になる★ 【② 顧客サマリを顧客で絞っていなかった】 【DBの列】 customer_summaries.operation_ratio / launch_ratio / clean_ratio / error_ratio (prisma/schema.prisma:699-702。すべて Float @default(0.0)) 【直し方】 lib/reports/target-customer.ts resolveTargetCustomer(customers, query) → app/api/reports/kpi/route.ts:96 で対象顧客を1社に決めてから読む ★静かに1社を選ばない★。決まらなければ 422(D-138 / 後述の第4章 D-138 / D-140) 【当時の安全弁】 「複数顧客の行が混ざったら例外(500)」だけがあった(D-133)→ D-138 で置き換え 【規模の前提について】 ★「本番想定1,100万〜7,000万行」は 制御盤11台 を前提にした外挿でした★。 ★11台という数字もテストデータ由来で本番の根拠がありません★(D-144 §5 #3)。 ★上限はむしろ上がっています★(60秒間隔で約3,053万行 / 10秒間隔で約1億8,316万行)。 ★★結論(必要な分だけ読む)は変わりません★★ 詳しくは docs/データの流れ_画面6-10_20260917.md §6.3 / docs/データの流れ_画面1-5_20260917.md §4.2。

10. 契約フラグの読み方が根本的に誤解されていた(D-080)

  • 誤解していた内容: 「0=未設定 / 1=D-Security / 2=D-Watch / 3=両方」という数値だと思っていた
  • 正しい仕様: 10桁の文字列で、桁の位置がサービスに対応する(1桁目=D-Security、2桁目=D-Watch、契約している桁を 1 にする)
  • 放置したらどうなっていたか: "0000000000" を数値に変換すると例外を出さずに 0 になるため、 「契約 = 3(両方)」のような判定は絶対に成立しません。結果、全施設が「契約なし」と判定されメニューが全部消えるが、 エラーもテスト失敗も出ないという状態になっていました。
  • 救い: この値を読むコードを1行も書いていない段階で気づいたため、手戻りはゼロです
技術的な裏付け 【DBの列】 facilities.contract 型: String @default("0000000000") @db.VarChar(10)(prisma/schema.prisma:139) 意味: ★桁位置がサービスに対応する★(D-080 / CLAUDE.md §4.6) 1桁目(index 0)= D-Security / 2桁目(index 1)= D-Watch。契約済みの桁を "1" にする ★3桁目以降は将来の予備と読める(定義書に明記なし。P1では読まない)★ 【桁位置の定義】 lib/config/index.ts:633-636 export const CONTRACT_DIGITS = { dSecurity: 0, dWatch: 1 } as const ★桁位置の数字をこのファイル以外に直書きしないこと★(:631) 【変換(唯一の消費者)】 lib/format/contract.ts:26-31 parseContract(contract: string): ContractFlags interface ContractFlags { monitoring_enabled: boolean; d_security_enabled: boolean }(:13-16) d_security_enabled: contract[CONTRACT_DIGITS.dSecurity] === "1" monitoring_enabled: contract[CONTRACT_DIGITS.dWatch] === "1" ★文字の完全一致だけで判定する★(:8) ★桁が足りない文字列・空文字・10桁超でも例外を投げない★(:23-24)— 存在しない桁は `undefined === "1"` が false になり自然に「契約なし」扱い 【取得】 lib/db/index.ts:1632 const { monitoring_enabled, d_security_enabled } = parseContract(row.contract); 【型】 Facility.monitoring_enabled: boolean(lib/types/index.ts:955) Facility.d_security_enabled: boolean(:960) 【画面】 components/features/facilities/FacilitiesScreen.tsx:148-154(監視列)/ :155-161(D-Sec列) true → ○ / false → ー 【★やってはいけない★】 Number("0000000000") === 0 ← ★例外を出さずに 0 になる★ facility.contract === 3 ← ★絶対に真にならない★ ★帰結: 全施設が「契約なし」になりメニューが全部消えるが、例外もテスト失敗も出ない★ 【値の例】 ★構造由来★ "0000000000" → ー / ー "1000000000" → ー / ○ ★構造由来★ "0100000000" → ○ / ー "1100000000" → ○ / ○ ★シード由来★ 施設1「メガバリューマート中央本店」= 両方 true(lib/fixtures/facilities.ts:53-54) ★シード由来★ 施設11「バリューマート川下店」= D-Securityのみ(:253-254) ★シード由来★ 施設12「フレッシュタウン湖畔店」= 未設定(:276-277) 【★★もう1つの罠(2026-09-17 に判明)★★】 ★`clean_schedules` にも `contract` 列がある★(prisma/schema.prisma:563。 `@default("0000000000") @db.VarChar(10)` で ★型も既定値も facilities.contract と同一★)。 ★アプリは一切読んでいない(正しい)★が、★名前と型が同じなので取り違えやすい★。 ★`facilities.contract` のみが正★(CLAUDE.md §4.6)。 なお `customers.contract` は削除依頼済みで、Prisma の model Customer には ★そもそも列が無い★ 詳しくは docs/データの流れ_画面6-10_20260917.md §8.2 / 付録A A-5。

第3章 — 変更点 全一覧

影響の大きい順に並べています。「状態」の 保留 は、まだ決着していないものです(★第7章★に再掲)。 ★この表は 2026-09-14 時点のものです。2026-09-15 以降の変更は第4章にあります★。

3-1. 影響が大きい変更(プロダクトの作り方・数字の意味が変わるもの)

# 変更したこと 変更前 変更後 なぜ変えたか 状態 決定
1 開発言語と作る主体 TeamWorks が PHP / Laravel で実装 DFA 内製で Next.js + TypeScript 非エンジニア2名で2言語は管理不能。AI支援の精度もこの構成が最高 確定 D-001
2 サーバーの種類 さくらのレンタルサーバ さくらのクラウド仮想サーバ レンタルサーバでは Next.js が必要とする常駐プログラムを動かせない 確定 D-003
3 DB設計図の「正」をどれにするか テーブル定義書(紙の資料) 実際のDBから機械的に吸い出したもの 手書き版と実物を突き合わせたら 49件の差分。うち3件は定義書側の綴り誤り、8件は廃止済み項目 確定 D-056 / D-059
4 開発用ダミーデータの作り方 API仕様書から自動生成するツール 手作りのサンプルデータ+切替スイッチ 自動生成だと全レコードが同じ値になり、絞り込み・色分け・ページングが何も確認できない 確定 D-044
5 契約状態の持ち場所 顧客・予定・施設の3テーブルに分散 施設テーブル1箇所のみ 同じチェーンでも店舗ごとに契約が違う。二重管理は必ず食い違う 確定 D-010
6 契約フラグの読み方 「0/1/2/3」の数値と誤解していた 10桁の文字列。桁の位置がサービスに対応 数値として比較すると 全施設が「契約なし」になりメニューが全部消えるが、エラーもテスト失敗も出ない 確定 D-080
7 月をまたぐ週の数え方 週は必ず7日間・週番号1〜5(DFA案) 月境界で分割。週の長さ1〜7日・週番号1〜6 集計表を作るのはTeamWorks。ルールが違うと同じ画面の中で数字が食い違う 確定 D-071
8 エラー種別の表示 日本語7分類に変換して表示 DBの値をそのまま表示(英語識別子が出る) 何が返るか不明。推測でマッピングすると現場に誤った故障原因を伝える。受領した24値に7分類のうち3つは1件も該当せず 確定 D-077
9 清掃が実施されたかの判定 履歴が無ければ未実施(こちらの推測) DBの結果コードを読む 定義書を読み直したら 最初から1つの項目で表現されていた 確定 D-106 / D-107
10 画面に出る日時 世界標準時とみなして読み、さらに+9時間(二重のズレ) 日本時間の時計の値として1回だけ読む 深夜2時の清掃が「未来の予定」と判定され 翌朝の一覧から消えていた 確定 D-114 / D-115
11 エラー発生率の分子 事前チェックで起きたエラー 本番の清掃で起きたエラー 事前チェックは本清掃を円滑にするためのもので集計対象外(TeamWorks回答)。率 50% → 10.7% 確定 D-118
12 事前チェックの値の意味 1→60分前、2→30分前 と読み替え その数値がそのまま何分前か(0は本清掃) TeamWorks回答で確定。従来は 真逆の変換 をしていた 確定 D-112 / D-113
13 アラート種別の持ち方 「1時間前異常 / 30分前異常 / 出発失敗」の3種固定 「事前チェック異常+分数」(20分前なども表現可) 事前チェックの間隔は設定で変わりうる。固定3種だと設定変更のたびにAPI契約を変える必要がある 確定 D-105
14 連続問題日数の数え方 ロボット1台ごとに数える 施設ごとに数える(1台でも問題ならその日は問題日) 仕様が「同一施設で2日以上連続」。実データに1施設3台の例が実在 確定 D-109 / D-110
15 進行中の期間の分母 まだ来ていない予定も分母に入れる 未来の予定を分母から外す 日曜の朝や月初に稼働率が極端に低く出て、施設別パフォーマンスが「10施設 → 0施設」になる(実測) 確定 D-109 / D-110
16 確定期間のレポート数値 履歴から自分で計算し直す案 TeamWorksの集計表をそのまま表示(当期のみ自前集計) 集計は先方の担当領域。二重に実装すると食い違ったときにどちらが正しいか決められない 確定 D-102
17 率(稼働率・完了率など)の精度 DB上は整数 小数第1位まで持つ 整数だと丸めた数字で◎○△×を判定することになり仕様違反(89.5%が90%になる) 確定 D-091 / D-094
18 エリア別完了率の意味 1回の清掃の中を売り場・通路・レジ前に分けた内訳 同じ施設・同じ日の全エリア(ロボット)の一覧 求めていた区域別内訳は取得できないと判明。取得可能な粒度で作り直した 確定 D-119
19 一覧のページ送り 1つも無い(最大200件を一度に表示するだけ) 一覧6画面すべてに追加 本番想定は1日約360行 ★→【D-144 で訂正。施設200件を前提にした外挿でした。実態はロボット30台未満】★。当日1日で切れる うえ、切れたことすら表示されなかった 確定 D-120
20 絞り込みの実行場所 全件読み込んでからプログラム側で絞る DBに条件を渡して絞ってもらう 実測でダッシュボード1,657行 → 116行。画面の数字は1つも変わらない 確定 D-131
21 実データで壊れる3箇所 D-Secログが全通信履歴を読む/顧客で絞っていない 等 必要な分だけ読む/1社に絞れなければエラー 本番想定1,100万〜7,000万行で 画面を開いた瞬間にサーバーが落ちる。2社目が入ると KPIが別会社の数字になる 確定 D-133
22 店舗検索の記号の扱い ドン・キホーテ戸塚 に「ドンキ」で 0件 中点・長音・空白・ハイフンを無視して照合 記号は店名の表記ゆれにすぎない。ただし住所とシリアルには適用しない(誤ヒットを実測) 確定 D-129
23 連続日数の表示上限 上限なし(Lv3 / 45日) Lv3 / 30日以上。上限を画面から変更できる(3〜365日、既定30日) 30日超は「日数の正確さ」より「訪問したか」が問題。判定には生の値を使う 確定 D-130
24 ログインの有無 URLを知っていれば誰でも全画面が開けた ログイン+社内IP制限 本番でIP制限を有効にし忘れると誰でも入れる状態だった 確定 D-101
25 D-Secログの接続状態 3状態(正常 / 通信断 / 異常) 2状態(正常 / 異常) DBの値が0/1の2値で「異常=生死報告なし=通信断」と同一 確定 D-065 / D-076
26 「セキュリティ解除されたまま」のアラート D-Secアラートの2種類目として表示 当面出さない(ダッシュボードの赤バナーも出なくなる) 必要な項目(いつから解除か・対応状況・備考)がDBに1つも無い。推測で埋めると嘘を出すことになる 保留(社内調整中) D-091 / D-097

3-1 の技術的な裏付け

★No.1〜No.11 と No.20 / No.21 は第2章に裏付けを載せてあります★(重複を避けるためここでは省きます)。 ★裏付けを付けていない行(No.3 / No.4 / No.5 / No.16 / No.19 / No.24 / No.26)は、 体制・手続き・保留の話で、裏付けるべき数値やデータ形式が無いためです★。

技術的な裏付け — No.12 事前チェックの値の意味(1→60分前という真逆の変換をやめた) 【DBの列】 clean_timetables.precheck Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:596) 意味: ★0 = 本清掃。0以外は「本清掃開始の何分前か」の分数そのもの★ (TeamWorks 第7回 A5-2 / A5-5。2026-09-11 受領。D-112) 【変換】 lib/db/precheck-mapping.ts:92-95 precheckMinutesFromCode(precheck: number): number | null precheck === CLEAN_TIMETABLE_PRECHECK.main(0) → null それ以外 → precheck(★生値そのまま★) ★★`1` は「1回目」ではなく「1分前」★★(A5-3)— ★以前の実装は 1 を60分前・2 を30分前と読んでおり、TeamWorks の指示と真逆だった★(指示書X) ★未知の値を「その他」に丸めない★(A5-5「0以外の予約語はありません」) 【列の割り当て】 lib/db/precheck-mapping.ts:125-132 statusColumnsFromPrecheckRows(m): [string, string] = PRECHECK_MINUTES_BEFORE.map(minutes => robotStatusFromErrors(m.get(minutes) ?? [])) lib/config/index.ts:214 PRECHECK_MINUTES_BEFORE = [60, 30] ★構造由来★ ★旧コード値(1 / 2)の行しか無い場合はどちらとも一致せず、両方「待機」になる★(期待値表 #6) 【値の例】 ★構造由来★ precheck=60 → 1h前ST の列 / precheck=30 → 30分前ST の列 ★シード由来★ precheck_minutes=20 → 「20分前異常」(lib/fixtures/prechecks.ts の id:"9"。 ★設定値以外の分数を表現できるかの境界値。消さないこと★) 詳しくは docs/データの流れ_画面1-5_20260917.md §3.4(a) / §2.3(e)。 技術的な裏付け — No.13 アラート種別の持ち方(3種固定 → 「事前チェック異常+分数」) 【型】 PrecheckItem.severity: "precheck" | "launch_failed"(lib/types/index.ts:437 / :462) PrecheckItem.precheck_minutes: number | null(:470) ★種別を「1時間前異常 / 30分前異常 / 出発失敗」の3値にせず、 「どちらの行か(severity)」+「何分前か(分数)」の2項目に分けた★(D-105) 【整形】 lib/prechecks/label.ts:32-39 precheckSeverityLabel(t: Translate, item: Pick<PrecheckItem,"severity"|"precheck_minutes">): string severity === "launch_failed" → t("出発失敗") precheck_minutes === null → t("事前チェック異常") ★黙って "null分前異常" を出さない★ それ以外 → t("{n}分前異常").replace("{n}", String(minutes)) 【この一覧は「2種類の行」を混ぜている】(lib/db/index.ts:1109-1113) "precheck" … 事前チェック行に紐づく robot_error_reports の行。期間の基準は★親の本清掃行の start_at★ "launch_failed" … 本清掃が実施されなかったこと(★エラー行ではない★)。期間の基準は★自身の start_at★ ★`launch_failed` は cleanLogs() の `departed === false` と同じ出来事★なので ★同じ判定関数 notExecutedFromHistory() を共有する★(D-103 項1) ★「本清掃行を指すエラー行」を launch_failed に読み替えないこと★(lib/db/index.ts:1261-1263)— 読み替えると二重計上になり、`departed` と別定義になる 【画面】 components/features/alerts/AlertsScreen.tsx:274-277(Pill)/ :32-34(色: launch_failed → 赤 / precheck → 警告色) 【値の例】 ★構造由来★ 60 → 「60分前異常」/ 30 → 「30分前異常」/ severity=launch_failed → 「出発失敗」 詳しくは docs/データの流れ_画面1-5_20260917.md §3.1 / §3.4(a)。 技術的な裏付け — No.14 連続問題日数を施設ごとに数える 【畳み込み】 lib/db/clean-log-mapping.ts:340-352 foldProblemDaysByDate(rows): ScheduleDayResult[] ★同一日に問題ありの行が1つでもあれば その日は「問題あり」(OR。A案。D-109)★ ★施設に複数台あると同じ日に複数行が生まれるので、畳まないと「N日連続」が水増しされる★ 【数える】 lib/judge/escalation.ts:32-39 countConsecutiveProblemDays(days: ScheduleDayResult[]): number ★直近から遡って数える。最新日が問題なしなら 0★ 【判定】 lib/judge/escalation.ts:52-59 judgeEscalationLevel(consecutiveDays: number): EscalationLevel | null 0以下 → null / >= 3 → "L3" / >= 2 → "L2" / それ以外 → "L1" lib/config/index.ts:27-32 ESCALATION_THRESHOLDS = { level2Days: 2, level3Days: 3 } ★構造由来★ 【★事故りやすい★】 ★表示期間そのもので絞って数えないこと★(lib/db/consecutive-source.ts:129-137 / 指示書AC C-2)— 「9/8× 9/9× 9/10×」(3日連続 = L3)を「9/10〜9/10」で絞って渡すと ★1日 = L1 に化ける★。 ★例外もテスト失敗も出ない★ 【遡り幅】 lib/db/consecutive-source.ts:132-137 consecutiveLookbackCalendarDays(maxDays) = (maxDays + CONSECUTIVE_LOOKBACK.plusDays) × CONSECUTIVE_LOOKBACK.calendarDaysPerScheduleDay = (30 + 1) × 7 = ★217 カレンダー日★(lib/config/index.ts:167-172。★構造由来★) ★plusDays を 0 にしないこと★ — 上限ちょうどまでしか読まないと `capped` が永久に false になり ★「30日以上」が二度と画面に出なくなる★ 【値の例】 ★シード由来★ 施設4(月水金スケジュール)は5日前・3日前・1日前の3回とも失敗 → ★カレンダー上は5日だが、スケジュール存在日の連続は3日 → L3★ (lib/fixtures/clean-logs.ts:148-153) 【★未検証★】 「7カレンダー日に1回は清掃予定がある」という遡り幅の前提は ★未検証★(D-144 §5 #4 / D-145)。 ★本番のスケジュールに週1回のものが1本でも入ると、連続日数が黙って切れる★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(j)。 技術的な裏付け — No.15 進行中の期間の分母から未来の予定を外す 【判定】 lib/db/clean-log-mapping.ts:263-265 isFutureSchedule() ★`clean_timetables` には1週間分の予定がまとめて登録される★(D-106)ので、 外すと ★最大7日先までの予定が全部「出発失敗」になる★ ★同時刻(start_at === now)は「来ていない」に入れない★(`>` であって `>=` ではない) 【記録済み実測(★当方のシード由来★)】 3日後の予定を1行入れると `launch_failed` が 13 → 14 件に増えた (lib/db/clean-log-mapping.ts:236) 【連動する判定】 lib/judge/kpi.ts:84-91 judgeFacilityStatus(operationRateValue: number | null): FacilityStatus null → "no_data"(★この状態を新設した★。No.36 参照) ★以前はここを「要改善(赤)」に寄せていたため、週の初め・月の初めに全施設が赤くなっていた★ (lib/judge/kpi.ts:78-82。決定1。2026-09-11 ユーザー確定) 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(f) / §4.5。 技術的な裏付け — No.17 率の精度を小数第1位まで持つ 【DBの列】 clean_status_histories.clean_ratio Float @default(0.0)(★実DDL は double(4,1)★) (prisma/schema.prisma:497。D-094) ★取得元で 100 に丸められるため上限は 100★(TeamWorks 回答 B-3) customer_summaries / facility_summaries の operation_ratio / launch_ratio / clean_ratio / error_ratio も同じく Float(prisma/schema.prisma:699-702 / :733-736) 【判定と表示の分離】 CLAUDE.md §4.3 判定 … lib/judge/grade.ts:29-35 judgeGrade(rate: number | null): Grade ★生値で比較★ 表示 … lib/judge/grade.ts:43-46 displayRate(rate: number | null): string null → "-" / それ以外 → `${Math.round(rate)}%` 【★なぜ整数だと困るか★】 ★89.5 を先に丸めると 90 になり、無いはずの ◎ が生まれる★ (★D-139 前は「90.4 の ◎ が消える」向きの事故だった。向きが変わっただけで禁止は同じ★) 【値の例】 ★シード由来★ 90.1 → "90%"(判定 ◎)/ 89.5 → "90%"(判定 ○)/ 9.9 → "10%"(判定 ×) 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(h) / §2.3(i)。 技術的な裏付け — No.18 エリア別完了率(同じ施設・同じ日の全エリア一覧) 【入口】 GET /api/clean-logs/{id}/areas ★★{id} は「エリアを特定するキー」ではなく、施設と日付を特定するための入口★★ (app/api/clean-logs/[id]/areas/route.ts:103-105。指示書Z 決定1)— ★clean_log_id で引くと他のエリアが消える★ 【2段構え】 1段目 :78-83 db.cleanLogs({ id: logId, ... }) → 404 判定と facility_id / JST日付の特定だけに使う :99 const date = log.scheduled_at.slice(0, 10) ★areaCompletions() の date と同じ作り方にする★(片方だけ生の Date から作ると 9時間ずれて突き合わせが外れる。D-115) 2段目 :106 db.areaCompletions({ facility_id: log.facility_id, date }) 【集計】 lib/clean-logs/areas.ts:110-139 buildAreaCompletions(rows, filter): AreaCompletionsResponse problem_count / total_count は ★filter で絞る前★の件数 overall = completionRate(Σclean_area, Σtask_area) ★面積ベース★ 並び順 lib/clean-logs/areas.ts:70-82 ★完了率の昇順。未実施(null)は先頭。同値は section_id 昇順★ 【★意図した非対称★】(指示書Z 決定7) 各行の completion_rate … ★clean_ratio の生値★(面積から再計算しない) overall_completion_rate … ★Σclean_area ÷ Σtask_area × 100★(面積ベース) ★合計が行の平均と一致しないことがある。どちらを選んでも揃わないので、揃わないことを明示する★ 【型】 AreaCompletionsResponse(lib/types/index.ts:303-317)/ AreaCompletionItem(:286-300) 【画面】 components/features/detail-log/AreaModal.tsx:70-71 / :104-120 / :131-144 ★0件の文言を total_count === 0 かどうかで分ける★ →「エリアのデータがありません」/「問題エリアはありません(全エリア◎○)」 【値の例】 ★シード由来★ 施設1・当日: task_area = 1000、行の completion_rate = 90.1 → clean_area = 1000 × 90.1 ÷ 100 = 901 → overall = 90.1 → 表示「90%」/ 判定「◎」 詳しくは docs/データの流れ_画面1-5_20260917.md §2.5。 技術的な裏付け — No.22 店舗検索の記号の扱い(住所とシリアルには適用しない) 【照合規則】 lib/clean-logs/query.ts:163-178 施設名だけ includesFoldName() ← ★記号(中点・長音・空白・ハイフン類)と大小文字を無視★ 機種・シリアル includesFoldPlain() ← ★大小文字のみ。記号は除かない★ ★記号を除くと `PUDU-CC1-0001` が `10001` でヒットし、桁の境界が消える★(指示書AG D-2) 住所も includesFoldPlain(lib/facilities/query.ts:108 / :93-96)— ★ハイフンは番地の区切りなので、除くと "23" が "1-2-3" に誤ヒットする★ 【他画面も同じ規則】 lib/ds-devices/query.ts:83-90(施設名は Name / シリアルは Plain) lib/facilities/query.ts:102-116(施設名・都道府県は Name / 住所・ロボットは Plain) 【残っている穴(★仕様です★)】 半角カナ・ひらがなでは引けない / スペース区切りのAND検索が無い / `メガドンキホーテ` で `MEGAドン・キホーテ` は引けない(D-129) 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(c) / docs/データの流れ_画面6-10_20260917.md §6.6。 技術的な裏付け — No.23 連続日数の表示上限(判定には生の値を使う) 【設定値】 lib/config/index.ts:133-140 CONSECUTIVE_DAYS_DISPLAY = { defaultMax: 30, minMax: ESCALATION_THRESHOLDS.level3Days(=3), maxMax: 365 } ★単位は「スケジュール存在日」。カレンダー日ではない★(D-026) ★minMax を L3 の日数に縛ってあるので、L3 を下回る上限は設定できない★ ★maxMax 365 はデータ保持2年の半分★ 【上限をかける場所】 lib/format/consecutive-days.ts:32-38 capConsecutiveDays(rawDays, maxDays = 30): { days: number; capped: boolean } ★rawDays > maxDays のときだけ capped: true★(★上限ちょうどは false★) app/api/clean-logs/route.ts:105 capCleanLogs(items, maxDays) 【★★escalation_level には触らない★★】 lib/clean-logs/query.ts:196-197 — ★L1〜L3 は上限をかける前の生値で判定済み★(指示書AF 禁止事項9)。 ★かけると「30日以上」と出ているのに Lv3 が出ない★ アドバイスAPIでも ★上限をかけてよいのは1箇所だけ★ (app/api/clean-logs/[id]/advice/route.ts:154。:143-146 に理由) 【型】 CleanLogItem.consecutive_problem_days: number(lib/types/index.ts:254) CleanLogItem.consecutive_problem_days_capped?: boolean(:262。★任意項目★) 【画面】 components/features/reports/ReportsScreen.tsx:592-600 ★capped なら t("{n}日以上")、でなければ t("{n}日")★ 【編集】 GET/PATCH /api/settings/display-limits 保存先: data/settings/display-limits.json(lib/settings/display-limits.ts:49-51) ★メインDBではなくDFA独自のファイルに保存する★(`/data` は .gitignore:50 で除外) ★読み込み時にも検証を通し、値が不正なら例外にせず既定値(30)に倒して warn を1行残す★ (lib/settings/display-limits.ts:147-174 / :213-237) ★この画面で唯一編集できる設定★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(j) / docs/データの流れ_画面6-10_20260917.md §10.5。 技術的な裏付け — No.25 D-Secログの接続状態を2状態にした 【DBの列】 controls.status Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:209) 意味: ★0 = 正常 / 1 = 異常★(D-065 Q1-2) ★★3状態目(通信断)は存在しない★★ — 「異常」=「生死報告が届いていない」なので 通信断と異常は同一(lib/types/index.ts:831-836) 【変換】 lib/db/ds-status.ts:20-26 connectionStatusFromControlStatus(status: number): DsConnectionStatus 0 → "ok" / 1 → "error" / それ以外 → ★throw Error★(黙って握りつぶさない) 【型】 DsDevice.connection_status: "ok" | "error"(lib/types/index.ts:837 / :846) 【並べ替え】 lib/ds-devices/query.ts:118-130 CONNECTION_STATUS_ORDER = { error: 0, ok: 1 } → ★異常が先★ 【画面】 components/features/ds-logs/DsLogScreen.tsx:30-34("ok" → 「正常」/ それ以外 → 「異常」) :36-39 色は ok → 緑 / error → 赤 【★危ない★】 ★実データで `status = 2` が来たら一覧全体が 500 になる★(lib/db/ds-status.ts:23-25)。 ★意図した設計(握りつぶさない)だが、被害範囲は D-Secログ画面の全滅★ 【★別物★】 ★`control_error_histories.status` の3値(0=未対応 / 1=対応中 / 2=対応済)は2状態化しない★ (D-079 項2)。★2つの「status」を同じ話だと思って一緒に潰さないこと★ 【値の例】 ★シード由来★ DS-001 は "error" / DS-002 は "ok"(lib/fixtures/ds-devices.ts:30 / :22) ★記録済み実測★ `controls` は11行(★当方のシード由来★。 `password` が全行 "seed-local-placeholder-password"。 docs/handoff/テストDBのデータ出所_20260917.md:91。2026-09-17) 詳しくは docs/データの流れ_画面6-10_20260917.md §6.2。

3-2. 影響が中くらいの変更(画面の見え方・計算結果が変わるもの)

# 変更したこと 変更前 変更後 なぜ変えたか 状態 決定
27 点検記録データの保存先 メインDBに入れる DFA独自の保管場所(DB分離+写真置き場) 業務データでライフサイクルが違う。TeamWorksへの依頼なしにDFA単独で改修できる状態を保つ 確定 D-004
28 点検記録のスコープ 要件定義書に記載なし(モックにのみ存在) 正式にP1に含める メインDBと別管理なのでDFA単独で作れる 確定(着手は後回し) D-031
29 トークン期限/残日数の表示 モック・UI仕様書・運用ガイドに記載あり 実装しない(列ごと削除) 画面要件整理シートで不要と確定。古い資料に記述が残っていただけ 確定 D-030
30 D-Secログの「備考」欄 読むだけの列(検知内容が入る想定) 運用担当がその場で書き込める入力欄(DFA独自保存) TeamWorksへの逆質問で「これは運用担当が書き留めるメモ」と判明。DBにカラムが無い 確定 D-065 / D-076
31 アドバイスの返し方 該当ルールを1件だけ返す 該当する全件を返す 3日連続かつオフラインの行では2つの対応が必要。1本に絞ると対応漏れが出る 確定 D-037
32 アドバイスの発火条件 モックは当日の判定だけで判断 仕様どおり連続日数で判断(R3=2日以上、R4=3日以上) モックは連続日数を持っていなかったための簡略実装だった 確定 D-038
33 アドバイスの文言 コード内に固定 運用担当が編集できる設定に分離(発火条件は仕様として固定) 文言は運用しながら磨くもの。変えるたびにテストが壊れるのは不合理 確定 D-036
34 Phase B のアドバイス3件 後回し(Phase B) P1に前倒し モックに既に存在し実装コストがほぼゼロ、運用上も有用 確定 D-039
35 連続問題施設の説明文 「完了率 n%」/人が書いた原因推定と助言 事実のみの機械的な文章 原因の推定にはエラー分類が必要だが分類しない方針。助言は別欄に既にあり二重になる 確定 D-111 / D-116
36 施設別パフォーマンスの状態 良好/要注意/要改善 の3つ 「判定なし」を加えた4つ 予定が1件も無い施設まで「要改善(赤)」になっていた 確定 D-110
37 表のセルの色分け基準 モックが 80/90 を直接埋め込み 既存の閾値(90/75)を流用 同じ行で「ステータス=良好」なのに「セル=警告色」という矛盾が起きていた 確定 D-069
38 判定基準の画面文言 「◎ 90%以上」(モック) 「◎ 90%超」 モックのままだと「ちょうど90.0%が◎」という誤った説明が画面に出る。確定仕様では90.0は○ ★→【2026-09-16 D-139 で変更。現在は「◎ 90%以上」で、90.0 は ◎ です】★ 確定 D-083 / D-084 / ★D-139★
39 対応日時の記録 1項目のみ(かつ綴り誤り) 「対応開始」「解消」の2項目 1つだとどちらかの時刻が失われる 確定 D-013
40 主要9テーブルの構造 施設を知るには多段にたどる必要があった 各テーブルに施設IDを直接持たせた DFAの運用は施設単位の参照・集計が中心 確定 D-012
41 集計テーブルの中身 率だけを持つ 丸める前の小数+分子・分母も持つ 丸めた整数だけでは正しい判定ができず、検算もできない 確定 D-022
42 集計バッチの回し方 毎日、進行中の期間を作り直す 期末に1回だけ(進行中はDFA側でその場集計) 毎日の洗い替えはTeamWorks側のバッチ負荷が上がる 確定 D-023
43 「連続」の数え方 単純なカレンダー日数 清掃予定がある日の連続 定休日など予定がない日を挟んでも、問題の連続性は途切れていない 確定 D-026
44 清掃完了率の計算根拠 時間ベース/走行距離ベースの可能性もあった 面積ベースに確定 Pudu Cloud 管理画面の算出ロジックが面積ベースだったため 確定 D-021
45 「出発した/しなかった」の判定 「予定時刻+猶予30分」をDFA側で計算 DBが持っている状態を読む 猶予時間は設定値なので変わりうる。DFAが計算すると設定変更の瞬間に過去のログの判定まで変わる 確定 D-091
46 ダッシュボードの「予定件数」 清掃ログの行数から数える スケジュールマスタから数える 行数だと、データ収集が止まった日と本当の定休日が区別できなくなる 確定 D-049
47 ダッシュボードの「失敗の内訳」 4分類を常時表示 項目ごと出さない 分類元のデータが存在しない。空リストにすると「失敗ゼロ件」と誤表示される 保留(M3で実装予定) D-048
48 グループ・会社の絞り込み 各画面に選択メニューあり(選んでも何も起きない) UIごと削除 ★→【D-138 で復活。3階層で実動作する】★ 固定の選択肢だけあって何も起きないUIは、不具合と区別がつかない 保留 ★確定(2026-09-16 復活)★ D-053 / ★D-138★
49 事前チェック一覧の既定の絞り込み 「未解消のみ」。サマリの合計が合わなかった 「未解消+対応中」を既定に。合計が合うようにした 対応状況は3状態あるのにサマリが3値で、合計が合う解釈がこれしかなかった 確定 D-054
50 事前チェックのID 数値 文字列 TeamWorks回答で「英字を含むため数値にできない」と判明 確定 D-061
51 深夜をまたぐ終了時刻 DBは「2830」のような4桁数字 「翌04:30」 と整形して返す 運用担当が「28:30」を読み替える負担を持ち込まない 一部保留(英語表示が未決) D-060
52 対応機種の扱い 「対応機種」という概念がコード上に無かった PUDU CC1 のみを対応とし、未対応機種は隠さず「未対応」バッジを付けて表示 隠すと将来ロボットが画面から静かに消え、誰も気づけない 確定 D-087
53 ロボットが正常なときの表示 「待機中」(日本語) Standby.(受領資料の原文どおり) 「待機中」はこちらが勝手に作った日本語で、DBにもメーカー資料にも存在しない値だった 確定 D-098
54 バッテリー残量の表示 表示する想定 恒久的に空欄 DBが現在値しか持たず履歴が無い。過去のアラートに今の残量を貼ると別時点の値を嘘として出すことになる 確定 D-114
55 URLのID番号のチェック 12abc や 1.9 が黙って通っていた 数字だけ・1以上のみ受け付ける 今は実害が無いが、IDを直接DB検索に使うようになると 送ったのと違うデータが返る 確定 D-123
56 アドバイス文言の入力上限 上限なし タイトル100字/本文400字/アクション100字・10件 すぐ隣の備考欄(200字)と同じ保存方式なのに、ここだけ上限が無かった 確定 D-124
57 CSV出力の安全対策 値をそのまま出力 記号で始まる値の頭に1文字足す Excelが 数式として実行 してしまうため。正常な施設名・日付は1文字も変わらない 確定 D-125
58 記号だけの検索 条件が消えて 全件が出ていた 0件 にする 絞り込みが効いていない状態。あわせて記号1文字で26万行を読む経路も消えた 確定 D-134
59 危険なSQLの書き方の禁止 手順書に「注意する」と書くだけ 違反すると機械的に検出される ようにした 文書は次の人が読まなければ素通りする。なお危険な箇所は現時点で0件 確定 D-122
60 動作ログの仕組み 自作/未定 既製の仕組み+ファイル管理はOSに任せる 自作ロガーはエラー本文を落とす罠がある。アプリからファイル直書きはディスク満杯等の失敗要因を抱え込む 確定 D-047
61 作業の保存タイミング レビュー前はコミットしない 検証が通ったらすぐ保存する 2026-08-30、検証済みの成果物20ファイル以上が一括で消え復元できなかった 確定 D-050

3-2 の技術的な裏付け

★裏付けを付けたのは、数字・判定・データ形式が関わる8件(No.32 / 36 / 37 / 45 / 46 / 51 / 52 / 54)です★。 ★残りは保存先・文言・運用手順の話なので付けていません★。

技術的な裏付け — No.32 アドバイスの発火条件を連続日数で判断する 【判定】 lib/judge/advice.ts:50-94 matchAdviceRules(input: AdviceInput): AdviceRuleId[] R1: !departed && consecutiveProblemDays <= 1 R2: completionRate !== null && completionRate < GRADE_THRESHOLDS.poor(10) R3: consecutiveProblemDays >= ESCALATION_THRESHOLDS.level2Days(2) R4: consecutiveProblemDays >= ESCALATION_THRESHOLDS.level3Days(3) B1: completionRate !== null && judgeGrade(completionRate) === "△" B2: status が B2_HANDLE_UP_STATUSES に一致(lib/config/robot-status-match.ts) B3: status が B3_OFFLINE_STATUSES に一致(★このマッチ配列は現在空なので発火しない★) ★複数該当したら全部返す★(D-037。1本に絞ると対応漏れが出る) 【入力は生値】 app/api/clean-logs/[id]/advice/route.ts:81-87(:78-80 に注記) ★表示上限をかけた値を判定に渡さない★ 【文言】 loadAdviceRules()(lib/settings/advice.ts)→ 既定は lib/config/advice-rules.ts ★文言は運用設定で編集できる。発火条件は仕様として固定★(D-036) 【型】 CleanLogAdviceResponse(lib/types/index.ts:366-375) { escalation_level; consecutive_problem_days; consecutive_problem_days_capped; max_level; advices: AdviceItem[] } AdviceItem(:269-277)= { rule; level: 1|2|3; icon; title; estimated_cause; recommended_actions: string[] } 【画面】 components/features/detail-log/CleanLogTable.tsx:219-266 ★行が開けるのは grade が △ か × の行だけ★(:174 / :183) ★取得に失敗しても行は開く★(DetailLogScreen.tsx:227-242。一覧が持っている値で埋める) 【値の例】 ★構造由来★ 連続3日 かつ 完了率 5% の行 → R2・R3・R4 が同時に返る(3件とも表示) ★構造由来★ 完了率ちょうど 70.0 の行 → ★D-139 以降は ○ なので B1 は出なくなった★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.4。 技術的な裏付け — No.36 施設別パフォーマンスに「判定なし」を足した 【判定】 lib/judge/kpi.ts:84-91 judgeFacilityStatus(operationRateValue: number | null): FacilityStatus null → "no_data" >= FACILITY_STATUS_THRESHOLDS.good(90) → "good" >= FACILITY_STATUS_THRESHOLDS.warning(75) → "warning" それ以外 → "bad" lib/config/index.ts:467-472 FACILITY_STATUS_THRESHOLDS = { good: 90, warning: 75 } ★暫定値★ 【型】 FacilityStatus = "good" | "warning" | "bad" | "no_data"(lib/types/index.ts:584) 【画面】 components/features/reports/FacilityPerformanceTable.tsx:121-126 STATUS_LABEL = { good:"良好", warning:"注意", bad:"要改善", no_data:"判定なし" } :137-142 statusColor: good→ok / warning→wn / ★no_data→txM(中立色)★ / bad→ng 【★なぜ必要だったか★】 lib/judge/kpi.ts:78-82 — `clean_timetables` には1週間分の予定がまとめて登録される(D-106)ので ★週の初め・月の初めは「予定はあるが実施0件」が正常★。 ★以前はここを「要改善」に寄せていたため、週の初めに全施設が赤くなっていた★ 【★Pill に渡す色★】 :128-135 — ★16進のカラーコードを渡すこと★。 Pill は `background: ${color}22` で透過背景を作るので、 ★`var(--ok)` を渡すと `var(--ok)22` という不正な値になって背景が出ない★ 【値の例】 ★シード由来★ 施設9: operation_rate 68.0 → "bad" → Pill「要改善」(赤) (lib/fixtures/reports.ts:200-206) ★シード由来★ 施設11: operation_rate 95.2 → "good" → Pill「良好」(緑)(:313-318) 詳しくは docs/データの流れ_画面1-5_20260917.md §4.5。 技術的な裏付け — No.37 表のセルの色分け基準(モックの 80/90 を使わない) 【モック】 d-watch-mock.jsx:802 / :804 ★80 / 90 をハードコード★ 【実装】 components/features/reports/FacilityPerformanceTable.tsx:73-79 rateTone(value, goodTone): null → "muted" / >=90 → goodTone / >=75 → "wn" / else "ng" ★FACILITY_STATUS_THRESHOLDS(good=90 / warning=75)を使う★(lib/config/index.ts:467-472) 【★なぜ★】 :53-71 — 同じ表の「ステータス」列は judgeFacilityStatus() の結果なので、 ★稼働率セルだけ別の数値で色を塗ると「ステータス=良好なのに稼働率セル=警告色」という矛盾が起きる★。 加えて CLAUDE.md §7(閾値のハードコード禁止) 【エラー率だけ別の閾値】 :82-85 errorRateTone(value): null → "muted" / value > FACILITY_TABLE_THRESHOLDS.errorRateWarning(8) → "ng" / else "muted" lib/config/index.ts:485-488 ★8.0 ちょうどは薄字のまま(`>` であって `>=` ではない)★ 【画面側で唯一計算する値】 :98-103 overallDiff(item): number | null op = operation_rate.diff / cv = completion_rate.diff ★どちらかが null なら null★(差を取れない以上、改善とも悪化とも言えない) = Math.round((op + cv) / 2) :105-108 diffText: null か 0 → "→" / 正 → `▲{diff}` / 負 → `▼{|diff|}` 【値の例】 ★シード由来★ 施設9: (68.0-75.0) と (55.0-62.0) の平均 = -7 → "▼7"(赤) ★シード由来★ 施設9: error_rate 12.5 → ★12.5 > 8 なので ng★ → "13%"(赤) 詳しくは docs/データの流れ_画面1-5_20260917.md §4.5。 技術的な裏付け — No.45 「出発した/しなかった」はDBの状態を読む 【やらないこと】 ★「予定時刻+猶予30分」をDFA側で計算しない★(D-091 項1) lib/config/index.ts:178 LAUNCH_GRACE_MINUTES = 30 ★★この定数は、どの判定からも読まれていない★★(`docs/データの流れ_画面1-5_20260917.md` 付録B) 【代わりに読むもの】 lib/db/clean-log-mapping.ts:152-157 notExecutedFromHistory()(第2章 No.3 の裏付けと同じ) 【アラート側の検知日時】 lib/db/index.ts:1366 — `launch_failed` 行の detected_at は ★予定時刻そのもの★(scheduled_at)。 ★猶予30分を足さない★(lib/db/precheck-mapping.ts:1059 付近の注記 / D-091 項1) 【★なぜ★】 猶予時間は設定値なので変わりうる。 ★DFAが計算すると、設定を変えた瞬間に過去のログの判定まで変わる★ 【★未解決★】 ★実際の出発時刻がDBのどこにあるかは未確定★ — `clean_status_histories.started_at` は定義書上「清掃スケジュールの清掃開始日時」= ★予定時刻とも読める★(docs/handoff/analysis/table-definitions-02-clean-error.md:217)。 ★第9回 Q11-1 で「この理解で実装してよいか」を照会中(2026-09-17 送付済み。D-147)★。 ★回答が来るまで「出発失敗」の判定ロジックには着手しない★(D-145 項4)— ★予定時刻の列だった場合、出発失敗が永久に0件になり、例外もテスト失敗も出ない★ 詳しくは docs/データの流れ_画面1-5_20260917.md §3.4(c) / §4.2。 技術的な裏付け — No.46 ダッシュボードの「予定件数」はスケジュールマスタから数える 【DBの列】 clean_schedules.weeks String @db.VarChar(7)(prisma/schema.prisma:556) ★「日月火水木金土」の7桁。清掃する曜日が "1"。起点は日曜★ clean_schedules.inactive Int @default(0) @db.UnsignedTinyInt(:564)★0 = 有効(論理が反転)★ 【集計】 lib/dashboard/aggregate.ts:84-103 computeScheduleProgress(logs, schedules, date) planned = schedules.filter(s => s.active && s.weekdays[dow] === "1").length completed = logs.filter(r => r.departed).length ★「完了」ではなく「出発」★ rate = planned === 0 ? null : completed / planned * 100 曜日の出し方 lib/dashboard/aggregate.ts:67-72 weekdayIndex() は ★Date.UTC で出す★ (`new Date("YYYY-MM-DD").getDay()` は実行環境のタイムゾーンで答えが変わるので使わない) 【★なぜログの行数ではだめか★】 lib/dashboard/aggregate.ts:77-79 / D-049 — ★収集処理が止まって当日の行が0件のとき、「本当に定休日」と「収集処理の障害」が区別できなくなる★ 【食い違いの検知】 app/api/dashboard/route.ts:138-147 logEvent("warn", { event: "data.anomaly" }) で記録する(★原因の断定は書かない★。 docs/logging-design.md §7) 【画面】 components/features/dashboard/ScheduleProgressCard.tsx:21-29 rate === null → 「本日の清掃予定はありません」(★0% のバーを出さない★) それ以外 → `{completed} / {planned} 完了` + ProgressBar components/ui/ProgressBar.tsx:17-27 NaN / Infinity は 0 に、0〜100 にクランプ 【★別概念★】 ダッシュボードの「/ N件」(成功カードの補助行)は ★当日の清掃ログ行数★であって `schedule_progress.planned` とは ★別の数★(DashboardScreen.tsx:201-207。D-049) 詳しくは docs/データの流れ_画面1-5_20260917.md §1.3。 技術的な裏付け — No.51 深夜をまたぐ終了時刻(「2830」→「翌04:30」) 【DBの列】 clean_schedules.start_time String @db.VarChar(4)(prisma/schema.prisma:558) ★HHMM 形式。0000〜2359★(TeamWorks 回答 B-5) clean_schedules.end_time String @db.VarChar(4)(:561) ★★時が 24〜29 を取りうる★★(例: "2830" は翌日4:30) ★日跨ぎで翌6:00 まで設定可能にするための表現。HH:MM として素朴にパースすると壊れる★ 【変換】 lib/format/schedule.ts:35-42 formatStartTime(hhmm: string): string hour が 0〜23 以外 → ★RangeError★ lib/format/schedule.ts:51-61 formatEndTime(hhmm: string): string hour 24〜29 → `翌${pad2(hour - 24)}:${minute}` hour 0〜23 → `${pad2(hour)}:${minute}` それ以外 → ★RangeError(30時以上)★ 【整形の置き場所】 lib/db/index.ts:1696 / :1699 — ★整形は db.schedules()(データ層)が済ませてから Schedule を返す約束★ (lib/schedules/query.ts:28-29。★クエリ層は一切関与しない★) 【型】 Schedule.start_time: string / Schedule.end_time: string(lib/types/index.ts:972-973。★整形済み文字列★。D-060) 【画面】 components/features/schedules/SchedulesScreen.tsx:147-150 {item.start_time} - {item.end_time} ★end_time をパースしないこと★(:33-36) 【並べ替え】 lib/schedules/query.ts:173-175 ★id 昇順★。 ★曜日や時刻で並べない★ — 「翌04:30」のような整形済み文字列では意図した順序にならない 【値の例】 ★構造由来★ "0200" → "02:00" / "2430" → ★"翌00:30"★ / "2830" → ★"翌04:30"★ ★構造由来★ "3000" → ★RangeError(値域外なので静かに変な表示にせず止める)★ ★シード由来★ id27(施設5・日跨ぎ)は fixtures が "00:30"、★DB版は "翌00:30"★ — ★USE_FIXTURES を切り替えると表示が変わる唯一の箇所。DB版が正★(D-086 項10) 【★危ない★】 ★"3000" が1件でも入るとスケジュール画面が 500 になる★(lib/format/schedule.ts:60)。 ★値域24〜29 の根拠は TeamWorks 回答 B-5 だけ★ 【一部保留】 ★英語表示(「翌」の対訳)は未決★ 詳しくは docs/データの流れ_画面6-10_20260917.md §9.3。 技術的な裏付け — No.52 未対応機種は隠さず「未対応」バッジを付ける 【DBの列】 devices.maker_id Int @db.UnsignedTinyInt(prisma/schema.prisma:82。★0〜255★) ★makers テーブルは提供されない★(D-057 A-1) devices.model_name String @db.VarChar(100)(:83) 【対応表】 lib/config/index.ts:620-625 ROBOT_MODELS ★構造由来★ { maker:"PUDU", model:"CC1", supported: true } { maker:"PUDU", model:"MT1", supported: false } { maker:"Keenon", model:"C30", supported: false } { maker:"Keenon", model:"C40", supported: false } lib/config/index.ts:597-600 MAKER_NAMES = { 1: "PUDU", 2: "Keenon" } ★★未知の maker_id は String(maker_id) をそのまま出す。「不明」等に潰さない★★(:593-595。D-077 と同じ思想) 【判定】 lib/format/robot-model.ts:36-45 isSupportedModel(maker: string, model: string): boolean normalize(v) = v.trim().toUpperCase()(:28-30) ROBOT_MODELS.some(e => e.supported && normalize(e.maker)===M && normalize(e.model)===Mo) 【型】 RobotSummary { id: number; maker: string; model: string; serial_number: string }(lib/types/index.ts:917-922) 【画面】 components/features/facilities/FacilitiesScreen.tsx:165 / :166 / :179-181 / :182 {!isSupportedModel(...) && <Pill text={t("未対応")} color={c.wn} />} 【★フロントで判定している理由★】 :172-177 — ★CLAUDE.md §9.6(判定結果はAPIから受け取る)の例外ではない★。 ★DFA側の静的な対応表を引くだけで、APIレスポンスに依存する判定ではないため★ 【★既知の不具合(直さないと判断済み)★】 lib/i18n.ts:161 の `未対応: "Unsupported"` が ★D-Secアラートの「未対応」(=まだ対応していない)と同じキー★。 ★英語表示にすると `Unsupported`(非対応)と出続ける★(D-143 §3 #1。理由は「英語を配布するか未定」)。 ★英語を配布すると決めた時点で必ず直す★ 【値の例】 ★シード由来★ 施設1: PUDU / CC1 / "8111R5628060011" → ★バッジ出ない★ ★構造由来★ maker_id=1 / model_name="MT1" → 「PUDU」「MT1」+★「未対応」バッジ★ ★構造由来★ maker_id=9(表に無い)→ ★"9" がそのまま Pill に出る★+未対応バッジ 詳しくは docs/データの流れ_画面6-10_20260917.md §8.4。 技術的な裏付け — No.54 バッテリー残量は恒久的に空欄 【DBの列】 robots.battery Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:240) ★★現在値のみで履歴が無い★★ 【取得】 lib/db/index.ts:1314 battery_level: null ★★実DB経由では常に null★★(D-090 項8 / D-105) lib/db/index.ts:1375 launch_failed 側も null ★現在値を代入すると、別時点の値を過去のアラートに貼ることになる★(:1311-1313) 【型】 PrecheckItem.battery_level: number | null(lib/types/index.ts:490) 【画面】 components/features/alerts/AlertsScreen.tsx:286-290 ★battery_level !== null のときだけ「残量 {n}%」の行を出す★ 【値の例】 ★シード由来★ 78 / 12 / null(lib/fixtures/prechecks.ts) ★構造由来★ ★実DB経由は常に null → この行は実データでは出ない★ 【★行ごと消さない理由★】 ★fixtures には値が入ったまま=画面の残量表示の経路を残すため★(D-105 項5)。 ★「記録が無い差分」ではなく意図した仕様★(D-143 §3 #8 で確認済み) 詳しくは docs/データの流れ_画面1-5_20260917.md §3.4(e)。

3-3. 影響が小さい変更(表示の細部・内部の作法)

# 変更したこと 変更前 → 変更後 なぜ 決定
62 完了率ちょうど10.0%の判定 × → △ 「× = 10%未満」という確定仕様と矛盾していた( 境界値は下のランクに落ちる規則で統一 ★→【2026-09-16 D-139 で規則が逆になりました。★境界はすべて「上のランクに入る」★。ただし ★10.0 が △ である結論だけは変わりません★】★) D-020b / ★D-139★
63 パーセントの表示桁 小数第1位まで(91.4%)→ 整数(91%) DFA判断。ただし 判定は丸める前の生値 で行う D-020
64 判定基準の書き方 「10〜69%」→「10%以上70%以下」 ★→【2026-09-16 D-139 で「10%以上70%未満」に変更】★ 「10〜69%」だと69.5%がどこにも該当しない読み方になる D-062 / ★D-139★
65 「グループなし」の表し方 空欄可 → 数字の 0 TeamWorks が検索効率を理由に採用 D-011
66 一覧の並び順 仕様書に記載なし → 固定の並び順を明記 実装者ごとに解釈が変わりうる状態だった D-052 / D-055
67 入力エラーの返し方 どの項目が不正か分からない → 失敗した項目名を返す 3本とも情報は持っていた。1本だけ直すと不整合が増えるため3本同時に対応 D-064
68 重複していたテーブル 使わないと決定 完全に同一構造で、一度も行が入っていない D-058
69 「上位10件に戻す」の見た目 グレー文字 → 隣のボタンと同じリンク色 モックどおりだと押せると分からない。モック準拠ルールの意図的な例外 D-121
70 エラー種別バーの色数 7色 → 5色 共通部品が5色しか持たず、7色に意味づけする仕様も無かった D-069
71 空データ時の文言 モックが持っていない → 「該当なし」等を新規に追加 モックは静的なダミーだったため空の状態が存在しなかった D-069
72 メーカー名の対応 (対応表なし)→ 1=PUDU / 2=Keenon と 仮に割り当て DBから対応表が提供されない前提のため D-085
73 実在の住所・電話・人名 モックとAPI契約に残存 → ダミー値に置換 API契約ファイル本体にも実在の人名が2箇所残っていた D-043 / D-083
74 確認方法のルール AIに聞いて確認 → 検索・テスト実行・ログ実測で確認 実在しない名前を断定的に答えた事例があったため D-051

3-3 の技術的な裏付け

★裏付けを付けたのは、数字・判定・データ形式が関わる4件(No.62 / 63 / 65 / 72)です★。

技術的な裏付け — No.62・No.63 判定と表示(境界値の期待値表) 【判定】 lib/judge/grade.ts:29-35 judgeGrade(rate: number | null): Grade rate === null → "×" rate >= GRADE_THRESHOLDS.excellent(90) → "◎" rate >= GRADE_THRESHOLDS.good(70) → "○" rate < GRADE_THRESHOLDS.poor(10) → "×" それ以外 → "△" lib/config/index.ts:18-25 GRADE_THRESHOLDS = { excellent: 90, good: 70, poor: 10 } ★構造由来★ ★★境界はすべて「上のランクに入る」★★(D-139。2026-09-16) ★★× の境界だけ「未満」のまま → 10.0 は △★★ 【表示】 lib/judge/grade.ts:43-46 displayRate(rate: number | null): string null → "-" / それ以外 → `${Math.round(rate)}%` 【型】 Grade = "◎" | "○" | "△" | "×"(lib/types/index.ts:17) 【画面】 components/features/detail-log/CleanLogTable.tsx:209(完了率)/ :212(GradeBadge) components/ui/GradeBadge.tsx:9-15 ◎ / ○ → "ok" / △ → "wn" / × → "ng" 【★同じ規則が2箇所にある★】 lib/db/clean-log-mapping.ts:287-289 の isProblemCompletion() は `rate === null || rate < GRADE_THRESHOLDS.good` で、★judgeGrade() と結果が常に一致する★。 ★片方の比較演算子だけを動かさないこと★(D-139 以前は `<=` が正しかった) ★期待値表★(CLAUDE.md §9.4 / lib/fixtures/clean-logs.ts:82-92。★シード由来★) | 完了率 | 判定 | 表示(整数丸め) | 注意 | |---|---|---|---| | 90.1 | ◎ | 90% | ★丸めると90だが◎★ | | 90.0 | ◎ | 90% | ★D-139 で ○ から変わった★ | | 89.5 | ○ | 90% | ★表示は90%だが判定は○★ | | 70.1 | ○ | 70% | | | 70.0 | ○ | 70% | ★D-139 で △ から変わった★ | | 10.1 | △ | 10% | | | 10.0 | △ | 10% | ★「10%未満」が×なので △★ | | 9.9 | × | 10% | | | 0 | × | 0% | ★「0%で実施した」★ | | null | × | - | ★未実施★ | 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(h) / §2.3(i)、_画面6-10_20260917.md §10.3。 技術的な裏付け — No.65 「グループなし」を数字の 0 で表す 【DBの列】 customers.group_id BigInt @default(0) @db.UnsignedBigInt(prisma/schema.prisma:96。★NOT NULL★) ★0 = ホールディングスなしのセンチネル★(D-011。TeamWorks が検索効率を理由に採用) 【変換】 lib/format/group-id.ts:77-79 groupIdFromDb(groupId: number): number | null return groupId === GROUP_ID_NONE ? null : groupId; lib/config/index.ts:651 GROUP_ID_NONE = 0 ★構造由来★ ★★Number() で BigInt を落としてから渡すこと★★(lib/format/group-id.ts:73-75)— ★`0n === 0` は false★ なので BigInt のままだと静かに外れる 【取得】 lib/db/index.ts:1636 const group_id = groupIdFromDb(Number(row.customer.group_id)); :1646 group_name: group_id === null ? null : (row.customer.group?.group_name ?? null) ★「所属なし」のときは名前も null★ 【型】 Facility.group_id: number | null(lib/types/index.ts:939)/ group_name: string | null(:941) 【画面側の送信】 lib/masters/options.ts:222-227 appendClassificationParams() ★`>= 1` のときだけ set する★ — ★`group_id=0` を漏らさないため★ 【★危ない★】 ★`=== 0` をルート・fixtures・DataSource に散らさないこと★(lib/format/group-id.ts:54-57)— 1箇所直し忘れると ★「グループ0」という存在しないホールディングスが画面に出る★。 ★例外もテスト失敗も出ない★ 【★未解決★】 ★`group_id = 0` はローカルDBへ投入できない★ — `customers.group_id` に実FK制約(`customers_group_id_foreign` → `groups.id`)があり、 `groups.id = 0` の行が無いため外部キー違反になる(D-138 項6。第9回 Q7-4 で照会中) 【値の例】 ★シード由来★ 施設1: group_id=1 / group_name="バリューホールディングス" ★シード由来★ 施設12: ★group_id=null / group_name=null★(lib/fixtures/facilities.ts:265-269) 詳しくは docs/データの流れ_画面6-10_20260917.md §8.6。 技術的な裏付け — No.72 メーカー番号の対応表(1=PUDU / 2=Keenon) 【設定】 lib/config/index.ts:597-600 MAKER_NAMES: Record<number, string> = { 1: "PUDU", 2: "Keenon" } ★DFA側で仮に割り当てた番号★(D-085。★makers テーブルは提供されない★。D-057 A-1) 【変換】 lib/db/clean-log-mapping.ts:27-29 formatModelName(makerId: number, modelName: string): string → `${MAKER_NAMES[makerId] ?? String(makerId)} ${modelName}` lib/db/index.ts:1657(施設マスタ側)も同じ `?? String(maker_id)` ★★未知の maker_id は数字をそのまま出す。「不明」等に潰さない★★(lib/config/index.ts:593-595)— ★対応表に足す必要があることに気づけるようにするため★ 【値の例】 ★構造由来★ maker_id=1, model_name="CC1" → "PUDU CC1" ★構造由来★ maker_id=9(表に無い), model_name="X1" → ★"9 X1"★ ★シード由来★ fixtures の既定値は "PUDU CC1"(lib/fixtures/clean-logs.ts:50) ★記録済み実測★ `devices` は1行(maker_id=1 / model_name="CC1")=★当方由来★ (docs/handoff/テストDBのデータ出所_20260917.md:96。2026-09-17) 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(b)。

第4章 — 追補: 2026-09-15〜09-17 に決めたこと(D-135〜D-150)

★この章は 2026-09-17 に足しました★。初版(2026-09-14)の本文は第1〜3章で、 ★そこに書かれていない16件の決定★をここにまとめています。 ★書き方は第2章・第3章と同じ★です(何を変えたか / なぜか / 決定番号)。

4-1. この3日間で何が起きたか(要約)

日 何をしていたか
★2026-09-15★ ★ダッシュボードに「要レスキュー店舗」を新設★し、★詳細ログを2年分見られるように★した
★2026-09-16★ ★グループ・会社の絞り込みを復活★/★判定の境界を「含める」に変更★/★清掃面積の新画面★/溜まっていた作業の詰まりを外した
★2026-09-17★ ★責任分界の訂正★/★規模の前提の置き直し★/★「実測」と呼んでいた数字の多くが当方のダミー由来と判明★/第9回確認事項の送付

★このうち★★最も重い2件★★は、★判定境界の変更(D-139)★と ★「本番DBに入っている行は全部こちらのダミーだった」という発見(D-145 / D-148)★です。

4-2. 1件ずつ

1. 要レスキュー店舗をダッシュボードに新設した(D-135 / D-137)★モックに無い画面★

  • 変更前: ダッシュボードに「今すぐ手を打つべき店舗」を示すものが無かった
  • 変更後: 詳細ログの上に 「要レスキュー店舗」カードを新設。★3つの条件のどれかに当たった施設★を出す
    • ★未実施が3日以上連続★ / ★連続問題レベル3★ / ★完了率が前回比20%以上の急落★
  • なぜ: ユーザー指示(2026-09-15)。★毎朝かならず見に来る確認項目★なので、 0件のときもカードごと消さず「0件でした」と分かるようにしています
  • 理由は1つに丸めません: 3つとも当たったら3つとも表示します。 「なぜレスキューが要るのか」が運用担当の判断材料そのものだからです
  • ★4つ目の条件(出発失敗を未実施と別に数える)は落としました★ — ★実際の出発時刻がDBのどこにあるか不明★で、 推測で作れないためです(第9回 Q11-1 で照会中)
  • あとで直した点(D-137): 当初「連続日数の降い順に5件まで」にしていたところ、 ★急落だけで該当する施設が構造的に一覧から落ちていました★。 ★APIは全件返し、画面が初期5件+「もっと表示」を出す★形に変えています。 ★件数表示も「切ったあとの数」ではなく「本当の総数」になりました★
技術的な裏付け 【DBの列】 clean_timetables.start_at DateTime @db.DateTime(0)(prisma/schema.prisma:589) clean_status_histories.clean_error Int UnsignedTinyInt(:487) clean_status_histories.clean_ratio Float(double(4,1))(:497) clean_status_histories.clean_area Float(double(12,6))(:492) clean_status_histories.task_area Float(double(12,6))(:491) ★clean_area / task_area の2列は本件のために取得に足した★(D-135 項6。増分は約4MB の外挿) 【取得】 app/api/dashboard/route.ts:101 db.rescueRequired({ date, facility_ids: facilityIds }) prisma : lib/db/index.ts:1732〜 → loadConsecutiveInputs()(lib/db/consecutive-source.ts) fixtures: lib/db/index.ts:542〜 → rescueInputsFromCleanLogs()(lib/db/rescue-mapping.ts:47-64) 【中間型】 ConsecutiveInput(lib/db/clean-log-mapping.ts:292-319) { id; facility_id; scheduled_at; completion_rate: number | null; clean_area: number; task_area: number } ★clean_area / task_area は履歴が無ければ 0(null にしない)★ — 面積は「足し合わせる量」なので 0 を足すのが正しい ★★`??` を `||` にしないこと★★ — 面積は 0 が正当な値 【判定】 lib/judge/rescue.ts:241-268 judgeRescue(input: RescueInput): RescueJudgement R-1 not_executed_streak : countNotExecutedStreak(days) >= RESCUE_THRESHOLDS.notExecutedDays(=3) (lib/judge/rescue.ts:177-184 / lib/config/index.ts:69-72) R-2 escalation_l3 : judgeEscalationLevel(consecutive) === "L3"(:193-195) R-3 completion_drop : (前回 − 当日) ÷ 前回 >= COMPLETION_DROP.dropRatio(=0.2) (:219-228 / lib/config/index.ts:48-51) lib/judge/rescue.ts:117-125 facilityDayRate(rows): number | null = completionRate(Σclean_area, Σtask_area) ★面積加重。単純平均ではない★ ★分母0は null★(lib/judge/kpi.ts:20-23) ★「前日」はカレンダー前日ではなく「前回のスケジュール存在日」★(D-135 項3-A / D-026) ★前回0% / 前回未実施 / 当日未実施 / 上昇 は急落としない★(同 B〜E) ★R-1 の「3日」は ESCALATION_THRESHOLDS.level3Days を流用せず別の設定値にした★(D-135 項7-1)— ★たまたま両方3だが意味が違う。流用すると片方を変えたときにもう片方まで動く★ 【型】 RescueRequiredItem(lib/types/index.ts:1047-1072) { facility_id; facility_name; evaluated_on; reasons: RescueReason[]; not_executed_days; consecutive_problem_days; completion_rate: number | null; previous_completion_rate: number | null; drop_ratio: number | null } RescueReason = "not_executed_streak" | "escalation_l3" | "completion_drop" (★正は lib/judge/rescue.ts:34★) 【API】 GET /dashboard の rescue_required(★新規エンドポイントは作らない★。 app/api/dashboard/route.ts:155-157。★件数で切らない=全件返す★) ★completion_rate / previous_completion_rate / drop_ratio は丸めない生値を返す★ 【画面】 components/features/dashboard/RescueRequiredCard.tsx :91 rescueVisibility(items, expanded)(lib/dashboard/rescue-visibility.ts:37-47) canToggle = items.length > RESCUE_INITIAL_VISIBLE(=5。lib/config/index.ts:102) ★★「超える」ときだけ true。ちょうど5件なら出さない(>= にしない)★★ :99 件数表示は ★items.length(絞る前の総数)★ :52-73 日本語を作るのはこの層(D-111) completion_drop → t("完了率が前回比{n}%低下")、n = Math.round(drop_ratio * 100) :131 判定日 evaluated_on(★リクエストの date とは限らない★) 【値の例】 ★構造由来★ 未実施3日連続の閾値 3 / 急落の閾値 0.2(→「20%低下」) ★構造由来★ 前回 30% → 当日 18% は drop_ratio = (30-18)/30 = 0.4 → 「40%低下」 (★差で見ると「12ポイント」だが、ユーザーは比を選んだ★) 【★母集団が違うのは意図★】 ★当日に清掃予定が無い施設も要レスキューには出す★ (各施設の「当該日以前で最も新しいスケジュール存在日」で評価する)。 ★ダッシュボードの数値カードには入れない★ので、件数が一致しなくて構わない 【★出所の注意★】 ★D-137 の「実データで7件中2件が消えていた」という記述の「実データ」は 当方のシード由来でした★(D-145 で訂正済み)。 ★★結論(APIで件数を切らない)は変わりません★★ — ★並べ方と切り方の組み合わせの問題で、 データに依存しないため★ 詳しくは docs/データの流れ_画面1-5_20260917.md §1.5。

2. 清掃詳細ログを2年分見られるようにした(D-136)

  • 変更前: 期間の指定は最大31日まで
  • 変更後: ★最大2年(731日)★。★日付を指定しないときは上限なし★。 ★画面を開いたときの初期表示は「今日1日」のまま(変えていません)★
  • なぜ: ユーザー指示(2026-09-15)「詳細ログは2年分見れるように」。 ★「31日」は初期表示ではなく上限だった★ので、上限だけを広げました
  • 730ではなく731の理由: ★2年のあいだにうるう日が入りうる★ため(両端を含む日数)
  • CSVには別の上限を置きました: ★日数ではなく「行数」で 20,000行★。 同じ「2年」でも、★1店舗だけなら軽く、全店舗だと200倍重い★ためです。 超えるときは★ボタンを押せなくし、同じ場所に理由を出します★
技術的な裏付け 【設定値】 lib/config/index.ts:360 MAX_DATE_RANGE_DAYS = 731 ★構造由来★(31 から変更。定数名は変えていない) lib/config/index.ts:385 MAX_CSV_ROWS = 20_000 ★構造由来・暫定値★ 【期間の検証】 lib/clean-logs/query.ts:107-117 ★上限がかかるのは date_from と date_to の「両方」が指定されたときだけ★ (★「未指定なら上限なし」は追加実装が不要だった。既存の作りをテストで固定した★) 422 の実物(lib/clean-logs/query.ts:110-115): { "error": { "code": "validation_failed", "message": "date_from〜date_toは731日以内で指定してください", "details": { "date_to": ["指定された範囲は900日です"] } } } 【CSV の上限】 ★上限は「含む」★ — ちょうど 20,000行 は出せて、20,001行から 422 (★`>` であって `>=` ではない★) サーバ … ★絞り込みを適用したあと、CSVを組み立てる「前」に行数を見る★。 ★組み立ててから長さで判断しない★(25万行ぶんの文字列を作ってから捨てるのでは メモリを使い切る危険が残る) 画面 … DetailLogScreen.tsx:162 const csvBlocked = total > MAX_CSV_ROWS; ★一覧を取ったときの meta.total をそのまま使う(CSVのために問い合わせを増やさない)★ ★サーバと同じ向きにすること★(:155-157)— ★片方だけ `>=` にすると、押せるのに422が返る(またはその逆)★ :451-468 ★上限超過時は `<a>` ではなく `<button disabled>` に差し替える★ — ★`<a>` に `disabled` 属性は無く、付けても押せてしまう★ DetailLogScreen.tsx:463 は `<a href download>`。★router.push を使わない★ — RSC のプリフェッチで ★エクスポートが2回走る★ 【422 の details】 ★数値の形★(`{ total: 25000, max: 20000 }`)— ★文言から数字を読み取らせないため★ (lib/types/index.ts:53-56。★この形を許すのは /clean-logs/export と /reports/kpi の2箇所だけ★) 【★この上限の根拠は暫定★】 lib/config/index.ts:373-377 — ★「31日・502行で0.73秒」という1点からの外挿で、その 502行・31日はいずれも 当方の scripts/seed-local-db.ts が決めた値★(D-145。2026-09-17)。 ★本番の清掃回数は不明★。★実データと索引が入ったら測り直すこと★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.1 / §2.6。

3. グループ・会社の絞り込みを復活させた(D-138)

  • 変更前: マスタが無いため、選択メニューごと画面から削除していた(D-053)
  • 変更後: ★ホールディングス → 屋号(ブランド)→ 店舗★ の3階層で復活。5画面に絞り込みが通りました (清掃詳細ログ / アラート / レポート / D-Secログ / D-Secアラート)
  • なぜ: マスタ(/groups /customers)が揃ったため
  • ★見出し語は「屋号(ブランド)」★(ユーザー確定 2026-09-15「屋号でよいです、ただ顧客によってはない場合もあります」)。 ★1箇所にまとめてあるので、TeamWorks の回答が来たら1行で直せます★
  • ★レポートのKPIは「対象の会社が1社に決まるときだけ」出します★ — 複数社を合算した数字は出せません(★合算するための材料がDBに無い★)。 決まらないときはエラーではなく「屋号を選んでください」という案内を出します
技術的な裏付け 【対象顧客の決定】 lib/reports/target-customer.ts resolveTargetCustomer(customers, query) customer_id 指定 → その顧客で絞る(★存在確認はしない★。該当が無ければ 200・空) group_id 指定 → 傘下の顧客がちょうど1社ならその顧客。★2社以上は 422★ どちらも未指定 → 顧客がちょうど1社ならその顧客(P1 はここ)。★2社以上は 422★ ★group_id 指定で傘下0社も 422(候補は空配列)★ — 0社も「定まっていない」ため ★★静かに1社を選ばない★★(D-133 の方針を継承) 【422 の実物】 app/api/reports/kpi/route.ts:103-110 { error: { code: "validation_failed", message: TARGET_CUSTOMER_REQUIRED_MESSAGE, details: { customer_ids: [1, 2] } } } ★details は「数値の配列」★(d-watch-api.yaml:693-703)— ★画面が文言を読まずに次の操作を出せるようにするため★ ★500 ではなく 422★ — サーバの不具合ではなく、指定が足りないという話 【施設IDへの解決】 lib/facilities/resolve-by-master.ts:88-93 resolveFacilityIdsByMasters() lib/facilities/intersect-ids.ts intersectFacilityIds()(★AND。積が空なら [] を返し null にしない★) lib/facilities/filter-by-ids.ts:48-55 filterRowsByFacilityIds() ★DataSource が引数を取らない4本(ds-devices / ds-alerts / facilities / schedules)は ルート側でメモリフィルタしている★ 【型】 Facility.customer_id: number(lib/types/index.ts:931。★null にならない★) Facility.group_id: number | null(:939)/ group_name: string | null(:941) 【画面側の送信】 lib/masters/options.ts:218-229 appendClassificationParams() ★`>= 1` のときだけ set する(group_id=0 を漏らさない)★ 【却下した案(記録)】 全顧客を合算する → ★稼働率・エラー発生率を合算する列がサマリに無い★ 率の加重平均で近似する → ★TeamWorks が計算した値ではなくなり、検算可能性を失う★(D-022) 500 のまま → ★2社目が入った瞬間、レポート画面が常時500になり運用が止まる★ 【値の例】 ★シード由来★ fixtures は4顧客あるので、★USE_FIXTURES=true では分類を選ぶまで 422★ (★実運用は対象顧客1社のみなので発火しない。開発時に必ず踏む★) 【★未解決★】 ★`group_id = 0` はFK制約でローカルDBに投入できない★(第9回 Q7-4) 詳しくは docs/データの流れ_画面1-5_20260917.md §4.2。

4. ★判定の境界を「含める」に変えた(D-139)★★この追補でいちばん重い変更★★

  • 変更前: 「◎は90%超」「○は70%超」(境界値は下のランクに落ちる)
  • 変更後: ★「◎は90%以上」「○は70%以上」★(境界値は上のランクに入る)
  • ★変わったのは 90.0(○→◎)と 70.0(△→○)の2つだけ★です。 ★×の境界(10%未満)は変えていません★ので、★10.0 は △ のまま★です
  • なぜ: ユーザー指示(2026-09-16)「◯、×は90%以上のように境はふくめてください」。 ★×側は同日「10%未満を×としてください」で確定★
  • ★閾値の数値(90 / 70 / 10)は1つも変えていません★。変えたのは比較の向きだけです
  • ★連動して直したものがあります★ — 「その日が問題のある日か」を判定する箇所が ★70.0 を問題ありのまま数え続けていた★ため、そこも直しました。 ★直し忘れても例外もテストの失敗も出ず、完了率ちょうど70.0の日だけが静かに問題日として数えられ続けます★
技術的な裏付け 【変えた場所】 lib/judge/grade.ts:29-35 judgeGrade() `rate > excellent` → ★`rate >= excellent`★ / `rate > good` → ★`rate >= good`★ ★`rate < poor`(× の境界)は変えていない★ → ★10.0 は △ のまま★ ★GRADE_THRESHOLDS の数値(90 / 70 / 10)は変えていない★(lib/config/index.ts:18-25) 【★連動して直した箇所(ここがいちばん危なかった)★】 lib/db/clean-log-mapping.ts の isProblemCompletion() `completionRate <= GRADE_THRESHOLDS.good` → ★`<`★(D-109 決定3 の撤回) ★judgeGrade() と入力が違うだけの同じ規則★なので、片方だけ動かすと規則が割れる ★直し忘れても例外もテスト失敗も出ない★。波及の順: 連続問題日数 → L1 / L2 / L3 → R3 / R4 のアドバイス → 要レスキュー店舗の R-2(連続問題レベル3) → レポートの「連続問題施設」 【丸めの事故の「向き」が逆になった】 旧: 「90.4 を丸めると 90 になり ◎ が★消える★」 新: 「89.5 を丸めると 90 になり、無いはずの ◎ が★生まれる★」(69.5 → 70 で ○ も同じ) ★どちらにせよ「表示用に丸めた値で判定しない」は変わらない★ (lib/judge/grade.ts のアンチパターン記録テストを新しい向きで固定し直した) 【画面の文言も直した】 components/features/detail-log/CleanLogTable.tsx:135-164(「判定 ?」の凡例)/ components/features/system/SystemScreen.tsx:90-98(判定閾値のセクション) ★閾値を直書きせず GRADE_THRESHOLDS から出す★ ★「10-69%」のような表記にしないこと★ — ★69.5 がどこにも該当しない読み方になる★(D-062)。 ★正しくは「10%以上70%未満」★ 【★fixtures に手書きの矛盾が残っていた★】 lib/fixtures/clean-logs.ts:96 — 完了率 70.0 の行(施設5)に `consecutive_problem_days: 1`(旧規則の値)が手書きで残っており、 ★USE_FIXTURES の true/false でこの1行だけ挙動が食い違っていた★。 ★この行を固定するテストが無かったため 2,048件のテストをすべて通過していた★。 ★0 に修正し、「fixtures 全行の判定が判定関数の戻り値と一致する」不変条件のテストを新設★ 【値の例】 ★構造由来★ 90.0 → ◎(旧 ○)/ 70.0 → ○(旧 △)/ 10.0 → △(変わらず) 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(i)、_画面6-10_20260917.md §10.3。

5. 進行中の期間のKPIも会社で絞るようにした(D-140)

  • 変更前: ★確定した週・月は選んだ会社の数字、進行中の週・月は全社合算★になっていました
  • 変更後: ★どちらも同じ会社の数字★になります
  • なぜ: D-138 で確定期間だけを絞り、★進行中の期間を絞り忘れていた★(D-138 自身が残課題として記録していた)。 ★P1は対象顧客が1社なので実データでは表に出ませんが、2社目が入った日に食い違います★
  • ★サンプルデータでは絶対に見つからない不具合でした★ — サンプルデータ側は4社とも正しく動いていて、実DBを読む経路だけがこの不具合を持っていました
  • あわせて: レポート画面は★4本のデータを1本ずつ出す★ようにしました。 ★以前はKPIだけが「会社が決まらない」状態でも画面全体がエラー表示になっていました★
技術的な裏付け 【直した場所】 app/api/reports/kpi/route.ts:130-133 resolveTargetCustomer() が対象顧客を1社に決めた★後★、 resolveFacilityIdsByMasters({ groupId: null, customerId: target.customerId }) で施設IDを解決し、 reportKpis() の当期ブランチ(clean_timetables を読む側)の where に `facility_id: { in: [...] }` を足した 【★リクエストの group_id から直接引かないこと★】(指示書AL §2-3 / 落とし穴1)— 確定期間は customer_summaries(★顧客単位★)、当期は詳細テーブル(★施設単位★)なので、 ★2つの軸が同じ顧客を指していないと「確定期間は1社・当期は全社」という食い違いが起きる★ ★確定期間の経路は1行も変えていない★(D-138 で査読済みのため) 【確定期間と当期の違い】 確定期間 … customer_summaries.operation_ratio / launch_ratio / clean_ratio / error_ratio (prisma/schema.prisma:699-702)を ★そのまま読む★(D-102。★履歴から再計算しない★) lib/db/index.ts:1857〜 ★要求された期間+その1つ前(前期比の材料)の2期間だけ引く★ 当期 … CLAUDE.md §4.2 の式で詳細テーブルからその場集計 lib/judge/kpi.ts:20-23 rate(numerator, denominator): number | null ★分母0は null★ lib/judge/kpi.ts:26-46 operationRate / launchRate / completionRate / errorRate 【前期比】 lib/judge/kpi.ts:58-64 toKpiValue(value, prev): KpiValue = { value, prev, diff: value === null || prev === null ? null : value - prev } ★★どちらかが null なら diff も null(0 にしない)★★ 【★近似の申し送り★】 lib/db/report-mapping.ts:212-219 — ★詳細データは「出発したか」の1ビットしか持たない★ため、 ★稼働率と出発成功率は同じ分子を使っている★。 ★「定刻より遅れて出発した」を区別する列がDBに無い★(第9回 11章で照会中) 【画面】 components/features/reports/ReportsScreen.tsx:190-205 ★Promise.allSettled★(Promise.all ではない) lib/reports/block-visibility.ts ★どのブロックを出すかは純粋関数に切り出した★ :458-512 KPI が 422 のときは ★「選べる屋号が{n}件あります」+「屋号を選ぶ →」ボタン★を出し、 ★実際に屋号セレクトへフォーカスを移す★(:147-149。★誘導を文言だけで終わらせない★) ★4本とも失敗したときだけ従来どおり画面全体のエラーにする★ 【記録済み実測(★当方のローカルDB。dwatch_local★)】 当週の完了率が 顧客1 = 57.613889 / 顧客2 = 52.407143 / 顧客3 = 46.44 となり、独自に組んだSQL検算値と小数まで一致。 ★不具合の発見時、APIが返していたのは全社の値(55.27)だった★ 詳しくは docs/データの流れ_画面1-5_20260917.md §4.2。

6. 「清掃面積」の画面を新設した(D-141)★モックに無い画面★

  • 変更前: 清掃した面積を累計で見る画面が無かった
  • 変更後: 左メニュー「モニタリング」の4つ目として ★「清掃面積」★ を新設。 ★ホールディングス / 屋号 / 店舗の粒度を切り替えられ、期間でも絞れます★
  • なぜ: ユーザー指示(2026-09-15 / 16)
  • ★開いた瞬間が「累計」★です(期間の既定が「全期間」)。 ★ただしデータの保持は2年なので、厳密には「直近2年の合計」★です。★画面にもその旨を書いてあります★ (★3年目に数字が減ったときの問い合わせを防ぐため★)
  • ★集計表(サマリ)は読まず、常に履歴から合計します★ — この画面の期間は自由な日付範囲ですが、★集計表は「年/月/週」の粒度でしか存在しない★ためです
  • ★帰結: レポート画面の完了率と、この画面の完了率は数字が食い違いえます★(経路が違うため)。 ★どちらかが壊れているわけではありません★
  • 単位は「㎡」(ユーザー確定 2026-09-16)。★ただしTeamWorksからの確認は取れていません★。 ★1箇所にだけ置いてあるので、回答が違っても1行で直せます★
技術的な裏付け 【DBの列】 clean_status_histories.clean_area Float @default(0.000000)(★double(12,6)★)(prisma/schema.prisma:492) clean_status_histories.task_area Float @default(0.000000)(:491) clean_status_histories.started_at DateTime @db.DateTime(0)(:498) 【★絞る対象★】 lib/db/index.ts:1802 const range = dbDateRange(query.date_from, query.date_to) ★★started_at(実績)であって clean_timetables.start_at(予定)ではない★★ — ★履歴が無い清掃(未実施)はそもそも面積を持たない★ ★集計と番人で「同じ where」を使う★(lib/db/index.ts:1805-1814)— 別々に書くと ★片方だけ期間を絞り忘れても誰も気づけない★ 【集計クエリ】 lib/db/index.ts:1817-1824 prisma.cleanStatusHistory.groupBy({ by: ["facility_id"], where, _sum: { clean_area: true, task_area: true }, _count: { _all: true }, ← ★清掃実施回数=履歴の行数(予定の数ではない)★ }) 【★番人★】 lib/db/index.ts:1826-1831 同じ where で clean_timetable_id ごとの件数を数え、 `having: { _count: { gt: 1 } }` が1件でもあれば warn を1行出す ★1つの清掃に履歴が2行以上あると SUM() が同じ面積を2回数える★。 ★例外もテスト失敗も出ず、数字が大きくなるだけ★(第9回 Q2-3 で照会中) ★原因の断定は書かない★(docs/logging-design.md §7) 【★BigInt の罠★】 lib/db/clean-area-source.ts:132-134 — ★facility_id は BigInt で返るので Number() に通すこと★。 通さないと Map のキーが `1n` と `1` で別物になり、 ★★全施設が「マスタに無い施設」に化けて合計が 0 になる★★ 【施設マスタ】 lib/db/clean-area-source.ts:161-192 — ★`where` を足さないこと★(:153-155)。 ★`deleted_at` で絞ると、その施設の面積が内訳から落ちるだけでなく、全体合計そのものが減る★ 【ロールアップ】 lib/clean-area/aggregate.ts:144-209 aggregateCleanArea(facilityStats, facilities, scope) completion_rate = completionRate(bucket.clean_area, bucket.task_area)(:194。★分母0は null★) 並び順(:219-225)★clean_area の降順 → 同値なら id の昇順 → id === null は末尾★ ★バケットのキーは文字列★(:72-74)— ★数値キーの Map に null を混ぜると 「グループなし」がグループID 0 と同じ扱いに化ける★ 【型】 CleanAreaRow(lib/types/index.ts:1205-1241)/ CleanAreaTotal(:1248-1253)/ CleanAreaResponse(:1286-1290) 【API】 app/api/clean-area/route.ts:101-106 ★meta.total は items の件数(清掃回数ではない。清掃回数は total.executed_count)★ ★unmatched_facility_ids は返さない(異常の通知先はログであって画面ではない)★ ★★単位("㎡")もレスポンスに入れない★★ 【整形】 lib/format/number.ts:46-52 formatNumber(value: number): string !Number.isFinite(value) → ★"-"(∞ を画面に出さない)★ それ以外 → new Intl.NumberFormat("en-US").format(Math.round(value) + 0) ★Math.round を先に通す★(端数処理の規則を Intl の実装依存にしないため) ★ロケールを "en-US" に固定する理由★: 引数なしの toLocaleString() は実行環境のロケールを使うので ★サーバとブラウザで同じ数値が違う文字列になり、ハイドレーション不整合になる★ lib/format/number.ts:61-63 formatArea(value) = `${formatNumber(value)}${AREA_UNIT}` lib/config/index.ts:697 AREA_UNIT = "㎡" ★1箇所に置く。画面やAPIに直書きしない★ 【画面】 components/features/clean-area/CleanAreaScreen.tsx:293-324(数値カード4枚)/ :330-334(「期間を指定しないときは全期間の合計です。ただしデータの保持は2年…」)/ :411-426(もっと表示。CLEAN_AREA_INITIAL_VISIBLE = 10。lib/config/index.ts:705) ★日数の上限は設けない★(lib/clean-area/query.ts:14-18)— ★1本の集計クエリで、返るのは粒度ごとの行数(最大でも施設数)。期間を広げても行数は増えない★ 【値の例】 ★構造由来★ 341635.4 → "341,635" → "341,635㎡" / NaN・Infinity → "-" 【★出所の注意★】 ★かつてここに書かれていた「全24行の task_area が 1000 ちょうど」という実測は、 当方のシード(scripts/seed-local-db.ts:1669-1670)を見たものでした★(D-145 で訂正済み)。 ★TeamWorks の集計式への当て推量は取り下げ。結論(サマリを読まない)は構造由来で独立に成立★ 詳しくは docs/データの流れ_画面1-5_20260917.md 第5章。

7. 溜まっていた作業の詰まりを外した(D-142)

  • 何をしたか: コードを書き足す作業ではなく、★止まっていたものを動かす作業★です
    • ★4日間・5作業が触るのを避け続けていた未保存の変更★を保存した
    • ★セキュリティの申し送り4項目を、社内メモから外部に出せる文書に起こした★
    • ★PR(本流への取り込み)の説明文を最新化した★
    • ★TeamWorks への第9回確認事項が「未送付」であることを確定させた★
  • なぜ: ★第1回・第4回で「送付した記録」を落とし、回答済みなのに「送付待ち」のまま2回引き継いだ事故がある★ため。 ★今回が3回目になりかけていました★
  • ★併せて判明したこと★: 実在のDBユーザー名がコードの既定値として置かれていました。 ★環境変数を読むコードでも、書いてある既定値は実質のハードコードです★。 ★同じ値が2回コードに入りかけていた★ので、再発防止の注記を入れました

8. ★責任分界を訂正した(D-143)★

  • 誤っていた内容: 「本番のアプリサーバー(VM)はDFAが用意・構築する」
  • 正しい内容: ★サーバー・ネットワークの用意と構築保守(本番のアプリVM / DB / テスト環境)は すべて TeamWorks★。★DFA が作るのはビューアー(このリポジトリ)だけ★
  • なぜ: ユーザー回答(2026-09-16)「DFAはビューアーの作成のみです」 「サーバーを契約してお金を払うのはDFAですが、構築保守しているのは TeamWorks です」
  • ★費用負担(DFA)と実施主体(TeamWorks)は別です★。 ★「貴社にお渡しいただいたテスト環境」という従来の表現は事実と違う★ので使いません
  • ★ただしTeamWorks側の明示確認は未取得★(第9回 Q12-1 で照会中)
  • あわせて決めたこと: 本流への取り込み方(★マージコミット★)/ D-Secアラートの「通信断」の色(★アンバー維持★)/サイドバーの件数バッジ(★足さない★)

9. 規模の前提を置き直した(D-144)

  • 変更前: 性能の試算はすべて ★「施設200件」★ を前提にしていた
  • 変更後: ★「ロボット30台未満」★(ユーザー申告 2026-09-17)。★実態の約7倍を見積もっていました★
  • なぜ: ユーザーから「一応現状は30台未満なので、それも鑑みてください」という新情報があったため
  • ★結論は変わりません★ — ★索引が効かない構造の問題は、行数によらず起きる★ためです。 ★ただし「2年分はタイムアウトで落とせない」という見込みは、この規模では成立しません★
  • あわせて: ★TeamWorks への索引の依頼を取り下げました★(「対応中」と伺ったため。 ★実測・対象の一覧は参考情報として残し、先方が突き合わせられるようにしています★)
技術的な裏付け 【規模に依存する設定値(lib/config/index.ts 全705行を精査。9件)】 MAX_CSV_ROWS(:384) / MAX_DATE_RANGE_DAYS(:359) / DB_IN_CLAUSE_MAX_ITEMS(:463) / PAGINATION.maxPerPage(:326) / defaultPerPage(:325) / RESCUE_INITIAL_VISIBLE(:102) / CLEAN_AREA_INITIAL_VISIBLE(:704) / CONSECUTIVE_DAYS_DISPLAY.maxMax(:139) / CONSECUTIVE_LOOKBACK.calendarDaysPerScheduleDay(:169) ★値を変える必要があるのは MAX_CSV_ROWS だけ(それも据え置きと判断)★。 ★残りは値据え置きでよく、問題はコメントの陳腐化★ 【解消した懸念】 ★施設200件超で実行計画が劣化する懸念は発火しない★ — 1エリア=1ロボットなので ★閾値はロボット201台★(現在の7倍以上)。 ★`eq_range_index_dive_limit = 200` を MariaDB 10.11.19 で実測★(★構造由来。サーバー変数★) 【狭まらなかったもの】 ★制御盤の心拍テーブルの行数見積もりは、むしろ上限が上がった★ — 従来の前提「制御盤11台」は ★テストデータ由来で本番の根拠が無かった★。 60秒間隔: 1,158万 → ★3,053万行★ / 10秒間隔: 6,948万 → ★1億8,316万行★ 【★★この節の数字の多くは、後日ダミー由来と判明しました★★】(D-145。2026-09-17) ★無効になったもの★: 「1台1日あたり1.45回」という回数の根拠と、 そこから出した「2年で約193万行」「CSV 約3.7MB」「年間約1.6万行」。 ★538行も31日も12エリアも、すべて当方の scripts/seed-local-db.ts が決めた値★ ★有効なままのもの★: ★「ロボット30台未満」はユーザー申告なのでテストDBと無関係★ / ★索引が効かない構造の問題は行数によらず起きる★ / ★走査行数が台数×回数に対して二乗で効くこと(式の性質)★ ★CSV上限を据え置く根拠が「外挿が安全側だから」→ ★★「本番の回数が分からないので、いま変える理由が無い」★★ に置き換わりました★ 詳しくは docs/handoff/規模前提の見直し_20260917.md。

10. ★★本番DBに入っている行は、1行残らず当方のダミーだった(D-145 / D-148)★★

  • 何が分かったか: ★接続先(133.125.238.144 / clean_watch)に入っている17テーブル・1,787行は、 すべて当方の投入スクリプト由来★でした。★先方由来の行は1行も見つかっていません★
  • 決め手: ★customers / groups に「テスト顧客001」「テストグループ001」という文字列が残っていた★ — これは★当方の投入スクリプトの旧版にしか存在しない文字列★で、★先方が偶然使うことはあり得ません★
  • さらに(D-148): ★そこは「テストDB」ではなく本番DBそのもの★でした(ユーザー確認 2026-09-17)
  • なぜ重いのか:
    • ★「実測」と書いていた数字の10件が無効になりました★(サマリの面積 / 行数と面積の合計 / 清掃回数からの外挿 / 遅延0分 / 判定の分布 など)
    • ★本番DBの設定(暗号化なし・どこからでも接続可)がそのまま本番の状態だった★ので、 セキュリティの申し送りの深刻度が上がりました
    • ★当方のダミー1,787行が本番DBに入ったままです★(★本番前にTeamWorksが削除する予定★。 ★削除の連絡を受けたら記録し、必要な再実測を行うこと★)
    • ★本番DBには書き込みません★(ユーザー指示 2026-09-17)。 ★4カラムの更新機能は、書き込んで安全な環境ができるまで着手しません★
  • ★結論が変わった決定はありません★。★変わったのは「理由書き」2件だけ★です
  • 再発防止: ★CLAUDE.md に作法を1項目追加★ — ★「実測」と書くときは数字の出所(当方のシード由来 / 先方投入 / 構造)を必ず併記する。 出所を書けない数字は「実測」と呼ばない★
  • ★この事故は5日前に予告されていました★ — docs/decisions.md D-111 項7(2026-09-11)が ★「テストDBでは判定できない: 本番のデータ量 / サマリバッチの実挙動 / 本番の time_zone 設定」★と 明記していました。★表はあったのに守られませんでした★
技術的な裏付け 【仕分けの結果】(D-145 項2) ★構造由来(有効)★ 2件 … `eq_range_index_dive_limit=200` の MariaDB 実測 / 索引が1本も入っていないこと(information_schema.STATISTICS) ★★データ由来(無効)★★ 10件 … サマリ全24行が 1000 / 538行・Σ面積 / 1台1日1.45回→193万行 / 毎日18件 / 遅延0分×538 / clean_task_reports・link_schedules が0行 / problem 37.0% / 急落が消えていた(D-137 §0)/ 番人0件(D-141 §2)/ CONSECUTIVE_LOOKBACK の「実データで検証」(D-144 §5 #4) 【決め手となった照合】 Σtask_area 494,000 = 494行 × TASK_AREA_SQM(1000)(scripts/seed-local-db.ts:317。 未実施44行は task_area = 0) 538行 = fixtures 259 + ボリューム 279(EXTRA_VOLUME_DAYS = 31 × 9台) clean_schedules の id 9001〜9009 = EXTRA_VOLUME_SCHEDULE_ID_BASE(同 :445) 判定の分布は EXTRA_VOLUME_GRADE_CYCLE(同 :459。★当方が決めた巡回表★)の写し ★器(39テーブルのスキーマ・索引・型・照合順序・サーバー変数・権限)は先方由来★ ★投入は 2026-09-13 02:27:57〜02:28:19 JST の22秒一括。器の作成から約31時間後★ 【理由書きが変わった2件】 ★MAX_CSV_ROWS 据え置きの根拠★ … 「外挿が安全側だから」→「本番が不明なので変える理由が無い」 ★CONSECUTIVE_LOOKBACK★ … ★「検証済み」→「未検証」に後退★(値は据え置き)。 ★本番のスケジュールに週1回のものが1本でも入ると、連続日数が黙って切れる★。 ★実データが入ったら最初に確かめること★ 【本番DBの状態(2026-09-13 実測。★構造由来=サーバー変数と権限★)】 have_ssl = DISABLED → ★本番DBが平文★ require_secure_transport = OFF → ★TLS を強制していない★ 接続元 参照用アカウント@% → ★どこからでも接続できる★ 公開IPで到達可能 → ★閉域ではない★ ★「検証データしか入っていないので支障はない」という前提は、実データが入った瞬間に崩れる★ 【解決していたこと】 ★更新用の権限はすでに付与されていた★ — `SHOW GRANTS` の実測で `GRANT UPDATE ON clean_watch.robot_error_reports` / `…control_error_histories` を確認。★2026-09-03 から「未受領」としていたのは当方の確認漏れ★。 ★★ただし本番DBなので実行しない★★(D-148 項5) 詳しくは docs/handoff/テストDBのデータ出所_20260917.md。

11. 清掃詳細ログに「清掃結果」列を足した(D-146)

  • 変更前: ★「出発」列に ✗ が出ても、その理由が画面のどこにも出ていませんでした★ (値はAPIまで来ていたのに使われていなかった)
  • 変更後: ★「出発」の隣に「清掃結果」列を新設★(10列 → 11列)。★CSVにも同じ位置で足しました★
  • ★「出発失敗」と「異常(清掃未実施)」は別の表示にします★(ユーザー確定 2026-09-16)
    • ★履歴の行が無い★ → 「出発失敗」
    • ★未実施として記録された★ → 「異常(清掃未実施)」
    • ★この2つを同じ表示に潰すと、区別が画面から消えます★
  • ★コード表に無い値はそのまま数字で出ます★。★「その他」「不明」に潰しません★(D-077 と同じ方針)
  • あわせて直したもの: ダッシュボードの清掃ログ表に★「出発」列を1つだけ足した★(4列 → 5列。 ★「8列に戻す」ではありません★)/★押しても何も起きなかった「判定 ?」ボタンを機能させた★/ ★ダッシュボードでも行を開けるようにした★
技術的な裏付け 【DBの列】 clean_status_histories.clean_error 型: Int @default(0) @db.UnsignedTinyInt(NOT NULL)(prisma/schema.prisma:487) 意味: 0=正常 / 1=異常(清掃未実施) / 2=異常(セキュリティ有効) / 3=異常(D-S制御盤異常) / 4=異常(清掃ロボット異常) (lib/config/index.ts:263-274。原文は docs/handoff/analysis/table-definitions-02-clean-error.md:213) 【取得】 lib/db/index.ts:913 clean_error: history?.clean_error ?? null ★★`??` を `||` にすると 0(正常)が null に化ける★★ — ★tsc も lint も捕まえない★ 【型】 CleanLogItem.clean_error: number | null(lib/types/index.ts:242) ★null =「履歴の行が無い(清掃結果が記録されていない)」★ 【整形】 lib/format/clean-result.ts:34-39 formatCleanResult(cleanError: number | null): string if (cleanError === null) return CLEAN_RESULT_NOT_DEPARTED_LABEL; // "出発失敗" return cleanErrorLabel(cleanError); lib/config/index.ts:302-304 cleanErrorLabel(code: number): string → CLEAN_ERROR_LABELS[code] ?? String(code) lib/config/index.ts:286-292 CLEAN_ERROR_LABELS ★定義書の原文ママ。括弧は半角★ ★半角括弧を全角に直さないこと★ — ★実データと突き合わせたときに原文が分からなくなる★ 【画面】 components/features/detail-log/CleanLogTable.tsx:208 <td>{formatCleanResult(r.clean_error)}</td> ★セルの値は t() に通さない★(:203-207)— ★原文ママのラベルに対訳を当てると 何が原文だったか分からなくなる。コード表に無い値は String(code) なのでそもそも訳せない★ ★対訳を足したのは見出し「清掃結果」だけ★ colSpan は `compact ? 5 : 11`(:168 / :221。★2箇所ある。片方だけ直しても tsc も lint もテストも通り、見た目が詰まるだけなので目視でしか気づけない★) 【CSV】 lib/clean-logs/csv.ts — ★画面と同じ位置(「出発」と「完了率」の間)に入れた★。 ★列順を画面と変えると、突き合わせる人が毎回並べ替えることになる★。 ★新しい列も csvCell() を通している★(D-125。Excel が数式として実行するのを防ぐ) ★既存のCSVテストは列を「位置」で見ているので添字を直した。 ★危ないのは「落ちたから添字を合わせる」だけで新しい列のテストを書かないこと★なので、 清掃結果列そのもののテストを追加してある★ 【値の例】 ★構造由来★ null → "出発失敗" / 0 → "正常" / 1 → "異常(清掃未実施)" / 2 → "異常(セキュリティ有効)" / 5 → ★"5"★(★「その他」「不明」に潰さない★) ★シード由来★ fixtures は 0 か 1 しか作らない(lib/fixtures/clean-logs.ts:66) 【★別件との切り分け★】 ★これは「表示するだけ」で、判定ロジックは1行も書いていません★。 ★出発失敗を「判定する」実装(第9回 Q11-1 待ち)とは別件★ 【★見つかった思い込み★】 CleanLogTable.tsx の説明文に 「ダッシュボードではアドバイスを一切出さない規約」と書かれていたが、 ★★この「規約」はどの資料にも存在しなかった★★(grep 0件)。 ★実装者の自己申告が「規約」として残っていた★。 ★消すだけにして、別の「規約」を書かなかった★ — ★出所不明の根拠を足したのが原因だから★ 詳しくは docs/データの流れ_画面1-5_20260917.md §2.3(g) / §1.6。

12. TeamWorks 確認事項 第9回を送付した(D-147)

  • 送付日: ★2026-09-17★(送付者 daichi)
  • 内容: ★28ページ / 12章 / 質問25問 / お願い5件 / 参考情報1章★
  • なぜ記録するか: ★第1回・第4回で「送付した」記録を落とし、回答済みなのに「送付待ち」のまま 2回引き継いだ事故がある★ため。★以後、送付と回答受領の両方を必ず記録します★
  • この回答待ちで止まっていること(抜粋):
待っているもの 止まっていること
実際の出発時刻がどの列か(Q11-1) ★要レスキューの4条件目「出発失敗」が実装できない★
「清掃実施回数」の提供(Q6-2) ★確定期間の稼働率とエラー発生率が出せない★(分母が無い)
分類の実データ投入(7章) ★グループ・会社の絞り込みを実データで確認できない★
本番構成・TLS・接続元IP制限(12章) ★本番の構成理解が確認できない。平文の経路が残る★

13. 記録が残っていなかったUI差分を全件片付けた(D-149)

  • 何をしたか: モックと実装の差分26件を ★「記録済み4件 / 決着済み6件 / 仕様12件 / 不具合2件 / 判断4件」★ に振り分け
  • なぜ: ★「記録が無い差分」を放置すると、次の担当者が「作り忘れ」と誤認して直してしまう★ため
  • ★「記録なし」と書かれていた4件は、実際には記録がありました★ — ★「記録が無い」と書く前に必ず検索すること★
  • ★不具合2件は、ユーザー判断により保留★です(英語表示だけの問題で、★日本語表示は1文字も変わりません★)。 ★英語を配布すると決めた時点でまとめて直します★

14. 点検記録は「サービスとして出す」と確定した(D-150)

  • 決まったこと: ★点検記録はサービスとして展開する★(ユーザー回答 2026-09-17)。 ★これまで「サービスとして展開するかも保留中」としていた点が解消★
  • ★ただし、着手をふさいでいる未決が4件あります★
# 決まっていないこと 決まらないとどうなるか
B2 ★写真の保管場所の実体★(外部サービス / サーバー内のディスク) ★作る量が桁で違う★。サーバー内だと★社外から写真が見えない★
B3 ★その保管場所を誰が用意し、費用を払い、保守するか★ ★D-143 が名指ししたのは「アプリVM / DB / テスト環境」の3つだけで、ここは入っていない★
★B4★ ★★点検記録に「点検結果」を持たせるか★★ ★★契約にもモックにも項目別のOK/NGもコメント欄も1つも無い★★。★記録できるのは写真と申し送り1本だけで、点検の合否がどこにも残らない★
B5 ★誰がどこから入力するか★(現場スタッフ / DFA運用担当 / スマホ) スマホなら★スマホ幅レイアウト・IP制限の設計変更・トークンの配布★の3決定が連鎖する
  • ★B4 が設計上いちばん重い★ — ★「点検記録」という名前なのに点検の結果が残らない構造★であり、 ★後から足すと既存の記録が全部「結果なし」になります★
技術的な裏付け 【★実は既に決まっていた10件★】(ユーザーは「色々決まっていない」と認識していたが、API契約に書き込まれていた) ★写真は Before/After × 7項目 = 最大14枚 / 1枚10MB / jpeg・png★(d-watch-api.yaml:1347) ★点検7項目の英語 enum★(:2248) brush / dust_box / filter / squeegee / suction_hose / waste_tank / brush_back ★署名付き一時URLで配信★(:2256) 必須5項目 / 文字数上限(100・100・2000)/ 論理削除(:1354 ほか) ★型と fixtures は作成済み★(lib/types/index.ts / lib/fixtures/inspections.ts) メインDBに保存しない(D-004) 【★その他の未解決★】 ★`item_type`(YAML の英語 enum)と `item`(TS の日本語リテラル)が食い違っている★ — D-084 項11 が「着手前に解決が必要」と明記したまま ★未解決★ ★package.json に SQLite ドライバの依存が1つも無い★(実測)— ★D-004 の「SQLite」は方針止まりで技術選定は未着手★ ★独自ストアに持つのは facility_id / robot_id なのに、返すのは施設名・ブランド・シリアル★。 ★別ストアなので JOIN できない★ — 「読み取り時にメインDBを引く」か「登録時に名前をコピーする」かの選択が要る ★API契約自体の抜け3件★: GET /inspections に 422 の宣言が無い / POST に 413 が無い (10MB上限を書きながら超過時の応答が未定義)/ InspectionDetail に deleted_at・created_at が無い 【★この機能の一次資料は2つしかない★】 ★モックと d-watch-api.yaml だけ★。 D-031 が「要件定義書・画面要件整理シートに記載がなかった」と明記しており、 ★他画面のように UI仕様書へ戻って裏を取れない★。★これが不確定性の根本原因★ 詳しくは docs/handoff/点検記録_決まっていないこと_20260917.md。

4-3. この追補で「古くなった」初版の記述

初版の記述 どう変わったか
第2章 No.7「本番想定は1日あたり約360行」 ★施設200件を前提にした外挿でした★。実際はロボット30台未満(D-144)。★「切れる」という結論は不変★
第2章 No.8 / No.9 の実測値 ★当方のシード由来です★(D-145)。★本番の規模の根拠には使えません★
3-1 No.23「上限なし(Lv3 / 45日)」の背景 ★連続日数の遡り幅の前提(7日に1回は予定がある)は★★未検証★★に戻りました★(D-144 / D-145)
3-3 No.62「完了率ちょうど10.0は △(境界値は下のランクに落ちる規則で統一)」 ★規則そのものが D-139 で逆になりました★(境界は上のランクに入る)。★10.0 が △ である結論だけは不変★
6-1 の「本番/ステージングDBの接続先が未受領」 ★接続先は受領済みで、しかも本番DBそのものでした★(D-148)。★書き込みはしません★
6-1 の「DB更新用アカウントが未発行」 ★実は付与済みでした★(D-145 項4-2。★当方の確認漏れ★)。★ただし本番DBなので実行しません★
6-2 の「施設数・ロボット台数の見積もりが資料に1件も存在しない」 ★ロボット30台未満★(ユーザー申告 2026-09-17。D-144)

第5章 — モック(画面デザイン案)との差分

モックは「画面のレイアウト・文言・遷移の正」として扱う決まりです。ここから外れるときは必ず理由を残しています。 なお 画面の数は10対10で一致 しており(モックの10画面=実装の10画面)、差分は画面の中身に集中しています。

★2026-09-16 追記(D-141)★: その後 ★「清掃面積」画面を新設した★ので、 ★実装は11画面★になりました。★モックには対応する画面がありません★(「面積」「㎡」「累計」のヒットは0件)。 ★ユーザーの明示指示に基づく例外★で、★d-watch-mock.jsx は1文字も書き換えていません★。 ★同じく「要レスキュー店舗」カード(D-135)もモックに無いUIです★。

5-1. モックにあるが、実装していないもの

画面/機能 モックでの扱い 実装での扱い 理由
点検記録の画面本体 一覧・サマリ・写真・新規登録まで揃った完成画面 「準備中」の2行のみ 写真の保管場所が未決。 サービスとして展開するかも保留中 ★→【2026-09-17 D-150 で「サービスとして出す」と確定。★ただし写真の保管場所ほか4件が未決で、着手はまだできません★。第4章 4-2 の14を参照】★
点検記録のメニュー項目 サイドバーに表示 非表示(URL直打ちでのみ到達) 中身が未実装のため、デモまで一時的に隠している
システム管理の「表示設定」タブ タブ+ダッシュボード項目のON/OFFトグル4つ タブUI自体が無い 未着手。★docs/spec.md は「実装する」としており、仕様書と実装が食い違っている★
通知設定/顧客管理/グループ管理 タブ タブ+編集モーダル 作らない D-Security側の画面を流用するため(D-032)
ユーザー管理/権限管理/監査ログ タブ タブ+モーダル 作らない ユーザー管理が未実装なので権限管理・監査ログが成立しない。P2以降
グループ/会社の絞り込み(全画面) 固定の選択肢メニュー 全画面で削除 ★→【2026-09-16 D-138 で復活。ホールディングス→屋号→店舗の3階層で、5画面に実動作するものを入れました。点検記録は画面自体が未実装なので対象外】★ マスタが無く、選んでも何も起きないUIは不具合と区別がつかない(D-053)。★マスタが揃ったので解除★
D-Secログの「トークン期限」「残日数」列 表の列として存在 列ごと削除 仕様から除外済み(D-030)
アラート/D-Secアラートの対応状況の更新 クリックで状態が変わる 表示のみ、更新できない DBの更新用アカウントが未発行のため
施設マスタの「インポート」「新規追加」ボタン ボタンあり(モックでも押せない飾り) 置かない 登録・更新はD-Security側で行うため、D-Watchは参照のみ
レポートのCSVエクスポート 押せるボタン ボタンは置くが押せない状態 対応するAPIがAPI仕様書に存在せず、契約に無いものを勝手に足さない方針
レポートの日付範囲入力欄 日付の入力欄が2つ 作らない(週次/月次の切替+期間の前後移動に一本化) 二重の期間指定になるため
ダッシュボードの「失敗の内訳」 常時表示・4項目 表示されない 分類元のデータが存在しない(M3送り)
契約フラグによるメニューの出し分け (モックにも無いが仕様は要求) 未実装 メニューは施設をまたぐ共通UIで、「どの施設の契約か」が決まらないため

5-2. モックに無いが、実装で追加したもの

画面/機能 追加した理由
ログイン画面 モックには存在しなかった。URLを知っていれば誰でも開ける状態を解消するため
ページ送り(全一覧6画面) モックのままだと201件目以降を画面に出す手段が無い。本番想定は1日約360行。ユーザー承認を得たうえでのモック準拠ルールの例外
連続日数の表示上限と、その編集画面 30日を超えた連続は「日数の正確さ」より「訪問したか」が問題になるため。設定を画面から変える作法をこの1件で確立した
D-Secログ備考のその場編集 備考はDBに無く「運用担当が書き留めるメモ」と判明したため、ビューアー側の独自機能として実装
施設別パフォーマンスの4つ目の状態「判定なし」 予定が1件も無い施設まで赤色になっていたため
「編集不可」バッジ 触れるのに保存できないUIは事故のもとなので、編集できない項目を明示した
レポートの期間の前後移動ボタン 日付範囲入力を落とした代わりの操作手段
空データ・読み込み中・エラーの表示 モックは静的なダミーだったため、これらの状態を持っていなかった
清掃詳細ログの期間31日上限チェック 上限を超える指定をAPIに投げる前に画面側で弾く ★→【2026-09-15 D-136 で上限を2年(731日)に変更】★
セキュリティバナーの複数件表示 モックは1件固定だが、実データは複数施設が同時に未解消になりうる
★要レスキュー店舗カード★(2026-09-15 / D-135・D-137) ★「今すぐ手を打つべき店舗」を毎朝ひと目で見るため。ユーザーの明示指示による★
★清掃面積の画面★(2026-09-16 / D-141) ★清掃した面積を累計・期間別・粒度別に見るため。ユーザーの明示指示による★
★グループ・会社の絞り込み(3階層)★(2026-09-16 / D-138) ★モックにも選択メニューはあったが動かなかった。マスタが揃ったので実動作させた★
★清掃詳細ログの「清掃結果」列★(2026-09-17 / D-146) ★✗(出発失敗)の理由が画面のどこにも出ていなかった★
★CSVの行数上限と、押せない理由の表示★(2026-09-15 / D-136) ★2年分・全店舗だと落とせないため。★押せないボタンは理由が無いと不具合と区別がつかない★★

5-3. モックと扱いを変えたもの

画面/機能 モック 実装 理由
判定基準の文言 「◎ 90%以上」 「◎ 90%超」 ★→【2026-09-16 D-139 で「◎ 90%以上」に戻した】★ モックの文言が誤り。そのまま出すと誤った説明になる(★その後 D-139 でユーザー指示により境界を「含める」規則に変更し、結果的にモックと同じ文言になった★)
エラー種別の表示 日本語7分類 DBの値をそのまま(英語識別子) 推測でマッピングしない方針。結果として色分けが全部グレーになる
連続問題施設の補助文 「完了率 n%」 事実のみの説明文 連続日数の数え方(清掃予定日ベース)を数字だけでは説明できない
施設別パフォーマンスのセル色 80/90 を直接埋め込み 既存の閾値(90/75)を流用 同じ行の「ステータス」列と矛盾していた
D-Secログの接続状態 3状態 2状態 DBの値が0/1の2値だった
エラー種別内訳の見出し 「今週」固定 週次/月次の切替に連動 モック側のバグを修正した
ダッシュボードの「成功」の補助行 「/ 300施設」 「/ N件」(当日の清掃ログ行数) 1施設2台なら2行になるため。★なお「300施設」という数字は 出典・根拠が無い★
スケジュール進捗 常にバーを表示 予定0件なら「本日の清掃予定はありません」 0%のバーは誤読を招く
清掃詳細ログの検索 入力と同時に絞り込み 少し待ってからサーバーに問い合わせ 打鍵ごとに問い合わせが飛ぶのを防ぐため
清掃詳細ログのCSVボタン 押せない飾り 実際にダウンロードできる —
システム管理の構成 9タブ(かつ定義が2箇所にあり矛盾) 「運用設定」1画面 モックの二重定義はどちらが正か判断できないため、片方を採用
実在の住所・電話・店名 MEGAドンキ渋谷本店 等が残存 ダミー値に置換 実在情報をそのまま使わない方針。★モック側は資料として書き換えない★

第6章 — 一度決めて、あとで撤回した決定

「決めたが変えた」ものは隠さず記録しています。同じ議論を繰り返さないためです。

決定 一度決めた内容 撤回した理由 最終的にどうなったか
D-024 月をまたぐ週は「週の開始日(日曜)が属する月」に入れる TeamWorks の集計と帰属がズレる D-071(月境界で分割)に置き換え
D-067 項3 月をまたぐ週は「1日を含む週がその月の第1週」 同上。同じ週を2つの月に分けない方式だったため先方とズレる D-071 に置き換え
D-067 項2 / D-057 付随 エラー種別は既知の値だけ翻訳し、未知は「その他」に寄せる 何が返るか不明な状態での推測マッピングは、現場に誤った故障原因を伝える D-077(DBの値をそのまま表示)に置き換え
D-070 ロボット状態 -200 を「LostLocation」と表示する変換 同上(画面に出す前に変換する実装方針そのものを取り下げ)。★「-200 が LostLocation である」という事実認識は取り下げていない★ 変換しない
D-111 連続問題施設で出発失敗と完了率低下が混在したら「出発失敗」を優先表示 直近は清掃を実施していても「出発失敗」と書くことになり事実と食い違う(実データに2件該当) D-116(両方書く)に置き換え
D-034 エリア別完了率は動かない仮実装のまま先送り TeamWorks回答で「ロボット毎の清掃率なら取得可能」と判明 D-119 で本実装
D-020b サンプルデータ仕様書の「完了率10.0は ×」 確定仕様「× = 10%未満」と矛盾 △ に修正
D-068 「週番号の上限を5→6に直す」という指示は誤りとして却下 当時のルールでは最大5だったため却下は正しかったが、D-071 でルール自体が変わった 最終的に週番号は1〜6
★D-020b★(再掲) 「判定の境界値はすべて下のランクに落ちる」という規則 ★ユーザーが逆側に決め直した(2026-09-16)★。規則を統一するという思想は同じで、揃える向きが「上」になった ★D-139。90.0 は ◎、70.0 は ○。★10.0 が △ である点だけは変わらない★★
★D-109 決定3★ 「完了率ちょうど70.0%は問題あり」と直した ★D-139 で 70.0 が ○ になったため★ ★撤回(<= を < に戻した)。他の4件は有効★
★D-053★ マスタが無い間は、機能しない絞り込みUIを画面に出さない ★マスタ(/groups /customers)が揃った★ ★D-138 で5画面に復活。点検記録は画面自体が未実装なので対象外★
★D-133 の安全弁★ 複数顧客の行が混ざったら例外(500)にする ★分類が復活し「どの顧客かを決める根拠」ができた★ ★D-138 で customer_id の絞り込みに置き換え(例外の検知自体は防御的に残す)★
★D-135 項7-4 / 7-5★ 要レスキューは「連続日数の降順で5件まで」 ★急落だけで該当する施設が構造的に一覧から落ちていた★ ★D-137。APIは全件返し、画面が初期5件+「もっと表示」★
★D-136 §2 / D-144 の試算★ 「施設200件」を前提にした性能の試算 ★実態はロボット30台未満(ユーザー申告 2026-09-17)★/さらに ★回数の根拠が当方のダミー由来だった★ ★D-144 / D-145。★結論(索引が効かない構造の問題)は不変★。★回数の見積もりは根拠を失い「不明」に戻った★★
★D-141 §1 の推測★ 「TeamWorks の集計式が面積を合計として書き込んでいない可能性が高い」 ★見ていたのは当方が投入したダミーだった★ ★D-145。★当て推量は取り下げ。実装(サマリを読まない)は構造由来の理由で維持★★
★「テストDB」という呼称★ 接続先を検証用の別DBだと理解していた ★ユーザー確認(2026-09-17)で本番DBそのものと判明★ ★D-148。呼称を訂正し、★本番DBには書き込まない★方針を明記★

第7章 — 現在保留になっているもの

7-1. 最優先(これが解けないと先に進めない)

★2026-09-17 更新★: 1 と 2 は解消しました(ただし ★別の問題に置き換わっています★)。 3 と 4 は ★第9回確認事項として 2026-09-17 に送付済み★で、★回答待ち★です(D-147)。

# 保留していること なぜ止まっているか 誰待ちか 放置するとどうなるか
1 本番/ステージングDBの接続先が未受領 ★→【解消。ただし★接続先は本番DBそのものでした★(D-148)。★いまはそこに当方のダミー1,787行が入ったままです★(D-145)。★本番前にTeamWorksが削除する予定★】★ — — ★★削除されたら、当方の実測記録(行数・分布)は全部無効になります★★。★削除の連絡を受けたら記録し、必要な再実測を行うこと★
2 DB更新用アカウントが未発行 ★→【解消。★実は 2026-09-03 時点で付与済みで、「未受領」としていたのは当方の確認漏れでした★(D-145 項4-2)】★ ★★ただし本番DBなので書き込みを実行しません★★(ユーザー指示 2026-09-17。D-148 項5) DFA(環境の用意) ★4カラムの更新機能の実装・検証に着手できない★。★書き込んで安全な環境をどう用意するかが未定★
3 DBが公開状態で、通信が暗号化されていない ★→【★「テスト用」ではなく本番DBの状態でした★(D-148 項2)。深刻度が上がっています】★ DB本体とネットワークはDFAの担当範囲外 TeamWorks ★本番に実データが入った後では「平文の経路を通ったパスワードのまま運用開始」になり、作り直すしかなくなる★。期限は実データ投入前。★第9回12章で依頼済み(2026-09-17 送付)★
4 集計表(確定した週・月の数字)の計算式が未回答 先方が「量が多いので後で回答」と留保 TeamWorks レポートの「先週・先月」の数字がどう作られたか検算できない。進行中の期間と定義が食い違っていても気づけない
★5★ ★「清掃実施回数」がサマリに無い★(第9回 Q6-2) 2026-09-17 送付。未回答 TeamWorks ★★確定期間の稼働率とエラー発生率が出せません★★(★分母=実施回数が無い★。task_count は予定回数)
★6★ ★実際の清掃開始時刻がどの列か★(第9回 Q11-1) 2026-09-17 送付。未回答 TeamWorks ★要レスキューの4条件目「出発失敗」が実装できない★。★予定時刻の列だった場合、出発失敗が永久に0件になり、例外もテスト失敗も出ない★
★7★ ★分類(ホールディングス/屋号)の実データが無い★(第9回 7章) 2026-09-17 送付。未回答 TeamWorks ★復活させた絞り込みを実データで確認できない★。★group_id = 0 がFK制約で投入できない問題も未解決★

7-2. ユーザー(DFA)の判断待ち

# 決めてほしいこと 現状どうなっているか 決めないとどうなるか
5 「定刻出発」の定義 判定材料がDFA側に無く、進行中の期間では 稼働率と出発成功率が必ず同じ値になる レポートに常に同じ数字が2つ並び、片方が指標として意味を持たない
6 管理画面(システム管理)のスコープ ユーザー管理・権限管理・監査ログはP2以降だが、繰り上げ案が検討メモに残っている(未決10件) 最終フェーズの管理画面に着手できない。とくに「閾値を画面から変えられるようにするか」が決まらないと判定ロジック全体の作り方が決まらない
7 異常の種類(セキュリティ有効/制御盤異常/ロボット異常)を画面のどこに出すか 値はAPIまで運んでいるが、表示は止めてある 異常の種類が画面に出ないまま。値は取れているのに使われていない
8 「セキュリティ解除されたまま」のアラートを出すか 社内調整中。当面は出していない D-Secアラート画面の作りと、ダッシュボードの赤バナーの要否が決まらない
9 同じエリアが同じ日に2回清掃されたときの合算方法 二重に数えられる可能性がある 該当例が実在すると、その施設の完了率が実態とずれる
10 施設数・ロボット台数・制御盤台数の見積もり ★→【2026-09-17 一部解消。★ロボットは30台未満★(ユーザー申告)。★制御盤の台数は依然として不明★で、心拍テーブルの行数見積もりは6倍ぶれます】★ 従来の試算はすべて「施設200件」を前提にしており、★実態の約7倍★でした(D-144) ★1台1日あたりの清掃回数は依然として不明★なので、CSVの行数上限を測り直せません
★11★ ★点検記録の未決4件★(写真の保管場所 / 誰が用意し費用を払うか / ★点検結果を持たせるか★ / 誰がどこから入力するか) ★サービスとして出すことは確定(D-150)。それ以外は未定★ ★★「点検記録」という名前なのに点検の合否がどこにも残らない構造のまま★★。★後から足すと既存の記録が全部「結果なし」になる★
★12★ ★書き込みを検証できる環境をどう用意するか★ ★接続先が本番DBなので、4カラムの更新を試せない★(D-148 項6) ★備考・対応状況を画面から書く機能を、一度も実地で試せない★
★13★ ★英語表示を配布するか★ ★未定のため、EN 関連の不具合(対訳キーの衝突・辞書の抜け37キー)を一律で保留★(D-143 / D-149) ★英語にすると「未対応」が Unsupported(非対応)と出続けます★。★日本語表示は1文字も変わりません★

7-3. 暫定値のまま動いているもの

項目 現在の値 位置づけ
清掃失敗と判定するまでの猶予 30分 運用しながら調整する前提の暫定値
施設別パフォーマンスの色分け 良好=稼働率90%以上/注意=75%以上 暫定。基準そのものは未確定
エラー率の要注意色 8%超 モックの見た目から起こした値
D-Sec備考の最大文字数 200字 一行メモ想定の暫定値
アドバイスB2(ハンドルアップ検知)の発火条件 特定の英文字列との一致 実データにこの値が来るか不明。来なければ静かに発火しなくなる
アドバイスB3(オフライン検知)の発火条件 空=発火しない 判定元が不明なため
ロボット状態の色分け表 日本語6語 実データからは来ない値。実データ接続後はこの列が全部グレーになる想定
メーカー番号の対応 1=PUDU/2=Keenon DFA側で仮に割り当てた番号
事前チェックの回数 2回(60分前・30分前)で固定 3回に増やすと3件目が黙って捨てられる
★完了率の「急落」と判定する下げ幅★(D-135) ★前回比 20%(=前回の8割を切ったら急落)★ ★ユーザー指定の暫定値。★差(ポイント)ではなく比★(lib/config/index.ts:48-51)★
★未実施が何日続いたら要レスキューか★(D-135) ★3日★ ★L3 の「3日」とは別の設定値にしてある★(意味が違うため。流用すると片方を変えたときにもう片方まで動く)
★CSVで出せる行数の上限★(D-136) ★20,000行★ ★★根拠を失った暫定値★★。★「本番の清掃回数が分からないので、いま変える理由が無い」という状態★(D-145)。★実データと索引が入ったら測り直す★
★清掃詳細ログの期間上限★(D-136) ★2年(731日)★ ★日付を指定しないときは上限なし。初期表示は今日1日のまま★
★面積の単位★(D-141) ★㎡★ ★DFAの判断であってTeamWorksの確認ではない★。★1箇所に置いてあるので回答が違えば1行で直せる★
★連続日数を遡る幅★ ★スケジュール存在日1日あたりカレンダー7日(既定で217日)★ ★★「7日に1回は清掃予定がある」という前提は未検証★★(D-145 で「検証済み」から後退)。★本番のスケジュールに週1回のものが1本でも入ると、連続日数が黙って切れる★

確定仕様であって暫定ではないもの: 判定基準(◎90%以上/○70%以上/△10%以上70%未満/×10%未満)と エスカレーション(L2=2日、L3=3日)。ただし 画面から変更する手段はまだありません(管理画面の判断待ち)。 画面から編集できるのは「連続日数の表示上限」1つだけです。

★注記(2026-09-16 追記)★: 判定基準の境界は D-139(2026-09-16 ユーザー指示)で「含める」規則に 変更されました。本文の判定基準は新しい規則に書き直してあります(作成時点の記述は 「◎90%超/○70%超/△10%以上70%以下/×10%未満」でした)。★変わったのは 90.0(○→◎)と 70.0(△→○)の2つだけで、10.0 は △ のままです★。本資料の作成日は 2026-09-14 のため、 上の「3-2 表 No.38」「5-3 判定基準の文言」「3-3 表 No.64」にも作成当時の旧規則が残っています (それぞれ印を付けました)。正は CLAUDE.md §4.1 と docs/decisions.md D-139 です。 ★詳しい経緯と、連動して直した箇所は第4章 4-2 の「4. 判定の境界を「含める」に変えた」を見てください★。

7-4. 後のフェーズに送ると決めた機能

送り先 機能
Phase B エラーコード別のアドバイス/1回の清掃の中の区域別完了率(売り場・通路・レジ前の内訳)
Phase C 異常の自動検出・予兆検知
P2以降 顧客アカウントの開放/ユーザー管理・権限管理・監査ログ/Keenon対応/PUDU MT1・Keenon C30・C40(土台は用意済み、設定1つで有効化できる)
P1内の後半 ダッシュボードの「失敗の内訳」/システム管理の「表示設定」タブ/レポートのCSVエクスポート

7-5. アプリの外(サーバー設定・インフラ)に申し送ったもの

# 内容 状態
1 テスト用DBの接続元を固定IPに絞る/通信を暗号化する/パスワードを再発行する 実データ投入前が期限。未対応
2 セキュリティ関連のヘッダはアプリではなくサーバー(nginx)側で設定する 手順書に記載済み。まだ「約束」であって実装ではない
3 IP制限が正しく効く構成であること 手順は確定。実機での確認は未実施
4 アプリのポートにサーバー経由以外で到達できないこと 未確認
5 本番サーバーの構成(実行環境のバージョン、再起動時に設定が消えないか) 未確認
6 「アプリ=クラウドVM/DB=データベース専用サービス」という構成理解で合っているか 明示の確認が未取得

7-6. 既知の残課題(動いてはいるが直したいもの)

# 内容
1 レポートの「全 n 施設を表示」が200件で切れる(施設が200を超えると件数表示と実際の行数が食い違う)
2 CSV出力と画面の件数が食い違う(CSVは全件、画面は1ページ分)
3 検索に意図的に残した穴 — 半角カナやひらがなでは引けない/スペース区切りのAND検索がない/メガドンキホーテ で MEGAドン・キホーテ は引けない。★これは仕様です。不具合として報告が上がったらD-129を参照してください★
4 集計表の値と自前の検算に最大0.5ポイントの差(ダミーデータの丸め誤差と判断し、上書きはしない)
5 API仕様書に「サーバーエラー」の記載が1本も無い(挙動の記載漏れ)
6 D-133 の性能対策は アプリ側だけでは半分しか直っていない(DBへの索引追加が必要) ★→【2026-09-17 D-144。★索引の依頼は取り下げました★(TeamWorks が対応中と伺ったため)。★実測・対象の一覧は参考情報として残し、先方が突き合わせられるようにしています★。★2026-09-17 時点でDBには索引が1本も入っていません★】★
7 性能の実測はすべて手元のMySQL環境で行っており、本番のMariaDBでの挙動は未確認 ★→【一部解消。eq_range_index_dive_limit = 200 は MariaDB 10.11.19 で実測済み(★サーバー変数なので構造由来★)。★それ以外は未確認のまま★】★
8 削除済みデータの扱いが3つの処理で不揃い
★9★ ★★「実測」と書いていた数字の10件が、当方のダミーを見たものでした★★(D-145)。★訂正注記は入れましたが、★本番の数字はまだ1つも持っていません★★
★10★ ★当方のダミー1,787行が本番DBに入ったままです★(★本番前にTeamWorksが削除する予定★。D-148)
★11★ ★英語表示の抜けが37キーあり、「未対応」の対訳が2つの意味で衝突しています★(★英語を配布すると決めた時点でまとめて直す★。D-143 / D-149)
★12★ ★規模の前提(1台1日あたりの清掃回数)が不明なので、CSVの行数上限を測り直せません★(D-144 / D-145)
★13★ ★画面(components/)のテストは1件も走りません★ — テストの対象が lib/ と app/ に限られているため。★画面の変更はブラウザでの目視確認が要ります★(D-146 項10)

第8章 — 用語ミニ辞典

用語 意味
モック 実際には動かない画面デザイン案。レイアウト・文言・遷移の「正」として扱う
TeamWorks DBの設計・構築・データ収集・集計バッチを担当する外部エンジニア会社。DFAはそのDBを 読むだけ
テーブル定義書 / DDL DBの設計図。定義書は紙の資料、DDLは実際にDBを作るための命令文。食い違ったときはDDLが正
API仕様書(d-watch-api.yaml) 画面とサーバーのあいだのやりとりの取り決め。実装より先にこちらを直す決まり
サマリ(集計表) 週・月・年ごとに数字をあらかじめ計算して保存したもの。TeamWorksが作る
当期 進行中の週・月・年。集計表がまだ無いので、DFA側がその場で計算する
fixtures(サンプルデータ) DB接続前の開発に使う手作りのダミーデータ
閾値(しきいち) 判定の境目の数値(90%、70%、10%、猶予30分など)。プログラム内に直接書かず設定として外に出す決まり
事前チェック 清掃開始の60分前・30分前にロボットの状態を確認する仕組み
D-Security / D-S制御盤 セキュリティ解除→清掃→復帰を自動化するオプション機能と、その中継用の機器
清掃エリア 施設内の清掃対象区域(旧称「区画」)。P1では「1エリア=1ロボット」を前提とする
静かに壊れる エラーも出ずテストも通るのに、画面の数字や表示が間違っている状態。本プロジェクトが最も警戒している壊れ方
★縦線★ ★「DBのどの列から来て / どの関数を通って / どんな型で / どの画面に / どんな値で出るか」を1本につないだ説明★。docs/データの流れ_* の書式
★シード由来 / 構造由来 / 記録済み実測★ ★数字の出所の印★。シード由来=当方が作ったサンプルデータ / 構造由来=型・設計図・設定値から読み取れる値 / 記録済み実測=日付と出所つきで記録済みの実測値。★印の無い数字を「実測」と呼ばない★(CLAUDE.md §6-6)
★要レスキュー店舗★ ★今すぐ手を打つべき施設★。未実施3日連続 / 連続問題レベル3 / 完了率が前回比20%以上の急落 のどれかに当たったもの(D-135)
★分類★ ★ホールディングス → 屋号(ブランド)→ 店舗★ の3階層の絞り込み(D-138)
★番人★ ★静かに壊れる代わりに警告をログへ出す仕掛け★。例: 1つの清掃に履歴が2行以上あると面積を二重に数えるので、その件数を数えて警告する(D-141)

第9章 — もっと詳しく知りたいときの道案内(対応表)

この資料は「何をなぜ変えたか」を書いたものです。 ★「その数字は実際にどこから来ているのか」を追いたくなったら★、下の表の行き先を開いてください。

★2つの文書はどちらも「DBの列 → 取得 → 変換(関数名と引数・戻り値)→ 型 → 画面 → 値の例」★という 同じ書式で、★合計3,000行・64本以上の縦線★が入っています。

文書 対象画面
★docs/データの流れ_画面1-5_20260917.md★ ダッシュボード / 清掃詳細ログ / アラート / レポート / 清掃面積
★docs/データの流れ_画面6-10_20260917.md★ D-Secログ / D-Secアラート / 施設ロボットマスタ / スケジュール / システム管理

9-1. 第2章(影響の大きかった変更10件)からの行き先

本資料の項目 行き先
1. 開発言語と作る主体 (★対応する章はありません★。体制の話のため)
2. 画面の時刻が9時間ズレていた 画面1-5 §2.3(d)「日付/予定」/ 画面6-10 §0.5「DATETIME は JST の壁時計値」/ 画面1-5 付録A #4
3. 清掃が実施されたかの判定 画面1-5 §1.2「数値カード」/ §2.3(f)「出発」
4. エラー発生率の分子 画面1-5 §4.4「エラー種別の内訳」
5. エラー種別を翻訳しない 画面1-5 §2.3(e)「1h前ST / 30分前ST」/ §3.4(b)「ロボット状態」
6. 月をまたぐ週の数え方 画面1-5 §4.1「期間の指定(月跨ぎ週の地雷)」
7. 一覧のページ送り 画面1-5 §3.3「カード」/ 画面6-10 §6.5「summary と meta.total の違い」
8. 絞り込みをDB側に降ろした 画面1-5 §2.2「ルートの処理順」/ 付録A #3
9. 実データで壊れる3箇所 画面6-10 §6.3「最終通信」/ 画面1-5 §4.2「対象顧客の決定」
10. 契約フラグの読み方 画面6-10 §8.2「契約フラグ」/ 付録A A-5(★第2の契約列★)

9-2. 第3章(変更点 全一覧)からの行き先

本資料の行 行き先
3-1 No.12 事前チェックの値の意味 画面1-5 §3.4(a)「種別バッジ」/ §2.3(e)
3-1 No.13 アラート種別の持ち方 画面1-5 §3.1「2種類の行を混ぜている」/ §3.4(a)
3-1 No.14 連続問題日数 画面1-5 §2.3(j)「連続問題日数とエスカレーション」
3-1 No.15 進行中の期間の分母 画面1-5 §2.3(f) / §4.5「施設別パフォーマンス表」
3-1 No.17 率の精度 画面1-5 §2.3(h)「完了率」/ §2.3(i)「判定」
3-1 No.18 エリア別完了率 画面1-5 §2.5「エリアモーダル」
3-1 No.22 店舗検索の記号 画面1-5 §2.3(c)「シリアル」/ 画面6-10 §6.6「検索の照合規則」/ §8.7
3-1 No.23 連続日数の表示上限 画面1-5 §2.3(j) / 画面6-10 §10.5「連続日数の表示上限」
3-1 No.25 D-Secログの接続状態 画面6-10 §6.2「接続状態」
3-2 No.30 D-Secログの備考 画面6-10 §6.4「備考(メインDBに列が無い)」
3-2 No.31〜34 アドバイス 画面1-5 §2.4「アドバイス」/ 画面6-10 §10.6「アドバイス文言」
3-2 No.35 連続問題施設の説明文 画面1-5 §4.3「連続問題施設」
3-2 No.36 施設別パフォーマンスの状態 画面1-5 §4.5
3-2 No.37 セルの色分け基準 画面1-5 §4.5
3-2 No.45 出発した/しなかった 画面1-5 §3.4(c)「検知日時」/ 付録B(★LAUNCH_GRACE_MINUTES はどの判定からも読まない★)
3-2 No.46 ダッシュボードの予定件数 画面1-5 §1.3「スケジュール進捗カード」
3-2 No.51 深夜をまたぐ終了時刻 画面6-10 §9.3「時間(HHMM 文字列。時が24〜29を取る)」
3-2 No.52 対応機種の扱い 画面6-10 §8.4「ロボット」
3-2 No.53 ロボットが正常なときの表示 画面1-5 §2.3(e) / §3.4(b)(★"Standby." は末尾のピリオド込みが原文★)
3-2 No.54 バッテリー残量 画面1-5 §3.4(e)「バッテリー残量」
3-2 No.56 アドバイス文言の入力上限 画面6-10 §10.6 / §10.7「3つのJSONストアの関係」
3-2 No.57 CSV出力の安全対策 画面1-5 §2.6「CSV エクスポート」
3-3 No.62・63 判定と表示の桁 画面1-5 §2.3(h) / §2.3(i)(★期待値表★)/ 画面6-10 §10.3
3-3 No.65 「グループなし」 画面6-10 §8.6「所属」
3-3 No.66 一覧の並び順 画面1-5 §3.4(f)「id と並び順」/ 画面6-10 §6.2・§7.5・§8.7・§9.6
3-3 No.70・71 色数・空データの文言 画面1-5 §4.4(★未知のエラー種別は中立色にフォールバック★)/ §2.5
3-3 No.72 メーカー名の対応 画面1-5 §2.3(b)「機種」/ 画面6-10 §8.4

9-3. 第4章(追補 D-135〜D-150)からの行き先

本資料の項目 行き先
1. 要レスキュー店舗(D-135 / D-137) 画面1-5 §1.5「要レスキュー店舗カード」
2. 詳細ログ2年分(D-136) 画面1-5 §2.1「クエリパラメータの実物」/ §2.6「CSV エクスポート」
3. 分類の復活(D-138) 画面1-5 §4.2「対象顧客の決定」/ 画面6-10 §0.5「共通の登場人物」
4. 判定の境界(D-139) 画面1-5 §2.3(i)「判定」/ 画面6-10 §10.3「完了率の判定閾値」
5. 当期KPIの絞り込み(D-140) 画面1-5 §4.2「確定期間のKPI」「当期のKPI」「422 のときの画面」
6. 清掃面積の新画面(D-141) 画面1-5 ★第5章まるごと★
7. 詰まりの解消(D-142) (★対応する章はありません★。手続きの話のため)
8. 責任分界の訂正(D-143) 画面6-10 §7.2「通信断はアンバー」/ 付録A A-6・A-7(★同時に決めたUI判断★)
9. 規模の前提(D-144) 画面1-5 §2.6 / 付録B「設定値の一覧」
10. 本番DBのデータは当方由来(D-145 / D-148) 画面1-5 §0.3 / 画面6-10 §0.4・付録B「値の出所の内訳」
11. 清掃結果列(D-146) 画面1-5 §2.3(g)「清掃結果」/ §1.6「コンパクト版清掃詳細ログ」
12. 第9回の送付(D-147) (★対応する章はありません★。手続きの話のため)
13. UI差分の振り分け(D-149) 画面6-10 付録A(★追っていて見つけた矛盾・危ない箇所15件★)
14. 点検記録(D-150) (★対応する章はありません★。★画面が未実装なので追える縦線がありません★)

9-4. 「危ない箇所」だけ見たいとき

見たいもの 行き先
★事故りやすい行の一覧(file:line 付き・13件)★ 画面1-5 ★付録A★
★追っていて見つけた矛盾・危ない箇所(15件)★ 画面6-10 ★付録A★
★設定値(閾値・上限・対応表)の一覧★ 画面1-5 ★付録B★
★追いきれなかったもの(=まだ分かっていないこと)★ 画面1-5 ★付録C★ / 画面6-10 ★付録C★

付記 — この資料の根拠

  • すべての項目は docs/decisions.md(★D-001〜D-150★)に決定番号付きで記録されています
  • 本資料の主要な数値・主張は、作成時に決定記録および実コードと1件ずつ突き合わせて確認済みです
  • ★技術的な裏付け(折りたたみ部分)の file:line と関数シグネチャは、 docs/データの流れ_画面1-5_20260917.md / _画面6-10_20260917.md から引いています★。 ★本追補の作成にあたって本番DBには接続していません★(D-148)
  • ★docs/code-map.md は一部の記述が古くなっています(「エリア別完了率は未実装」は誤り。D-119で実装済み)★
  • ★要件定義書 P1 v2.2 は「何を作るか」の正ですが、型・値・項目名の正ではありません(実DDLが正)。 また「PHP / Laravel」「トークン有効期限」の記述は現行方針では古いものです★
  • ★本資料に出てくる具体的な値には出所の印が付いています★(CLAUDE.md §6-6)。 ★★印が「シード由来」のものは当方のサンプルデータであり、本番の規模や実勢を示しません★★