Newer
Older
client_dwatch / docs / handoff / 指示書AO_ダッシュボードと詳細ログの列を直す.md

指示書AO — ダッシュボードと詳細ログの列を直す

作成: 2026-09-17 / ブランチ: develop(HEAD 1726817)/ 前提: D-143 §3(UI差分 第3章の判断)


★Cursor への指示文(ここから下をそのまま貼ってください)★

D-Watch の実装をお願いします。ブランチは develop です。

■ 最初に読むもの(この順で、全部)
  1. CLAUDE.md(全文。特に §4.3 端数処理 / §4.8 エラー種別は変換しない /
     §5 モックが正 / §7 禁止事項 / §9 実装規約)
  2. docs/handoff/指示書AO_ダッシュボードと詳細ログの列を直す.md(全文)
  3. components/features/detail-log/CleanLogTable.tsx(★今回の主役。1本で2画面を兼ねている★)
  4. lib/format/raw-label.ts(★段階1で作る関数の手本。置き場所も作法もこれに倣う★)
  5. lib/clean-logs/csv.ts と lib/clean-logs/__tests__/csv.test.ts
     (★テストが列を**位置**で見ている。段階3の前に必ず読む★)
  6. components/features/detail-log/DetailLogScreen.tsx の toggleRow() / openAreas() /
     AreaModal の使い方(★段階5でダッシュボードへ移植する★)

■ やること
  指示書AO の 段階1 から 段階7 までを、★番号順に★実行してください。
  ★段階1(清掃結果ラベルの純粋関数)は「期待値表をテストに落としてから実装する」
    順序を崩さないでください★(CLAUDE.md §6-2 / §9.4)。
  ★段階の順序には依存関係があります★ — 列を足してから行展開を有効にします
    (colSpan の値が変わるため。逆順だと展開行が詰まって壊れます)。

■ 段階ごとに必ずやること
  1. npx tsc --noEmit    → 0 であること
  2. npm run lint        → 0 であること
  3. npm test            → 全パス
  4. コミットする(★段階ごとに1コミット★。まとめない)

■ コミットの決まり
  ・git add -A は使わないでください。必ずファイルパスを明示してください
  ・コミットメッセージは日本語。「なぜそうしたか」を1〜2行入れてください
  ・push はしないでください

■ 絶対に触らないファイル
  README.md / docs/code-map.md /
  docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** /
  d-watch-mock.jsx / lib/judge/** / lib/db/** / prisma/**

■ 絶対にやらないこと
  ・★clean_error を7分類に変換すること★(D-077 / CLAUDE.md §4.8)
  ・★コード値(0/1/2/3/4)をコードに直書きすること★ — lib/config の CLEAN_ERROR を使う
  ・★未知のコードを「その他」「不明」に潰すこと★(D-089 項3 / D-104 項2)
  ・表示用に丸めた値で判定すること(CLAUDE.md §4.3)
  ・components/ に判定・整形ロジックを書くこと(★テストが走りません★。落とし穴1)
  ・生SQL($queryRaw / $executeRaw / *Unsafe / Prisma.raw)— ESLint が落とします
  ・メインDBのスキーマ変更・マイグレーション作成
  ・新しい CSS に font-family を書くこと
  ・仕様が不明なところを推測で実装すること

■ 動作確認
  ★dev server はポート3000です★。ログインが要るので目視が難しければ
  「できなかった」と正直に書いてください。★推測で「動いているはず」と書かないこと★。

■ 終わったら
  ・段階ごとのコミットハッシュ
  ・tsc / lint / test の結果(★テスト本数も★。着手前は 96ファイル / 2,141テスト)
  ・指示書どおりにできなかったところと、その理由
  ・判断に迷って自分で決めたところ(あれば全部)

0. この指示書の位置づけ

docs/handoff/UI差分第3章_調査_20260916.md(937行)で洗い出した8件のうち、 ★ユーザーが 2026-09-16 に「直す」と決めた4件★を実装します。

第3章 # 件名 ユーザー判断 本指示書
#2 死んだ「判定 ?」ボタン ★直す★ ★段階6★
#3 ダッシュボードの清掃ログ表が4列 ★★「出発」列だけ足す★★ ★段階4★
#6 コンパクト表の行展開が無効 ★直す★ ★段階5★
— 清掃結果(clean_error)が画面にもCSVにも出ていない ★出す★ ★段階1〜3★
#1 「未対応」の対訳キー衝突 ★★直さない★★ §2
#4 サイドバーの未対応件数バッジ ★★足さない★★ §2
#5 D-Secアラート「通信断」の色 ★★アンバー維持★★ §2
#7 D-Secアラートの期間フィルタ ★★今は入れない★★ §2
#8 バッテリー残量行 ★意図した仕様★ §2

★第3章の調査は「日付・1h前ST・30分前ST・出発の4列を戻す」を提案しましたが、 ユーザーは★★「出発」1列だけ★★と決めました。★提案のほうを実装しないこと★。★

0-1. ★D-107 の申し送りが片付きます★

lib/types/index.ts:238 に、こう書いてあります:

★★画面・CSV には出していない★★ — 2/3/4 の出し方が未決(D-107 の申し送り 項1)。

★本指示書でこの申し送りが片付きます★(段階2・段階3)。 ★★型のコメントと lib/config/index.ts:248-249 のコメントを、実装に合わせて直すこと★★ — 直さないと、次の担当者が「まだ出していない」と読んで二重に実装します。


1. ★先に知っておくこと(実測)★

★どれも 2026-09-17 に自分で開いて確かめた事実です。★ ★★行番号は着手時にズレている可能性があります。必ず自分で開いて確認してください。★★

1-1. ★表のコンポーネントは1本だけ★

components/features/detail-log/CleanLogTable.tsx   ← ★これ1本★
  ├─ components/features/dashboard/DashboardScreen.tsx:152   compact={true}
  └─ components/features/detail-log/DetailLogScreen.tsx:495   compact 無し

★★新規コンポーネントを作らないこと★★ — CleanLogTable.tsx:26-27 に 「表を分けると修正が2箇所に散るため、新規コンポーネントは作らずこのテーブルの prop で分岐する」 と明記されています。★この判断はそのまま維持します★。

1-2. ★現在の列★

# 列 compact(ダッシュボード) 非compact(詳細ログ)
1 施設名 ★出す★ 出す
2 機種 落とす 出す
3 シリアル 落とす 出す
4 日付 落とす 出す
5 予定 ★出す★ 出す
6 1h前ST 落とす 出す
7 30分前ST 落とす 出す
8 出発 ★★落とす → 段階4で出す★★ 出す
9 完了率 ★出す★ 出す
10 判定 ★出す★ 出す
— ★清掃結果★ ★★足さない★★ ★★段階2で足す★★
着手前:  compact = 4列 / 非compact = 10列
段階2後: compact = 4列 / 非compact = 11列
段階4後: compact = 5列 / 非compact = 11列   ← ★最終形★

1-3. ★colSpan が2箇所ある★

CleanLogTable.tsx:156   空状態の行        colSpan={compact ? 4 : 10}
CleanLogTable.tsx:205   アドバイス展開行  colSpan={compact ? 4 : 10}

★★列を足したら両方直すこと★★。★片方だけ直しても tsc も lint もテストも通ります★ (画面のテストが存在しないため)。★見た目が詰まるだけなので、目視でしか気づけません★。

最終的に★両方とも colSpan={compact ? 5 : 11}★になります。

1-4. ★行展開の無効化は :162-163★

// CleanLogTable.tsx:162-163
const hasAdvice = !compact && (r.grade === "△" || r.grade === "×");
const isExpanded = !compact && expandedId === r.id;

根拠として CleanLogTable.tsx:23-27 の docstring がこう書いています:

ダッシュボードではアドバイスを一切出さない規約のため、compact 時は行クリック・ アドバイス展開も無効化する。

★★この「規約」はどの資料にも存在しません★★。

$ grep -n "アドバイスを一切\|ダッシュボードではアドバイス" \
    docs/decisions.md docs/spec.md docs/handoff.md
(0件)

git blame では 47f2df4(2026-08-30「F-01 ダッシュボード画面を実装」)の初回実装で 書かれたもので、★実装者の自己申告★です。★段階5でこの2文を消してください★。

★★消すだけにして、別の「規約」を書かないこと★★ — 出所不明の根拠を足したのが この事故の原因です。★消す代わりに「ユーザー判断 2026-09-16(D-143 §3 #6)」と書きます★。

1-5. ★clean_error は API契約にも型にも既にある★

d-watch-api.yaml:1621-1632   clean_error(integer / nullable / 0〜4 の説明つき)
lib/types/index.ts:240       clean_error: number | null;

★★つまり YAML のスキーマ変更は要りません★★(CLAUDE.md §6-1 が要求する 「API契約を先に直す」は、今回は該当しません)。★着手時に自分で確認すること★。

★ただし d-watch-api.yaml:1632 の説明文★:

2/3/4 は未実施とは違う扱いで departed=true(画面への出し方は未決)

★この「未決」だけが古くなります★。★段階2で「詳細ログの『清掃結果』列とCSVに生値のラベルで出す」に 書き換えること★。★スキーマは1文字も変えません★(説明文だけ)。

1-6. ★ラベル関数は既にある★

lib/config/index.ts:262   CLEAN_ERROR            0〜4 のコード(★直書き禁止★)
lib/config/index.ts:285   CLEAN_ERROR_LABELS     コード → ラベル(★定義書の原文ママ★)
lib/config/index.ts:301   cleanErrorLabel(code)  無いコードは String(code) を返す
コード ラベル
0 正常
1 異常(清掃未実施)
2 異常(セキュリティ有効)
3 異常(D-S制御盤異常)
4 異常(清掃ロボット異常)
★5〜255★ ★★String(code) = 生値がそのまま出る。それが正しい挙動★★(D-077)

★★括弧は半角のまま。全角に直さないこと★★(lib/config/index.ts:277-278。 定義書の原文が半角)。★言い換え・要約もしないこと★。

★★cleanErrorLabel() は null を受け取れません★★(引数が number)。 ★だから段階1で null を扱う関数を1つ足します★。

1-7. ★CSVの列定義★

lib/clean-logs/csv.ts:62-73    CLEAN_LOG_CSV_HEADER(10要素)
lib/clean-logs/csv.ts:85-102   buildCleanLogCsv()。★行の組み立ては :90-99 の10セル★

★全列が例外なく csvCell() を通っています★(D-125)。 ★csv.ts:78-83 のコメントに「列の追加は『csvCell() で包む』だけで済む」とあります★。 ★★その約束を破らないこと★★。

1-8. ★テストの現況(着手前の実測。2026-09-17)★

$ npm test
 Test Files  96 passed (96)
      Tests  2141 passed (2141)

★この数字を基準にしてください★。


2. ★★やらないこと(ユーザーが「やらない」と決めたもの)★★

★★以下は「やり忘れ」ではありません。ユーザーが 2026-09-16 に判断した結果です(D-143 §3)。 親切心で直さないでください。★★

第3章 # 件名 ★理由★
★#1★ ★「未対応」の対訳キー衝突★(lib/i18n.ts:160 の 未対応: "Unsupported"。★英語で「非対応」の意味になる★) ★★英語を配布するか未定★★のためユーザーが「そのまま」と判断。★★★英語配布を決めた時点で必ず直す必要があります★★★ — ★D-Secアラートの「未対応」(人が対応していない)が Unsupported(機器が非対応)と出続けます★。★要対応 にも同型の地雷があります★(今は辞書に無く和文のままですが、素直に対訳を足した瞬間に同じ事故になります)
#4 サイドバーの未対応件数バッジ ★PATCH(対応状況更新)が未実装・DB更新用アカウントも未受領★。ユーザー判断で保留
#5 D-Secアラート「通信断」の色を赤に戻す ★★アンバー維持で確定★★
#7 D-Secアラートに期間フィルタを足す ★今は入れない★。入れるなら d-watch-api.yaml の変更から
#8 バッテリー残量行を消す ★意図した仕様★(D-105 項5)。★★空欄のまま残すのが正★★。★行ごと消さないこと★

★★#1 について、指示書に書いておく注記★★:

英語表示を配布すると決めた時点で、lib/i18n.ts の 未対応 キーは必ず直す必要があります。 直し方は「キーを日本語原文のまま使う方式」自体を変えるか(1階層辞書をやめる)、 呼び出し側で別の日本語(例: 未対応(対応状況))に分けるかの2択で、 ★どちらも lib/i18n.ts の作法を変える話なので、単独の指示書が要ります★。 ★本指示書では触りません。★


3. ★触ってはいけないもの★

対象 理由
README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** ★別作業★
★d-watch-mock.jsx★ ★★モックは原型として保存する★★(CLAUDE.md §7)。★1文字も書き換えない★
★lib/judge/**★ ★判定ロジックは変えません★
★lib/db/** / prisma/**★ ★今回はDB側を1行も変えません★(clean_error は既にAPIまで来ています)
★d-watch-api.yaml のスキーマ★ ★変更不要★(§1-5)。★★説明文1箇所だけ★★直します
★テストDB(133.125.238.144)への書き込み★ ★★読み取り専用★★

★★禁止★★: git add -A / 生SQL / スキーマ変更 / ★コード値の直書き★ / ★components/ への判定・整形ロジック★ / ★新しいCSSへの font-family★ / 推測での実装


4. 段階1 — ★清掃結果ラベルの整形を純粋関数にする★(★テストが先★)

4-1. 置き場所

★新規 lib/format/clean-result.ts★ と lib/format/__tests__/clean-result.test.ts。

★手本は lib/format/raw-label.ts★(27行。★D-077 を維持したまま「空である」事実だけを 差し替える純粋関数★)。★docstring の書き方もこれに倣ってください★。

★★components/ に書かないこと★★ — vitest.config.ts:14 の include は ["lib/**/*.test.ts", "app/**/*.test.ts"] で、★components/** は走りません★(落とし穴1)。

★lib/judge/ にも置かないこと★ — あちらは◎○△×等の★評価★の置き場所で、 こちらは★表示整形★です(lib/format/raw-label.ts:15-16 に同じ切り分けが書いてあります)。

4-2. 何をする関数か

export function formatCleanResult(cleanError: number | null): string
null   → 「出発失敗」
0〜4   → CLEAN_ERROR_LABELS のラベル(★定義書の原文ママ★)
5以上  → String(code)(★生値。D-077★)

★★null を「出発失敗」と表示する根拠★★(★コメントに書くこと★):

  • d-watch-api.yaml:1629-1630 — 「null は『履歴の行が無い(=清掃結果が記録されていない)』 ことを表し、★出発失敗として扱う★(D-107 設計3-C)」
  • lib/types/index.ts:219-221 — clean_error === 1 または null のとき departed === false
  • ★ユーザー確定(2026-09-16)★: ★「null は『出発失敗』と表示する」★

★★「(値なし)」にしないこと★★ — formatRawLabel() の EMPTY_VALUE_LABEL は 「DBのカラムが空」という事実だけを述べる文言で、★ここは「履歴の行が無い=出発していない」という 意味が確定している 場面★です。★ユーザーが文言を指定しています★。

★★文言は関数と同じファイルの定数に出すこと★★(EMPTY_VALUE_LABEL と同じ形):

/** `clean_error` が null(履歴の行が無い)のときの表示。★ユーザー確定 2026-09-16★ */
export const CLEAN_RESULT_NOT_DEPARTED_LABEL = "出発失敗";

★画面とCSVの2箇所から使うので、文字列を2回書かないためです★。

4-3. ★期待値表(★先にテストへ★)★

# 入力 期待 なぜ入れるか
★1-1★ null ★"出発失敗"★ ★ユーザー確定の本体★
1-2 CLEAN_ERROR.normal(0) "正常"
★1-3★ CLEAN_ERROR.notExecuted(1) ★"異常(清掃未実施)"★ ★★null と別物であること★★。★同じ「出発失敗」に潰さない★
1-4 CLEAN_ERROR.securityEnabled(2) "異常(セキュリティ有効)" ★D-107 申し送りの本題★
1-5 CLEAN_ERROR.controlBoardError(3) "異常(D-S制御盤異常)" 同上
1-6 CLEAN_ERROR.robotError(4) "異常(清掃ロボット異常)" 同上
★1-7★ 5 ★"5"★ ★★未知のコードは生値★★(D-077)
★1-8★ 255 ★"255"★ ★unsigned tinyint の上限★
★1-9★ 255 ★★「その他」「不明」を含まない★★ ★D-089 項3 / D-104 項2★。★clean-error.test.ts:60-65 に同じ形のテストがあるので倣う★
★1-10★ 0 ★"出発失敗" ではない★ ★null と 0 の取り違え(?? と `\ \ ` の事故)を止める★
1-11 全コード ★CLEAN_ERROR_LABELS と1文字も違わない★ ★ラベルを画面側で言い換えていないことの担保★

★★1-10 を必ず入れること★★ — clean_error ?? ... を clean_error || ... と書くと ★0(正常)が「出発失敗」に化けます★。★例外もtscエラーも出ません★。

★コード値を直書きせず CLEAN_ERROR を使うこと★(テスト側も。 lib/db/__tests__/clean-log-mapping.test.ts:285 に同じ趣旨のテストがあります)。

4-4. 完了条件

  • lib/format/clean-result.ts と lib/format/__tests__/clean-result.test.ts がある
  • ★テストを先にコミットしていなくてよいが、実装より先に書く★
  • npx tsc --noEmit → ★0★
  • npm run lint → ★0★
  • npm test → 全パス。★★97ファイル(+1)/ 2,150前後(+8〜12)★★
  • ★CLEAN_ERROR を import していること★(0〜4 の直書きが無いこと)

コミットメッセージ案

feat(format): 清掃結果ラベルの整形を純粋関数にする(null は「出発失敗」)

clean_error を画面とCSVの2箇所で使うため、null の扱いを1箇所に閉じた。
cleanErrorLabel() は null を受け取れず、?? と || の取り違えで 0(正常)が
「出発失敗」に化ける事故が静かに起きるため、期待値表で固定した(指示書AO 段階1)。

5. 段階2 — ★詳細ログの表に「清掃結果」列を足す★

5-1. 触るファイル

ファイル やること
components/features/detail-log/CleanLogTable.tsx ★!compact 付きの <th> / <td> を1組追加★・★colSpan を2箇所直す★
lib/i18n.ts ★清掃結果 の対訳を追加★
lib/types/index.ts ★clean_error の docstring から「画面・CSVには出していない」を消す★
lib/config/index.ts ★CLEAN_ERROR の docstring から「画面への出し方は未決」を消す★
d-watch-api.yaml ★clean_error の description 1行だけ★(★スキーマは変えない★)

5-2. 列の位置

★★「出発」列(:102-106 の <th> / :188-192 の <td>)の直後、「完了率」の直前★★。

★理由★: ★出発(✓/✗)も 清掃結果 も出所は同じ clean_status_histories.clean_error です★ (d-watch-api.yaml:1617-1620)。★隣に並べると、✗ の理由がその場で読めます★。 ★離すと、2列の関係が読み取れません★。

… 30分前ST │ 出発 │ ★清掃結果★ │ 完了率 │ 判定

5-3. 実装

{!compact && (
  <th className={styles.th} style={{ textAlign: "left" }}>
    {t("清掃結果")}
  </th>
)}
{!compact && <td className={styles.td}>{formatCleanResult(r.clean_error)}</td>}

★★セルの値を t() に通さないこと★★ — ★理由をコメントに書くこと★:

  • CLEAN_ERROR_LABELS は★定義書の原文★です(lib/config/index.ts:272-278)。 ★対訳を当てると「何が原文だったか」が分からなくなります★
  • ★未知コードは String(code)★ で、そもそも訳せません
  • ★既存の status_1h / status_30m(Pill)も t() を通していません★。★同じ扱いに揃えます★
  • ★見出し(清掃結果)だけ t() を通します★

5-4. ★colSpan を直す★

:156   colSpan={compact ? 4 : 10}  →  colSpan={compact ? 4 : ★11★}
:205   colSpan={compact ? 4 : 10}  →  colSpan={compact ? 4 : ★11★}

★★2箇所とも★★。★段階4で compact 側も 5 に変わります★。

5-5. ★i18n★

lib/i18n.ts の EN に足します。★施設名 〜 判定 が並んでいる塊(:23-31)の 出発 と 完了率 の間★に置いてください(★画面の列順と辞書の並びを揃えるため★)。

清掃結果: "Clean Result",

★★衝突の確認(★必ずやること★)★★:

grep -n "清掃結果" lib/i18n.ts

★着手前の実測では 0件★です。★1件でも出たら止まって報告してください★ — lib/i18n.ts は★キーが日本語原文の1階層辞書★なので、 ★同じ日本語を別の意味で使うと必ず衝突します★(未対応 → Unsupported の事故が実在。§2 #1)。

★清掃結果 は docs/spec.md や app/api/dashboard/route.ts のコメントでは 「昨夜の清掃結果サマリ」という別の意味で使われています★が、 ★★t() のキーとしては未使用なので衝突しません★★。 ★ただし将来「清掃結果サマリー」を t() に通す人が出たら、そちらは別キーになります★ (DashboardScreen.tsx:81 の 昨夜の清掃結果サマリー — {today} は今も t() を通していません)。

5-6. ★古くなるコメントを直す★

★これを忘れると、次の担当者が「まだ出していない」と読んで二重に実装します。★

場所 現在 直す
lib/types/index.ts:238 ★「★★画面・CSV には出していない★★ — 2/3/4 の出し方が未決(D-107 の申し送り 項1)。」★ ★「詳細ログの『清掃結果』列とCSVに formatCleanResult() で出す(指示書AO / D-1xx)」★
lib/config/index.ts:248-249 ★「★★どの画面にどう出すかは未決★★(D-107 の申し送り)。現状は API まで生値を運ぶだけで、画面・CSV には出していない。」★ 同上
d-watch-api.yaml:1632 「2/3/4 は未実施とは違う扱いで departed=true(画面への出し方は未決)」 「2/3/4 は未実施とは違う扱いで departed=true。詳細ログの『清掃結果』列とCSVに生値のラベルで出す(指示書AO)」

★★D-1xx は段階7で採番した実際の番号に置き換えること★★(先に書かない)。

5-7. 完了条件

  • ★詳細ログの表が11列★(数えること)
  • ★ダッシュボードは4列のまま★(★この段階では変わらない★)
  • ★colSpan が2箇所とも compact ? 4 : 11★
  • grep -n "清掃結果" lib/i18n.ts → ★1件(追加した行だけ)★
  • ★lib/types/index.ts / lib/config/index.ts / d-watch-api.yaml のコメントが実装と一致★
  • ★d-watch-api.yaml のスキーマ(型・nullable・enum)が1文字も変わっていないこと★
  • npx tsc --noEmit → 0 / npm run lint → 0 / npm test → 全パス(★本数は段階1のまま★)

★★テストは増えません★★ — ★画面のテストは存在しません★(vitest.config.ts:14)。 ★「増えないこと」自体が正しい★ので、増やそうとして components/ にテストを置かないでください。

コミットメッセージ案

feat(detail-log): 清掃詳細ログに「清掃結果」列を足す(10列→11列)

clean_error は API まで来ていたが画面に出しておらず、✗(出発失敗)の理由が
読めなかった。出発列の隣に置いて、同じ clean_error 由来であることを見せる。
D-107 の申し送り 項1(2/3/4 の出し方が未決)はこれで片付く(指示書AO 段階2)。

6. 段階3 — ★CSVに「清掃結果」列を足す★

6-1. 触るファイル

ファイル やること
lib/clean-logs/csv.ts ★ヘッダ(:62-73)と行(:90-99)の両方★
lib/clean-logs/__tests__/csv.test.ts ★★位置で見ているテストの添字を直す★★ + ★新しい列のテストを足す★

6-2. 列の位置

★★画面と同じ位置★★ — 出発 と 完了率 の間。

★理由★: csv.ts:61 のコメントが★「ヘッダ行の見出し(モックの列順に合わせる)」★です。 ★画面とCSVで列順が違うと、突き合わせる人が毎回並べ替えることになります★。

export const CLEAN_LOG_CSV_HEADER = [
  "施設名", "機種", "シリアル", "日付", "予定時刻",
  "1h前ST", "30分前ST", "出発", ★"清掃結果"★, "完了率", "判定",
] as const;
csvCell(r.departed ? "出発" : "未出発"),
★csvCell(formatCleanResult(r.clean_error)),★
csvCell(displayRate(r.completion_rate)),

★★必ず csvCell() を通すこと★★(D-125。csv.ts:78-83)。 ★「ラベルは固定文言だから安全」という判断をしないこと★ — ★未知コードは String(code) で、 - 始まりにはなりませんが、★列ごとに安全性を判断させないのがこの設計の要点★です。

6-3. ★★既存テストが位置で列を見ています(最大の罠)★★

lib/clean-logs/__tests__/csv.test.ts:112   cells(row)[8]   完了率
                                    :126   cells(base)[8]  完了率
                                    :131   cells(base)[7]  出発
                                    :132   cells(base)[9]  判定
                                    :134   cells(...)[7]   出発
                                    :152   c[0] c[1] c[2] c[5] c[6] ★c[8]★

★清掃結果 を index 8 に挿入すると★:

旧 新 列
[7] [7] 出発(★変わらない★)
— ★[8]★ ★清掃結果(新規)★
[8] ★[9]★ ★完了率★
[9] ★[10]★ ★判定★

★★[8] → [9] / [9] → [10] を、★5箇所★直すこと★★。

★★直し忘れると「テストが落ちて気づく」ので、そこは安全です★★。 ★危険なのは逆で、「落ちたから添字を合わせる」だけで済ませて、 ★新しい列そのもののテストを書かないこと★です。

★:164 の expect(cells(base)).toHaveLength(CLEAN_LOG_CSV_HEADER.length) は ★定数を見ているので自動で追随します★(直す必要なし)。

6-4. ★足すテスト★

# 入力 期待
★3-1★ clean_error: 0 ★cells(base)[8] === "正常"★
★3-2★ clean_error: null ★"出発失敗"★
★3-3★ clean_error: 2 ★"異常(セキュリティ有効)"★(★括弧は半角★)
★3-4★ clean_error: 5 ★"5"★(★生値★)
★3-5★ ★ヘッダの8番目★ ★CLEAN_LOG_CSV_HEADER[8] === "清掃結果"★

★★3-3 を必ず入れること★★ — ★異常(セキュリティ有効) は ( ) を含みますが、 csvCell() の引用判定は /[",\n\r]/ なので★引用されません★。 ★「引用されないのが正しい」ことを固定します★(後から誰かが「括弧も引用しよう」として 既存CSVの互換を壊すのを止めるため)。

★:139-157 の「★どの列に危険な値が来ても素通ししない★」テストに ★清掃結果の列は追加できません★(値が clean_error から機械的に決まり、 ★危険な文字列を作れない★ため)。★その旨を1行コメントで書いてください★ — 書かないと「この列だけ漏れている」と読まれます。

6-5. 完了条件

  • ★CLEAN_LOG_CSV_HEADER が11要素★
  • ★行の組み立ても11セル★(★csvCell() を11回通る★)
  • ★添字を直した5箇所がすべて通る★
  • ★新テスト5本★
  • npx tsc --noEmit → 0 / npm run lint → 0 / npm test → 全パス。
    ★★97ファイル / 2,155前後(段階1から +5)★★
  • ★CSVの列順が画面の列順と一致していること★(★目で並べて確かめる★)

コミットメッセージ案

feat(csv): 清掃詳細ログのCSVに「清掃結果」列を足す(画面と同じ位置)

画面(段階2)とCSVで列順が違うと突き合わせのたびに並べ替えになるため、
出発と完了率の間に入れた。既存テストが列を位置で見ているので添字を直し、
新しい列が csvCell() を通っていることを固定した(指示書AO 段階3 / D-125)。

7. 段階4 — ★ダッシュボードに「出発」列を足す★(4列 → 5列)

7-1. 触るファイル

components/features/detail-log/CleanLogTable.tsx ★のみ★。

7-2. やること

:102-106 の <th> と :188-192 の <td> から★!compact && を外す★だけです。

// before
{!compact && (
  <th className={styles.th} style={{ textAlign: "center" }}>{t("出発")}</th>
)}
// after
<th className={styles.th} style={{ textAlign: "center" }}>{t("出発")}</th>

★★ソース上の位置を動かさないこと★★ — 出発 の <th> は 30分前ST と 完了率 の間にあり、 ★!compact を外すだけで compact 時の列順が「施設名 / 予定 / 出発 / 完了率 / 判定」になります★。 ★並べ替える必要はありません★。

7-3. ★colSpan を直す★(★2箇所★)

:156   colSpan={compact ? 4 : 11}  →  colSpan={compact ? ★5★ : 11}
:205   colSpan={compact ? 4 : 11}  →  colSpan={compact ? ★5★ : 11}

★★これが最終形です★★。

7-4. ★docstring を直す★

CleanLogTable.tsx:24 の「★4列表示にする(施設名/予定時刻/完了率/判定のみ)★」が ★事実と違うようになります★。

★5列表示にする(施設名 / 予定 / 出発 / 完了率 / 判定)★

★★「機種・シリアル・日付・1h前ST・30分前ST を落とす」と落とすほうを書くこと★★ — ★出す列を並べると、列を足すたびに書き換えが要ります★。

★★「ユーザー判断(2026-09-16。D-143 §3 #3)で、足すのは『出発』1列だけと決めた」と 書き添えること★★ — ★書かないと、次に docs/spec.md:32(「機種・シリアル列を省略」=8列)と 突き合わせた人が「日付・1h前ST・30分前ST も戻し忘れている」と判断して足します★。

7-5. ★i18n・CSS は変更不要★(★実測★)

  • 出発 は★対訳済み★(lib/i18n.ts:29 の 出発: "Dep.")
  • CleanLogTable.module.css:1-5 が .table { min-width: 1000px }、 DashboardScreen.module.css:75-77 が .tableWrap { overflow-x: auto }。 ★★横スクロールで収まるので CSS 変更は不要★★
  • ★★新しいCSSを書かないこと★★(書くなら font-family を入れない。CLAUDE.md §0-3)

7-6. 完了条件

  • ★ダッシュボードの表が5列★(施設名 / 予定 / ★出発★ / 完了率 / 判定)
  • ★詳細ログは11列のまま★
  • ★colSpan が2箇所とも compact ? 5 : 11★
  • ★docstring が5列と書いてあり、ユーザー判断の出所が書いてある★
  • ★CSS を1行も足していないこと★
  • npx tsc --noEmit → 0 / npm run lint → 0 / npm test → 全パス(★本数は段階3のまま★)

コミットメッセージ案

feat(dashboard): ダッシュボードの清掃ログ表に「出発」列を足す(4列→5列)

△×の行しか出ない表で、出発したのかどうかが読めなかった。
日付・1h前ST・30分前ST は足さない(ユーザー判断 2026-09-16 / D-143 §3 #3)。
colSpan は空状態と展開行の2箇所とも追随させた(指示書AO 段階4)。

8. 段階5 — ★コンパクト表の行展開を有効にする★

8-1. ★なぜ段階4より後なのか★

★colSpan が compact ? 5 : 11 になっている必要があります★。 ★段階4より先にやると、展開したアドバイス行が4列ぶんしか広がらず、右端が詰まります★。 ★tsc も lint もテストも通るので、目視でしか気づけません★。

8-2. 触るファイル

ファイル やること
components/features/detail-log/CleanLogTable.tsx ★:162-163 の !compact && を外す★ / ★:23-27 の存在しない「規約」のコメントを消す★
components/features/dashboard/DashboardScreen.tsx ★state とアドバイス取得を足す★ / ★★エリアモーダルを足す★★(§8-4)

8-3. CleanLogTable.tsx

// before
const hasAdvice = !compact && (r.grade === "△" || r.grade === "×");
const isExpanded = !compact && expandedId === r.id;
// after
const hasAdvice = r.grade === "△" || r.grade === "×";
const isExpanded = expandedId === r.id;

★★モックと同じ形になります★★(d-watch-mock.jsx:311 は var hasAdvice = r.g === "△" || r.g === "×"; で compact を見ていません)。

★docstring(:23-27)から★★この2文を消すこと★★:

ダッシュボードではアドバイスを一切出さない規約のため、compact 時は行クリック・ アドバイス展開も無効化する。

★★代わりに書くこと★★:

★compact でも行展開する★(モック `d-watch-mock.jsx:311` は compact を見ていない)。
★かつて「ダッシュボードではアドバイスを一切出さない規約」と書かれていたが、
  ★★その「規約」はどの資料にも存在しなかった★★(grep 0件。実装者の自己申告)。
  ユーザー判断で展開を有効にした(2026-09-16 / D-143 §3 #6 / 指示書AO 段階5)。

★★「規約が無かった」という事実を残すこと★★ — ★消すだけだと、また誰かが同じ 思い込みを書き足します★。

8-4. ★★DashboardScreen.tsx(ここがいちばん量が多い)★★

DetailLogScreen.tsx から★そのまま移植できます★。

移す先 元
expandedId / adviceById / adviceLoadingId の state DetailLogScreen.tsx:84-88
toggleRow() DetailLogScreen.tsx:214-246
openAreas() DetailLogScreen.tsx:257-263
areaTarget / areaFilter / areaData / areaLoading / areaError DetailLogScreen.tsx:90-95
エリア取得の useEffect DetailLogScreen.tsx:265-297
<AreaModal ... /> DetailLogScreen.tsx:513-524
AreaTarget 型 DetailLogScreen.tsx:31-40

そして CleanLogTable への受け渡しを差し替えます(DashboardScreen.tsx:152-162):

expandedId={expandedId}          // was null
adviceById={adviceById}          // was {}
adviceLoadingId={adviceLoadingId}// was null
onToggleRow={toggleRow}          // was () => {}
onOpenAreas={openAreas}          // was () => {}

★★onOpenAreas を () => {} のまま残さないこと(★見落としやすい★)★★

★アドバイスが1件以上あると、展開部に★「エリア別詳細を見る →」ボタン★が出ます★ (CleanLogTable.tsx:235-244)。★★onOpenAreas が空関数のままだと、 このボタンは押しても何も起きません★★ — ★今回直している「死んだ『判定 ?』ボタン」と まったく同じ不具合を、その隣に作ることになります★。

★モックでは compact でもこのボタンからモーダルが開きます★ (d-watch-mock.jsx:358 は LogTable の中にあり、compact で分岐していません)。 ★CLAUDE.md §5 でモックが★遷移の正★です★。 ★したがって AreaModal をダッシュボードにも置きます★。

★★/detail-log へ遷移させて代用しないこと★★ — ★モックはモーダルを開きます★。 ★遷移にすると、開いていた行の情報が失われます★。

8-5. ★AreaModal の移植で気をつけること★

  • ★AreaModal は components/features/detail-log/ にあります★。 ★ダッシュボードから import して構いません★(CleanLogTable と同じ扱い。★複製しないこと★)
  • ★取得は openAreas() の中でやらないこと★ — DetailLogScreen.tsx:248-255 のコメントのとおり、 ★filter の切り替えでも取り直す必要がある★ので useEffect に寄せてあります。 ★2本に分けないこと★
  • ★cancelled フラグを落とさないこと★(DetailLogScreen.tsx:269)— ★古い応答が後から 届いて新しい表示を上書きします★
  • ★toggleRow() の catch 節をそのまま持ってくること★(:221-243)— ★取得に失敗しても行は開き、一覧が持っている値で最小限を埋めます★。 ★★row.consecutive_problem_days_capped ?? false の ?? を || にしないこと★★

8-6. ★i18n は今回変えない★

展開部の見出し(CleanLogTable.tsx:37-51 の「🔴 レベル3 — DFA介入を検討」等)は ★t() を通していません★。★EN では和文のまま出ます★。

★★これは詳細ログでも同じ既存の抜けなので、本段階では直しません★★ (§2 #1 と同じ理由 — ★英語を配布するか未定★)。★ダッシュボードでも和文が出ることを 報告に1行書いてください★。

8-7. 完了条件

  • ★ダッシュボードの △× 行をクリックすると展開する★
  • ★展開行が5列ぶんの幅いっぱいに広がる★(★詰まっていないこと★)
  • ★▼ / ▲ が判定列に出る★
  • ★「エリア別詳細を見る →」を押すとモーダルが開く★(★★空関数のままになっていないこと★★)
  • ★モーダルの絞り込み(問題エリアのみ / 全件)を切り替えると取り直す★
  • ★CleanLogTable.tsx に「規約」という語が残っていないこと★
    (`grep -n "規約" components/features/detail-log/CleanLogTable.tsx` → ★0件★)
  • ★詳細ログ側の挙動が変わっていないこと★(★同じ表を共有しているため★)
  • npx tsc --noEmit → 0 / npm run lint → 0 / npm test → 全パス(★本数は段階3のまま★)

コミットメッセージ案

feat(dashboard): コンパクト表の行展開アドバイスを有効にする

根拠とされていた「ダッシュボードではアドバイスを一切出さない規約」は
どの資料にも存在せず(grep 0件)、初回実装の自己申告だった。
モックは compact でも展開する。エリア別詳細のモーダルも合わせて移植した
(押しても何も起きないボタンを新たに作らないため)。
ユーザー判断 2026-09-16 / D-143 §3 #6 / 指示書AO 段階5。

9. 段階6 — ★「判定 ?」ボタンを機能させる★

9-1. 現状

ボタンは★compact でも表示されています★(CleanLogTable.tsx:110-120)。 凡例本体(:123-152)は showGradeInfo でしか開きません。 ダッシュボードは★showGradeInfo={false} / onToggleGradeInfo={() => {}}★を渡しています (DashboardScreen.tsx:158-159)。

★★押しても何も起きないのは仕様上の制約ではなく、状態を持たせていないだけです★★。

9-2. やること(★useState 1つ★)

components/features/dashboard/DashboardScreen.tsx ★のみ★:

const [showGradeInfo, setShowGradeInfo] = useState(false);
...
showGradeInfo={showGradeInfo}
onToggleGradeInfo={() => setShowGradeInfo((v) => !v)}

★CleanLogTable.tsx は変更不要★です。

9-3. ★D-053 との整合★(★聞かれる前に書いておきます★)

D-053 は★「固定の選択肢だけがあって何も起きないUIは、不具合と区別できない」★ため 絞り込み <select> を★消し★ました。

★本件は前提が違います★ — ★D-053 が「消す」を選んだのは、機能させるためのデータ (マスタ)が存在しなかったから★です。★こちらはデータも凡例本体も既にあり、 必要なのは useState 1つ★です。

同じ趣旨が lib/i18n.ts:320-321 にも明文化されています:

★★disabled だけにして理由を出さないことはしない★★ — 押せないボタンは、理由が書いていないと不具合と区別がつかない(D-053 と同じ理由)

★★したがって D-053 と整合するのは「機能させる」です★★。 ★「ボタンを消す」を選ばないこと★ — ★モックには ? があり、押すと開きます★ (d-watch-mock.jsx:301)。★消すと CLAUDE.md §5 に反します★。

9-4. ★★注記: EN表示で和文の塊が1つ増えます★★

凡例の文言は★t() を通していません★:

CleanLogTable.tsx:126   判定基準(タスク完了率):
CleanLogTable.tsx:142   (閾値はシステム設定で変更可能)

★開けるようにすると、EN表示で和文の塊が新たに1つ増えます★。

★★これは既存の抜けであって本段階が作る問題ではありません★★ (★同じ凡例は詳細ログでも開きます★)。★§2 #1 と同じ理由で今回は直しません★。 ★報告に1行書いてください★。

★★もし直すなら★★: 辞書に足すのは★2キーだけ★です。 ★ただし ◎ {GRADE_THRESHOLDS.excellent}%以上 のような★数値混じりの行★は、 lib/i18n.ts の★方式A(プレースホルダ + replace)★に乗せる必要があります ("{n}分前" などが前例)。★1文字キー(%以上 等)にしないこと★。 ★これは本指示書の範囲外です★。

9-5. 完了条件

  • ★ダッシュボードで ? を押すと凡例が開く★
  • ★もう一度押す / ✕ を押すと閉じる★
  • ★詳細ログ側の挙動が変わっていないこと★
  • ★CleanLogTable.tsx を変更していないこと★
  • npx tsc --noEmit → 0 / npm run lint → 0 / npm test → 全パス(★本数は段階3のまま★)

コミットメッセージ案

fix(dashboard): 「判定 ?」ボタンを機能させる(useState 1つ)

凡例本体もデータも既にあり、状態を渡していないだけだった。
D-053(何も起きないUIは消す)は「機能させるデータが無い」場合の話で、
本件は前提が違う。モックには ? があり押すと開く(CLAUDE.md §5)。
ユーザー判断 2026-09-16 / D-143 §3 #2 / 指示書AO 段階6。

10. 段階7 — ★記録★

10-1. ★★採番の前に必ず実測すること★★

grep -n "^### D-" docs/decisions.md | tail -3

★★^### D-0 では3桁台を拾えません★★(★二重採番の事故歴があります★)。

★着手前の実測では最終番号は D-143(docs/decisions.md:11255)★です。 ★★着手時点で増えている可能性があるので、必ず自分で確認してください★★。

★D-143 §3 の表と同じ形式で、採番の根拠を本文の冒頭に1行残すこと★:

★採番についての注記★: grep -n "^### D-" docs/decisions.md | tail -3 で最終番号が ★D-1xx★ であることを実測してから D-1yy を採った。

10-2. docs/decisions.md に書くこと

  • ★D-143 §3 の「未回答」3件(#2 / #3 / #6)が、ユーザー判断で確定したこと★ (★#3 は「8列に戻す」ではなく★★「出発1列だけ」★★。★調査の提案と違う★点を明記)
  • ★clean_error を画面とCSVに出したこと★と、★★D-107 の申し送り 項1 が片付いたこと★★
  • ★null を「出発失敗」と表示すると決めたこと★(★ユーザー原文を転記★)と、 ★clean_error = 1(異常(清掃未実施))とは別の表示にしたこと★
  • ★★未知コード(5〜255)は String(code) のまま画面に出ること★★ (D-077。★「その他」「不明」に潰さない★)
  • ★存在しない「規約」を根拠にコンパクト表の行展開が無効化されていたこと★と、 ★★その記述を消したこと★★(★同じ思い込みが再発しないように、事実として残す★)
  • ★エリアモーダルをダッシュボードにも置いた理由★ (★空関数のままだと「押しても何も起きないボタン」を新たに作ることになる★)
  • ★CSVの列順を画面と揃えた理由★と、★★テストが列を位置で見ているので添字を直したこと★★
  • ★★やらないと決めた5件(§2)とその理由★★ — ★次の担当者が「作り忘れ」と誤認しないため★
  • ★★英語配布を決めた時点で 未対応 の対訳を必ず直す必要があること★★

10-3. docs/handoff.md に追記すること

  • ★★lib/i18n.ts の 未対応 → Unsupported は直していない★★(D-143 §3 #1)。 ★英語配布を決めた時点で必ず直す★。★要対応 にも同型の地雷がある★
  • ★凡例(判定基準(タスク完了率): / (閾値はシステム設定で変更可能))と アドバイス展開部の見出しが t() を通っていない★ — ★EN で和文が出る★。 ★段階6でダッシュボードでも出るようになった★
  • ★1h前ST / 30分前ST はダッシュボードには出していない★(ユーザー判断)
  • ★サイドバーの未対応件数バッジは足さない★(PATCH 未実装・DB更新用アカウント未受領)
  • ★D-Secアラートの期間フィルタは入れない★(入れるなら d-watch-api.yaml の変更から)

10-4. 完了条件

  • ★採番を実測してから採ったことが本文に書いてある★
  • ★lib/types/index.ts / lib/config/index.ts / d-watch-api.yaml に書いた D-1xx が
    実際の番号に置き換わっている★
  • docs/handoff.md に5件の申し送りが入っている
  • ★git add にファイルパスを明示している★(★git add -A を使っていない★)

コミットメッセージ案

docs: ダッシュボードと詳細ログの列修正を記録する(D-1xx)

やったこと4件とやらないと決めた5件の両方を残す。存在しない「規約」が
根拠になっていた経緯と、英語配布時に「未対応」の対訳を直す必要があることを
申し送りに入れた(指示書AO 段階7)。

11. ★実装者へ — 特に踏みやすい落とし穴★

# 落とし穴 どうなるか
★1★ ★判定・整形を components/ に書く★ ★★テストが走らない★★(vitest.config.ts:14 の include は lib/** と app/** のみ)。★落ちないのでゼロ件成功と区別がつかない★
★2★ ★colSpan の片方だけ直す★ ★tsc も lint もテストも通る★。★空状態か展開行のどちらかが詰まる。目視でしか気づけない★
★3★ ★段階5 を段階4 より先にやる★ ★展開行が4列ぶんしか広がらない★。★同じく気づけない★
★4★ ★clean_error ?? x を `\ \ x` と書く★ ★★0(正常)が「出発失敗」に化ける★★。★例外も tsc エラーも出ない★(段階1 期待値表 1-10)
★5★ ★clean_error === 1 と null を同じ表示に潰す★ ★「履歴が無い」と「未実施として記録された」の区別が消える★(期待値表 1-3)
★6★ ★未知コードを「その他」「不明」にする★ ★D-089 項3 / D-104 項2 に反する★。★破棄済み D-067 の方針と紛らわしい★。★String(code) が正★
★7★ ★ラベルの半角括弧を全角に直す★ ★定義書の原文が半角★(lib/config/index.ts:277-278)。★実データと突き合わせたときに原文が分からなくなる★
★8★ ★CSVテストの添字を直し忘れる★ ★テストが落ちるのでそこは安全★。★危険なのは「落ちたから添字を合わせる」だけで新しい列のテストを書かないこと★
★9★ ★CSVで csvCell() を通さない★ ★D-125 の関門が1つ増える★。★csv.ts:78-83 の約束が崩れる★
★10★ ★CSVの列順を画面と変える★ ★突き合わせのたびに並べ替えになる★
★11★ ★onOpenAreas を () => {} のまま残す★ ★★今直している「死んだボタン」と同じものを隣に作る★★(§8-4)
★12★ ★?.length / `\ \ ` でモーダルの応答を分岐する★ — ★cancelled フラグを落とすと古い応答が新しい表示を上書きする★
★13★ ★t() に 清掃結果 と別の意味の日本語を足す★ ★★lib/i18n.ts はキーが日本語原文の1階層辞書★★。★同じ日本語を別の意味で使うと必ず衝突する★(未対応 → Unsupported の事故が実在)。★足す前に必ず grep する★
★14★ ★セルの値を t() に通す★ ★CLEAN_ERROR_LABELS は定義書の原文★。★訳すと何が原文か分からなくなる★。★未知コードはそもそも訳せない★
★15★ ★「規約」のコメントを消して別の根拠を書く★ ★出所不明の根拠を足したのがこの事故の原因★。★「規約は存在しなかった」という事実を残す★
★16★ ★モック(d-watch-mock.jsx)を直す★ ★★原型として保存する★★(CLAUDE.md §7)。★1文字も書き換えない★
★17★ ★d-watch-api.yaml のスキーマを変える★ ★変更不要★(§1-5)。★直すのは説明文1行だけ★
★18★ ★新しいCSSに font-family を書く★ ★漢字が中華字形になる★(CLAUDE.md §0-3)。★そもそも今回CSSは要らない★
★19★ ★git add -A★ ★★禁止★★(CLAUDE.md §7。★別作業の途中ファイルを巻き込む事故歴あり★)
★20★ ★決定番号を先に決める★ ★★二重採番の事故歴あり★★。★着手直前に `grep -n "^### D-" \ tail -3` で実測する★

12. ★やらないこと(この指示書の範囲外)★

項目 なぜ
★未対応 の対訳キー衝突を直す★ ★英語を配布するか未定★(§2 #1 / D-143)。★★英語配布を決めた時点で必ず直す★★
★サイドバーの未対応件数バッジ★ ★PATCH 未実装・DB更新用アカウント未受領★(D-143)
★D-Secアラート「通信断」の色★ ★アンバー維持で確定★(D-143)
★D-Secアラートの期間フィルタ★ ★今は入れない★(D-143)。★入れるなら d-watch-api.yaml の変更から★
★アラート画面のバッテリー残量行★ ★意図した仕様★(D-105 項5)。★空欄のまま残すのが正★
★ダッシュボードに日付 / 1h前ST / 30分前ST を足す★ ★★ユーザーが「出発だけ」と決めた★★(D-143 §3 #3 の追加判断)
★凡例・アドバイス見出しの対訳★ ★既存の抜け★。★英語配布の判断とセットで別途★
★1h前ST / 30分前ST の対訳★ ★モックも実装も t() を通していない生文字列★。★詳細ログでも同じなので今回は変えない★
★ダッシュボードの清掃ログをCSVに出す★ ★ユーザーが求めていない★(CSVは詳細ログのみ)
★clean_error による絞り込み★ ★API契約に無い★。★要るなら d-watch-api.yaml の変更から★
★画面のテストを書けるようにする★ ★vitest.config.ts の include を広げる話★。★別課題★
★スマートフォン幅のレイアウト★ 別課題