# 指示書BH — 清掃詳細ログとエラー一覧の並べ替え（2026-11-04）

**担当**: Cursor。監督（Claude Code）がレビューする。
**前提**: develop の `38e8f5d` 以降（施設ロボットマスタの並べ替え＝先行例が入っている版）。
**先行例**: `38e8f5d` — `lib/facilities/query.ts`（`FACILITY_SORT_KEYS`・`parseFacilityQuery()` の `sort`/`order`・`sortFacilities()`）、
`components/features/facilities/FacilitiesScreen.tsx`（`nextSort()`・`sortableTh()`・`changeSort()`）、`d-watch-api.yaml` の `/facilities` の `sort`/`order`。
★作業の前に必ず読むこと★。この指示書は「同じ形を2画面に広げる」もので、★先行例と違うところ（§2-1 の表）★に特に注意すること。

---

## 0. ユーザーの指示（原文）

- 2026-11-04「詳細ログやエラー一覧も項目でソートできるようにしたいです」
- 監督の質問への回答（2026-11-04）:
  - 清掃詳細ログの列: 「機種は含めて、事前チェックは除く」
  - 施設名: 「施設名は外す」（エラー一覧についても確認し「外す（6列）」）
  - 1回目のクリックの向き: 「悪いほうが上」
  - エラー一覧の「紐づく予定」: 並べ替えではなく ★絞り込みのプルダウン★ にする（軽いほう）。選択肢は ★3つ（すべて／予定あり／予定外）★
  - プルダウンの先頭の表示: 「予定：すべて」（監督の問い「今のまま／先頭だけ欄の名前を入れる」に「後者」。2026-11-04）
- 監督が示し、ユーザーが異を唱えなかった既定（このまま実装する）:
  - 並べ替えはサーバー側で全件に対して行う（ページ送りの前）
  - 同じ値どうしは、今の並び順（既定順）に従う
  - 空の値（完了率「-」・機種/シリアルなし・備考なし）は、向きに関係なく下
  - 日付・検知時刻は「新しい順 ↔ 古い順」の2状態
  - 完了率は丸める前の値で比べる
  - 清掃詳細ログの CSV は画面と同じ並び順
  - ダッシュボードの縮小表示には並べ替えを付けない
  - エラー一覧の編集中は並べ替えのボタンを押せない。保存直後は行を動かさない

## 1. 並べ替えられる列（これ以外は今のまま。見出しはボタンにしない）

### 1-1. 清掃詳細ログ（7列）

| 見出し | `sort` の値 | 1回目（▲ `asc`） | 2回目（▼ `desc`） | 下に固定する行 | 見出しのそろえ（今のまま） |
|---|---|---|---|---|---|
| 機種 | `model_name` | 文字の昇順 | 文字の降順 | `model_name` が null | 左 |
| シリアル | `serial_number` | 文字の昇順 | 文字の降順 | `serial_number` が null | 左 |
| 日付 | `scheduled_at` | 古い順 | ★無し（2状態）★ | 無し | 左 |
| 出発 | `departed` | ✗ が上 | ✓ が上 | 結果待ち | 中央 |
| 清掃結果 | `task_status` | 悪い順（§2-2 の順位） | 良い順 | 結果待ち | 左 |
| 完了率 | `completion_rate` | 低い順 | 高い順 | 完了率 null（出発失敗）→ その下に結果待ち | 右 |
| 判定 | `grade` | × → △ → ○ → ◎ | ◎ → ○ → △ → × | 結果待ち | 中央 |

- ★並べ替えない列★: 施設名（ユーザー指示）・予定（日付と同じ値。日付の見出しで並べる）・事前チェック1回目/2回目（ユーザー指示）
- ★日付は2状態★: 既定順が「新しい順」なので、日付の見出しは「既定（▼ 新しい順）↔ 古い順（▲）」を行き来する。
  ★既定順のときは日付の見出しに ▼ を出す★（`aria-sort="descending"`）。他の列で並べているあいだは日付は ⇅（★§12 で薄い △ に変更★）
- 「下に固定する行」は ★`asc` でも `desc` でも一番下★（向きで反転させない）。固定する行どうしは既定順
- そろえは今の `CleanLogTable.tsx` の見出しの `textAlign` を変えない（実物で確かめること。上の表と違ったら実物が正）

### 1-2. エラー一覧（6列）

| 見出し | `sort` の値 | 1回目（▲ `asc`） | 2回目（▼ `desc`） | 下に固定する行 |
|---|---|---|---|---|
| 検知時刻 | `detected_at` | 古い順 | ★無し（2状態）★ | 無し |
| シリアル | `serial_number` | 文字の昇順 | 文字の降順 | 無し（`---` も値として並べる） |
| 種類 | `error_type` | 文字の昇順 | 文字の降順 | 無し |
| レベル | `error_level` | 文字の昇順（CRITICAL → ERROR → WARNING） | 文字の降順 | 無し |
| 対応状況 | `resolution_status` | 未解消 → 対応中 → 解消（DB の `status` 0→1→2） | 解消 → 対応中 → 未解消 | 無し |
| 備考 | `note` | 文字の昇順 | 文字の降順 | `note` が null・空文字・空白だけ |

- ★並べ替えない列★: 施設名（ユーザー指示）・紐づく予定（→ §4 の絞り込み）・詳細（JP と EN で並びが変わるため）
- ★検知時刻は2状態★（清掃詳細ログの日付と同じ。既定順のとき ▼）
- ★レベルは文字の順★。重大度の順位表を作らないこと（D-077: 値を解釈しない）。今の3つの値では文字の順が重大度の順と一致するだけ
- 見出しのそろえは6列とも左（今のまま）

### 1-3. 見出しの切り替え（両画面共通）

| 今の状態 | 押した列 | 次の状態 |
|---|---|---|
| 既定 / 別の列で並べ中 | 普通の列 | その列の `asc`（▲） |
| その列の `asc` | 同じ列 | その列の `desc`（▼） |
| その列の `desc` | 同じ列 | 既定（`sort` を送らない） |
| 既定 / 別の列で並べ中 | 2状態の列（日付・検知時刻） | その列の `asc`（▲ 古い順） |
| 2状態の列の `asc` | 同じ列 | 既定 |

- ★1回目は必ず `asc`★（ユーザー指示「悪いほうが上」。完了率・判定・出発・清掃結果・対応状況は「`asc` ＝ 悪いほう」になるよう §2 で定義してある）。
  ★施設ロボットマスタは今のまま（1回目 `desc` ＝ ○ が上）★ — 切り替えの部品を共通にしても、施設マスタの動きは変えない
- 印: `asc` ＝ ▲、`desc` ＝ ▼、並べていない列 ＝ 薄い ⇅（施設マスタと同じ）。★§12 で変更: 並べていない列は薄い △、印の色は白（ダーク）★
- 並べ替えを変えたら ★同じ操作の中で `setPage(1)`★（`useEffect` で後追いしない）。`queryString` の `useMemo` の依存に `sort` を入れる
- 並べ替えの状態は URL に載せない（再読み込みで既定に戻ってよい。今の絞り込みと同じ）

## 2. 並べ方の定義（API）

### 2-1. 共通の規則

- 比較は「主キーの比較 → 同じなら既定順の比較」。★`desc` は主キーの比較だけを反転する★。
  ★`asc` の結果を `reverse()` して `desc` を作らないこと★（同じ値の中まで逆になる。`lib/facilities/query.ts` の `sortFacilities()` の注記と同じ）
- 「下に固定する行」は主キーの比較より前に分ける（向きの反転の外）。固定する行どうしは既定順
- 既定順:
  - 清掃詳細ログ ＝ `compareCleanLogItems()`（`lib/db/clean-log-mapping.ts:394`）
  - エラー一覧 ＝ `compareErrorReportKeys()`（`lib/error-reports/query.ts:134`）
- ★文字の比較は両画面とも `<` / `>`（コードポイント順）★。`localeCompare` を使わない（`lib/masters/query.ts:62-66` の方針。実行環境で順が変わる）。大文字小文字は区別する
- 完了率は ★生値★ で比べる（CLAUDE.md §4.3。89.5 / 90.0 / 90.1 は表示がどれも 90% だが、生値で比べて別の順位になる）

★★先行例（施設マスタ）と違うところ — ここを写し間違えると既定順が壊れる★★

| | 施設マスタ（先行例） | 今回（両画面） |
|---|---|---|
| `sort` を省略したとき | `"id"` に正規化（既定値あり） | ★`null`★（型は `CleanLogSortKey \| null` / `ErrorReportSortKey \| null`）。★既定値を `"scheduled_at"` / `"detected_at"` にしないこと★（`order` の既定が `asc` なので古い順になってしまう） |
| `order` を省略したとき | `"desc"` | ★`"asc"`★（1回目は悪いほうが上のため） |
| `sort` が既定（null）のとき | — | `order` は検証する（不正なら 422）が、並びには使わない |
| 2状態の列の `desc`（`sort=scheduled_at&order=desc` / `sort=detected_at&order=desc`） | — | ★パースの段階で `sort: null` に畳む★（既定順と同じ意味。422 にしない。画面は送らない） |
| `sort` が null のときの並べ替え関数 | — | ★並べ替えず、入力の写し（`[...items]`）をそのまま返す★（§7 罠1。fixtures 版の既定順を変えない） |
| 型の名前 | `FacilitySortOrder` | ★`TableSortOrder`★（`lib/table-sort.ts`。Prisma の生成物に同名の `SortOrder` があるので使わない） |

### 2-2. 清掃詳細ログの順位

- 出発: ✗（`departed` false かつ `result_pending` false）＝ 0、✓ ＝ 1。`asc` ＝ ✗ が上。結果待ち（`result_pending` true）は下に固定
- 判定: × ＝ 1、△ ＝ 2、○ ＝ 3、◎ ＝ 4。`asc` ＝ × が上。結果待ちは下に固定（`grade` は "×" のままなので ★`result_pending` を先に見る★）
- 清掃結果（`task_status`）。`asc` ＝ 順位の小さい順。結果待ちは下に固定:

  | 順位 | 行 |
  |---|---|
  | 1 | 出発失敗（`task_status` が null で結果待ちでない） |
  | 2 | 5 清掃異常終了 |
  | 3 | 2 清掃中断 |
  | 4 | 3 と 6 清掃キャンセル（★同じ順位★。表示名が同じため） |
  | 5 | 表に無い値（「不明(n)」。7 以上・負の値など） |
  | 6 | 0 清掃開始前 |
  | 7 | 1 清掃中 |
  | 8 | 4 清掃正常終了 |

  ★この順位表は `lib/config/index.ts` の `CLEAN_TASK_STATUS_LABELS` の隣に定数で置く★（`CLEAN_TASK_STATUS_SORT_RANK` など）。
  コードに数字を直書きしない。判定（◎○△×）には使わない（表示の並びだけ）
- 完了率: `completion_rate` の生値。null（出発失敗）は下に固定、結果待ちはさらにその下
- 機種・シリアル: 文字の比較。null は下に固定
- 日付: `scheduled_at`（予定外の行は `started_at` が入っている。今の既定順と同じ値）。`Date.parse` の差で比べる

### 2-3. エラー一覧の順位

- 対応状況: DB の `status`（0 未解消 / 1 対応中 / 2 解消）の数値。`asc` ＝ 未解消が上
- 備考: 前後の空白を除いた文字列。空なら下に固定
- 検知時刻・シリアル・種類・レベル: §1-2 の表のとおり

## 3. 期待値表（★監督が確定。テストはこれをそのまま使う。数字・順番を変えないこと★）

### 表S1 — 清掃詳細ログ（`sortCleanLogs()` の純粋関数のテスト。行は `CleanLogItem` としてテストの中で作る。置き場所 `lib/clean-logs/__tests__/sort.test.ts`）

| 行 | `id` | `row_key` | `scheduled_at`（JST） | 機種 | シリアル | 出発 | `task_status` | 完了率 | 判定 |
|---|---|---|---|---|---|---|---|---|---|
| R1 | 101 | `tt-101` | 11-03 02:00 | CC1 | S-003 | ✓ | 4 | 95.0 | ◎ |
| R2 | 102 | `tt-102` | 11-03 02:00 | CC1 | S-001 | ✓ | 2 | 45.0 | △ |
| R3 | 103 | `tt-103` | 11-02 02:00 | MT1 | S-002 | ✗（出発失敗） | null | null | × |
| R4 | 104 | `tt-104` | 11-03 03:00 | null | null | 結果待ち（`result_pending` true・`departed` false） | null | null | ×（画面「-」） |
| R5 | ★null★ | `rp-9001` | 11-02 21:00 | CC1 | S-004 | ✓ | 6 | 9.9 | × |
| R6 | 105 | `tt-105` | 11-01 02:00 | CC1 | S-005 | ✓ | 7 | 89.5 | ○ |
| R7 | 106 | `tt-106` | 11-01 02:00 | CC1 | S-006 | ✓ | 3 | 90.0 | ◎ |
| R8 | 107 | `tt-107` | 11-02 01:00 | CC1 | S-007 | ✓ | 5 | 70.0 | ○ |

- R5 は予定外（`unscheduled` true・`id` null・`report_id` あり）。ほかは予定のある行（`unscheduled` false）
- 既定順（`compareCleanLogItems()`）: **R4, R1, R2, R5, R3, R8, R6, R7**

| # | 指定 | 期待する順 |
|---|---|---|
| S1-1 | `sort` null（入力は既定順 R4, R1, R2, R5, R3, R8, R6, R7） | 同じ順で返る |
| S1-1b | `sort` null（入力をわざと R8, R1, R7, R2, R6, R3, R5, R4 にする） | ★その順のまま返る（並べ直さない）★ |
| S1-2 | `scheduled_at` asc | R6, R7, R8, R3, R5, R1, R2, R4（同じ時刻の R6・R7 と R1・R2 は既定順のまま） |
| S1-4 | `completion_rate` asc | R5, R2, R8, R6, R7, R1, R3, R4 |
| S1-5 | `completion_rate` desc | R1, R7, R6, R8, R2, R5, R3, R4 |
| S1-6 | `grade` asc | R5, R3, R2, R8, R6, R1, R7, R4 |
| S1-7 | `grade` desc | R1, R7, R8, R6, R2, R5, R3, R4 |
| S1-8 | `departed` asc | R3, R1, R2, R5, R8, R6, R7, R4 |
| S1-9 | `departed` desc | R1, R2, R5, R8, R6, R7, R3, R4 |
| S1-10 | `task_status` asc | R3, R8, R2, R5, R7, R6, R1, R4（キャンセルの R5・R7 は既定順のまま） |
| S1-11 | `task_status` desc | R1, R6, R5, R7, R2, R8, R3, R4（★R5・R7 は逆にならない★） |
| S1-12 | `model_name` asc | R1, R2, R5, R8, R6, R7, R3, R4 |
| S1-13 | `model_name` desc | R3, R1, R2, R5, R8, R6, R7, R4 |
| S1-14 | `serial_number` asc | R2, R3, R1, R5, R6, R7, R8, R4 |
| S1-15 | `serial_number` desc | R8, R7, R6, R5, R1, R3, R2, R4 |
| S1-16 | `grade` asc の結果を `slice(3, 6)` | R8, R6, R1（並べ替えたあとの件数は 8） |
| S1-17 | 入力の配列 | 書き換えない（並べ替えの前後で元の配列の順が同じ） |

- S1-6 の × の中（R5・R3）と ◎ の中（R1・R7）は既定順のまま
- S1-4 で 89.5（R6）と 90.0（R7）が別の位置になる（表示は両方 90%）
- （S1-3 は欠番。`scheduled_at` desc はパースで null に畳むので §表S2 の S2-7 で見る）

### 表S2 — 清掃詳細ログのクエリとルート

| # | 何を | 期待 |
|---|---|---|
| S2-1 | `parseCleanLogQuery()`・`sort` 省略 | `sort: null`・`order: "asc"` |
| S2-2 | `sort=` / `sort=%20` | 省略と同じ |
| S2-3 | `sort=grade`（`order` 省略） | `sort: "grade"`・`order: "asc"` |
| S2-4 | `sort=facility_name` | 422（★並べ替えない列★） |
| S2-5 | `sort=precheck_1` / `sort=foo` / `sort=Grade` | 422（大文字小文字は区別） |
| S2-6 | `order=up`（`sort` 省略でも） | 422（施設マスタと同じく検証する） |
| S2-7 | `sort=scheduled_at&order=desc` | 422 にならず、★`sort: null`★ に畳まれる |
| S2-8 | 422 の本文 | 入力値をエコーしない。`details` のキーはパラメータ名、中身は受け付ける値の一覧（`lib/facilities/query.ts` と同じ形） |
| S2-9 | `/clean-logs/export`（`app/api/clean-logs/export` のテスト。`db.cleanLogs` のモックが表S1 の8行を返す）に `sort=grade&order=asc` | CSV の行のシリアル列が S-004, S-002, S-001, S-007, S-005, S-003, S-006, 空 の順。不正な `sort` で 422 |
| S2-10 | `/clean-logs`（ルートのテスト）に `sort` を付けて `per_page`・`page` 2 | 2ページ目にも並べ替えが効き、`meta.total` は絞り込み後の件数（並べ替えで変わらない） |

### 表E1 — エラー一覧（`filterSortErrorReportKeys()` の純粋関数のテスト。`include_resolved` true）

★行は `ErrorReportKeySource`（DB の行の形。`clean_timetable_id` は `BigInt(501)` / `BigInt(CLEAN_TIMETABLE_ID_UNSCHEDULED)` / `null`）で作り、
★`errorReportKeyFromRow()` を通してから★ `filterSortErrorReportKeys()` に渡す（0 と NULL がどちらも予定外になることを、キーを手で作らずに確かめるため）。
置き場所は `lib/error-reports/__tests__/query.test.ts` の新しい `describe`。既存の表1 の定数（E1〜E9 など）とぶつからない名前にする（例: `BH_E1`）。

| 行 | `error_id` | 検知時刻（JST） | シリアル | 種類 | レベル | 対応状況（`status`） | 備考（`comment`） | `clean_timetable_id` |
|---|---|---|---|---|---|---|---|---|
| E-1 | `E-1` | 11-03 02:00 | 8111R-0002 | Offline | ERROR | 0 未解消 | null | 501 |
| E-2 | `E-2` | 11-03 02:00 | --- | SECURITY_ON | CRITICAL | 1 対応中 | 回線確認中 | 0 |
| E-3 | `E-3` | 11-02 23:00 | 8111R-0001 | BrushError | WARNING | 0 未解消 | `""`（空文字） | NULL |
| E-4 | `E-4` | 11-02 22:00 | DS-005 | D-S_CONTROL_FAILURE | CRITICAL | 2 解消 | 復帰を確認 | 502 |
| E-5 | `E-5` | 11-01 10:00 | 8111R-0001 | Offline | ERROR | 1 対応中 | null | 503 |
| E-10 | `E-10` | 11-02 22:00 | 8111R-0003 | LowBattery | WARNING | 0 未解消 | 現場に連絡済み | 504 |

既定順（`compareErrorReportKeys()`）: **E-1, E-2, E-3, E-10, E-4, E-5**（同じ 11-02 22:00 は `"E-10" < "E-4"`）

| # | 指定 | 期待する順 |
|---|---|---|
| E1-1 | `sort` null | E-1, E-2, E-3, E-10, E-4, E-5 |
| E1-2 | `detected_at` asc | E-5, E-10, E-4, E-3, E-1, E-2 |
| E1-3 | `serial_number` asc | E-2, E-3, E-5, E-1, E-10, E-4（`---` が先頭。同じシリアルの E-3・E-5 は既定順） |
| E1-4 | `serial_number` desc | E-4, E-10, E-1, E-3, E-5, E-2（★E-3・E-5 は逆にならない★） |
| E1-5 | `error_type` asc | E-3, E-4, E-10, E-1, E-5, E-2 |
| E1-6 | `error_type` desc | E-2, E-1, E-5, E-10, E-4, E-3 |
| E1-7 | `error_level` asc | E-2, E-4, E-1, E-5, E-3, E-10 |
| E1-8 | `error_level` desc | E-3, E-10, E-1, E-5, E-2, E-4 |
| E1-9 | `resolution_status` asc | E-1, E-3, E-10, E-2, E-5, E-4 |
| E1-10 | `resolution_status` desc | E-4, E-2, E-5, E-1, E-3, E-10 |
| E1-11 | `note` asc | E-2, E-4, E-10, E-1, E-3, E-5（回 U+56DE < 復 U+5FA9 < 現 U+73FE。空の3行は既定順で下） |
| E1-12 | `note` desc | E-10, E-4, E-2, E-1, E-3, E-5（空の3行は下のまま） |
| E1-13 | `schedule` `scheduled` | E-1, E-10, E-4, E-5 |
| E1-14 | `schedule` `unscheduled` | E-2, E-3（`clean_timetable_id` が 0 と NULL の両方） |
| E1-15 | `schedule` `unscheduled` ＋ `serial_number` desc | E-3, E-2 |
| E1-16 | `include_resolved` false・`resolution_status` asc | E-1, E-3, E-10, E-2, E-5（E-4 は解消なので出ない） |
| E1-17 | `include_resolved` false・`schedule` `scheduled` | E-1, E-10, E-5（3件） |

### 表E2 — エラー一覧のクエリ（`parseErrorReportQuery()`）

| # | クエリ | 期待 |
|---|---|---|
| E2-1 | `sort`・`order`・`schedule` 省略 | `sort: null`・`order: "asc"`・`schedule: null`（今と同じ並び・絞り込みなし） |
| E2-2 | 空文字・空白だけ | 省略と同じ |
| E2-3 | `sort=facility_name` / `sort=schedule_link` / `sort=error_list_id` / `sort=foo` | 422 |
| E2-4 | `order=up` | 422 |
| E2-5 | `schedule=all` / `schedule=main` / `schedule=Scheduled` | 422（受け付けるのは `scheduled` と `unscheduled` だけ。「すべて」は送らない） |
| E2-6 | 422 の本文 | S2-8 と同じ作法 |
| E2-7 | `sort=detected_at&order=desc` | 422 にならず、★`sort: null`★ に畳まれる |
| E2-8 | `sort=error_level`（`order` 省略） | `order: "asc"` |

## 4. エラー一覧の「紐づく予定」の絞り込み（プルダウン）

- クエリパラメータ `schedule`: `scheduled`（予定あり）/ `unscheduled`（予定外）。省略 ＝ すべて
- ★判定は `scheduledTimetableId()`（`lib/db/timetable-link.ts:20`）の1本★。`clean_timetable_id` が 0 か NULL なら予定外（CLAUDE.md §4.7）。
  ★`=== null` / `{ not: null }` / `=== 0` を直書きしない★
- 「予定あり」には、本清掃・事前チェック・予定不明（予定の表に id が見つからない）の行がすべて入る（★予定の表は引かない★。軽くするための決定）。
  つまり ★`unscheduled` ＝ 「紐づく予定」が「予定なし」（`schedule_link: "none"`）の行、`scheduled` ＝ それ以外★
- 絞り込みはアプリ側（`filterSortErrorReportKeys()`）で行う。`meta.total` は絞り込み後の件数
  - ★SQL の where に降ろさないこと★ — `errorReportListWhere()` は期間で既に `OR` を使っており、もう1つの `OR` を足すと条件が上書きされる事故が起きやすい。
    1段目は今も条件に合う全件のキーを読んでいるので、アプリで振り分けても読む量は増えない
- 画面: 絞り込み欄（`<fieldset>` の中）に `<select>` を足す。`aria-label`・`title` は「紐づく予定」。
  選択肢は ★「予定：すべて」（値は空＝送らない）「予定あり」「予定外」★（★文言はユーザー指示。変えない★。先頭だけ欄の名前を入れるのは、ほかのプルダウンと並んだときに何の欄か分かるように）
  - ★幅は固定★（既存の絞り込みの `<select>` と同じく `styles.select` ＋ 固定幅のクラス。いちばん長い表示（EN の "Schedule: All" か "Unscheduled"）と ▼ が収まる幅。JP/EN の切り替えでツールバーが動かないように。D-162）
  - 置く位置は「解消済みも表示」のチェックの左
  - 英訳: 「紐づく予定」"Schedule"（既存）、「予定外」"Unscheduled"（既存）、★「予定：すべて」"Schedule: All"・「予定あり」"Scheduled" を `lib/i18n.ts` に足す★（既にあるキーは足さない）
  - 変えたら `setPage(1)`（期間の入力と同じ書き方）。`<fieldset disabled>` の中なので編集中は自動で押せなくなる
- D-Sec アラート（`lib/ds-alerts/query.ts` の `sortDsAlerts()`）には関係させない

## 5. 実装の置き場所と変更点（★全件。漏らさないこと★）

### 段階1 — API 契約（★先に直す。CLAUDE.md §6-1★）

- `d-watch-api.yaml`
  - `/clean-logs` と `/clean-logs/export`: `sort`（enum 7値。★`default` は書かない★。説明に「省略時は既定順」）・`order`（enum `[asc, desc]`・`default: asc`。説明に「施設マスタ（desc）と違い、1回目は悪いほうが上のため asc」「`sort` が省略でも検証するが並びには効かない」「`sort=scheduled_at&order=desc` は既定順と同じ」）。
    §1-1・§2-2 の定義、「並べ替えは絞り込みのあと・ページングの前」「同じ値は既定順」「下に固定する行」を説明に書く。422 の説明に `sort`/`order` を足す。
    既定順の記述（`:494` 付近と `CleanLogItem` の説明 `:2239` 付近）は「`sort` を省略したとき」と書き足す
  - `/errors`: `sort`（enum 6値。`default` なし）・`order`（`/clean-logs` と同じ）・`schedule`（enum `[scheduled, unscheduled]`。説明: `unscheduled` ＝ `schedule_link` が `none` の行、`scheduled` ＝ `main` / `precheck` / `unknown` の行。予定の表は引かない）。
    ★`:909-910` の「ソート指定のクエリパラメータは無い」を書き換える★。422 の説明に `sort`/`order`/`schedule` を足す。`sort=detected_at&order=desc` は既定順と同じ、と書く

### 段階2 — 共通の部品

- `lib/table-sort.ts`（新規。純粋関数）
  - 型 `TableSortOrder = "asc" | "desc"`、`SortState<K> = { key: K; order: TableSortOrder } | null`
  - `nextSortState(current, key, { first, twoState })` — §1-3 の表をそのまま実装。`first` は `"asc"` か `"desc"`（施設マスタは `"desc"`）
  - `displayOrder(state, key, defaultKey?)` — 見出しに出す向き。`state` が null で `key === defaultKey` なら `"desc"`（既定順のとき日付・検知時刻に ▼）、
    `state` があれば `state.key === key ? state.order : null`、それ以外は null。施設マスタは `defaultKey` を渡さない（今のまま）
  - テスト `lib/__tests__/table-sort.test.ts`:
    - `nextSortState`: §1-3 の5行 ＋ 施設マスタの動き（`first: "desc"`: 既定 → desc → asc → 既定）
    - `displayOrder`: 「null・日付 → desc」「null・他の列 → null」「{日付, asc}・日付 → asc」「{完了率, asc}・日付 → null」「null・`defaultKey` なし → null」
- `components/ui/SortableTh.tsx`（新規）
  - `<th aria-sort>` の中に `<button type="button">`。props: 見出しの文字、`order`（`displayOrder()` の値）、`onClick`、`disabled`、`ariaLabel`（「○○で並べ替え」）、
    ★`align: "left" | "center" | "right"`★（`th` の `text-align` とボタンの `justify-content` に反映。今の見出しのそろえを変えないため）、
    ★`children`★（見出しの中に並べ替え以外のボタンを置く。清掃詳細ログの「判定」の「?」）
  - ★ボタンの中にボタンを入れない（HTML として不正）★。`children` があるときは、ボタンを `width: auto` にして、`th` の中でボタンと `children` を inline-flex で1行に並べる
    （`width: 100%` のままだと「?」が押し出されて、折り返さない見出しからはみ出す）
  - 印は `aria-hidden`。CSS は施設マスタの `.thSortable` 系（`FacilitiesScreen.module.css:135-166` 付近）を `components/ui/SortableTh.module.css` に移し、そろえを props で変えられるようにする
- `components/features/facilities/FacilitiesScreen.tsx` と `.module.css`: 上の部品を使うように置き換える（`align="center"`・`first: "desc"`）。★動きも見た目も1つも変えない★。
  置き換えたら施設マスタの画面で ▼ → ▲ → 既定 を確かめる

### 段階3 — 清掃詳細ログの API

- `lib/types/index.ts`: `CleanLogSortKey`（7値）を足す
- `lib/clean-logs/query.ts`: ★既存の `CleanLogQuery`（`:15`。`parseCleanLogQuery()` が返す `QueryParseResult<CleanLogQuery>` の `query`）★ に
  `sort: CleanLogSortKey | null`・`order: TableSortOrder` を足す（★`lib/types` に同じ名前の型を作らない★）。
  `parseCleanLogQuery()` に検証（表S2）。受け付ける値は配列1か所（`CLEAN_LOG_SORT_KEYS`）。2状態の列の desc は `sort: null` に畳む
  - ★今の `result` パラメータは空文字で 422 になる（`:122-127`）が、新しい `sort`/`order` は施設マスタに合わせて空文字 ＝ 省略★。`result` の挙動は変えない
- `lib/clean-logs/sort.ts`（新規）: `sortCleanLogs(items, sort, order)`。表S1 のテストを `lib/clean-logs/__tests__/sort.test.ts` に
  - ★`sort` が null なら並べ替えず `[...items]` を返す★
  - ★`filterCleanLogs()` の中に入れないこと★（`lib/clean-logs/__tests__/query.test.ts` が「入力の順を保つ」ことに依存している）
  - ★データ層（`db.cleanLogs()`・`compareCleanLogItems()`）の既定順を変えないこと★ — ダッシュボードの `problem_logs`（先頭5件）がこの順に依存している（`app/api/dashboard/route.ts`）
- `lib/config/index.ts`: 清掃結果の順位表（§2-2）
- `app/api/clean-logs/route.ts`: `filterCleanLogs()` のあと、`slice()` の前に `sortCleanLogs()`。`capCleanLogs()` の位置は今のまま。
  ルート冒頭の手順コメントに「並べ替え（`sortCleanLogs()`。`sort` 省略時は並べ替えない）」を足す。`capCleanLogs()` の docstring に「`filterCleanLogs()` → `sortCleanLogs()` → ページ切り出しの後」と書き足す
- `app/api/clean-logs/export/route.ts`: ★行数の上限の判定（`filtered.length > MAX_CSV_ROWS` の 422。`:79` 付近）の後、`buildCleanLogCsv()` の直前★に `sortCleanLogs()`
  （★入れ忘れると CSV だけ黙って既定順になる★。上限の判定より前に置くと、どうせ 422 で捨てる大量の行まで並べ替えることになる）
- ついでに直す古いコメント（「表に無い値（6 など）は数字のまま」→ 今は 6 は清掃キャンセル、7 以上は「不明(7)」）:
  `lib/clean-logs/csv.ts:43` 付近・`lib/types/index.ts:316` 付近・`lib/i18n.ts:225` 付近

### 段階4 — 清掃詳細ログの画面

- `components/features/detail-log/DetailLogScreen.tsx`: `sort` の状態（`SortState<CleanLogSortKey>`）。`queryString` に `sort`・`order` を載せる（既定のときは送らない）。
  CSV のリンクは同じ `queryString` を使っているので自動で揃う（★揃っていることを確かめる★）。並べ替えを変えたら `setPage(1)`
- `components/features/detail-log/CleanLogTable.tsx`: props `sort?: SortState<CleanLogSortKey>` と `onSort?: (key: CleanLogSortKey) => void` を足す（★省略可能★）。
  `onSort` が渡されたときだけ7列の見出しを `SortableTh` にする（向きは `displayOrder(sort, key, "scheduled_at")`）。
  ★ダッシュボード（`compact`）は渡さない → 今のまま★。「判定」の見出しの「?」ボタンは `SortableTh` の `children` に入れる
- 列幅: `table-layout: fixed`・見出しは折り返さない。★印を足すと EN で見出しがはみ出す列がある★（出発 56px・日付 64px・完了率・清掃結果・判定など。事前調査の計算）。
  並べ替える列の見出しの左右の余白を詰める（施設マスタは 12px → 6px）。★列の合計幅（最小 1134px）を広げないで収める★。収まらない列があれば監督に報告

### 段階5 — エラー一覧の API

- `lib/db/robot-error-select.ts` の `ROBOT_ERROR_LIST_KEY_SELECT`（1段目）に `serial_number`・`error_level`・`comment` を足す（`clean_timetable_id` は既にある）。
  ★`robot_id` / `robot` は足さない★（`robot-error-select.test.ts` が見張っている）。
  `:69-71` 付近のコメント「`clean_timetable_id` は並べ替えには使わない」を「予定あり／予定外の絞り込みに使う。`serial_number`・`error_level`・`comment` は並べ替えに使う」に直す
- `lib/db/error-report-mapping.ts`（★入力と出力で足すものが違う★）:
  - `ErrorReportKeySource`（DB の列名）に `clean_timetable_id: bigint | null`・`serial_number: string`・`error_level: string`・`comment: string | null`
  - `ErrorReportKey` に `serial_number`・`error_level`・`note: string | null`（`comment` の生値。空かどうかは並べ替えの側で trim して決める）・`scheduled: boolean`（`scheduledTimetableId(row.clean_timetable_id) !== null`）
  - `errorReportKeyFromRow()` で詰め替える
- `lib/types/index.ts`: `ErrorReportSortKey`（6値）・`ErrorScheduleFilter`（`"scheduled" | "unscheduled"`）を足す。
  ★`ErrorReportDbQuery`（`:807` 付近。「引数は必須」の作法 `:802`）★ に `sort: ErrorReportSortKey | null`・`order: TableSortOrder`・`schedule: ErrorScheduleFilter | null` を足し、
  `:802-806` の docstring に「`sort`・`order`・`schedule` もアプリ側で処理する」と書き足す
- `lib/error-reports/query.ts`: ★既存の `ErrorReportQuery`（`:19`。camelCase）★ にも同じ3つを足す（★`lib/types` に同じ名前の型を作らない★）。
  `parseErrorReportQuery()` に検証（表E2）。`filterSortErrorReportKeys()` に `schedule` の絞り込み（`key.scheduled` を見る）と `sort`/`order` の並べ替え（表E1）。
  `compareErrorReportKeys()` は既定順としてそのまま残す
- `lib/db/index.ts` の `errorReports()`（Prisma 版 `:1853` 付近・fixtures 版 `:715` 付近）: 新しいフィールドを `filterSortErrorReportKeys()` に渡すだけ。2段目は変えない
- `app/api/errors/route.ts`: パース結果（camelCase）を `ErrorReportDbQuery` に詰め替えて渡す

### 段階6 — エラー一覧の画面

- `components/features/errors/ErrorsScreen.tsx`: `sort` の状態と §4 のプルダウン。`queryString`（`:258-276` 付近）に `sort`・`order`・`schedule` を載せる
  - 6列の見出しを `SortableTh`（`align="left"`。向きは `displayOrder(sort, key, "detected_at")`）に。
    ★`disabled={editing}`★（見出しは `<fieldset>` の外なので、明示しないと編集中も押せてしまう。押すと別ページの下書きが保存されずに消える — `lib/errors/edit-drafts.ts`）
  - ★保存後は取り直さない（今のまま）★。並べている列の値を変えた行は、次に読み込むまで今の位置に残る（「解消にしても次に取り直すまで消さない」`:374` と同じ考え方）
  - 列数 `COLUMN_COUNT`（`:71`）と `<colgroup>` は変わらない
- 列幅: 「DETECTED AT」は約113px で列は 120px。印を足すとはみ出す。段階4 と同じく余白を詰めて収める

### 段階4・6 共通 — `lib/i18n.ts`

- 並べ替えボタンの `aria-label` の英訳（「機種で並べ替え」"Sort by model" など）と §4 の2語。
  ★両画面ぶんをまとめて1か所に足す。両画面に共通の「シリアルで並べ替え」は1回だけ。既にあるキーは足さない★（同じキーを2回書くと tsc エラーになる）

## 6. 既存テストの扱い

- ★新しい必須フィールド（`sort`・`order`・`schedule`、キーの `serial_number` など）を足したために、型エラーや `toEqual` の形の違いで壊れたテストは、
  既定値（`sort: null, order: "asc", schedule: null`、行の新しい列）を足すだけなら直してよい★。★並び・件数・where の期待値は変えない★
- 分かっているもの（行番号は目安）:
  - `lib/db/__tests__/fixture-clean-logs.test.ts:20-34` の `memoryQuery()`
  - `lib/error-reports/__tests__/query.test.ts` の `key()` ヘルパー（`:30-40`）・`base`（`:55-63`）・`parse("")` の `toEqual`（`:207-221`）。★表1（`:70-166`。監督確定）の期待値は変えない★
  - `lib/db/__tests__/fixture-error-reports.test.ts` の `BASE`
  - `lib/db/__tests__/error-report-mapping.test.ts:390-399`・`:407-416`・`:440-448`（キーと元の行の形）
  - `app/api/errors/__tests__/route.test.ts:557-575`（パース結果・1段目の select が定数と一致）
- ★並び順を固定しているテストは変わらないはず★: `app/api/clean-logs/__tests__/route.test.ts`（`:418` など）・`lib/db/__tests__/unscheduled-clean-log.test.ts:190-224`・
  `lib/db/__tests__/fixture-clean-logs.test.ts:212-229`・`lib/db/__tests__/fixture-error-reports.test.ts:161-191`。★変わったら実装の誤り★
- 既定値を足すだけでは直らないテストが出たら、直さずに監督に報告する

## 7. 既知の罠

1. ★fixtures 版と Prisma 版で、同じ時刻の行の既定順が違う★（fixtures 版は `scheduled_at` だけで並べ直すため。`lib/db/index.ts:631-633`）。
   新しい並べ替えは「同じ値なら `compareCleanLogItems()`」なので、`sort` を指定したときは両方で同じになる。★既定順（省略時）の食い違いは今回直さない★（ダッシュボード・既存テストに波及するため）。
   だから ★`sort` が null のときは並べ替え関数が何もしない★（§2-1 の表）。fixtures の画面で既定順の同時刻の並びが Prisma の規則と違って見えても、それは今回の不具合ではない
2. 結果待ちは要求ごとの「今」で決まるので、同じ並べ替えでも時間がたつと行が動く（仕様どおり）
3. `grade` は結果待ちでも "×"。★`result_pending` を先に見る★
4. 予定外の行の `id` は null。並べ替えの比較で `id` を使わない（既定順に任せる）
5. `task_status` の 3 と 6 は同じ順位。6 を「キャンセル」と別扱いにしない
6. 古い応答を捨てる仕組み（`cancelled` / `result.query !== queryString`）は、並べ替えを `queryString` に載せれば自動で効く。★別の取得経路を作らない★
7. エラー一覧の 1段目に `comment`（TEXT）が増えるので転送量が少し増える（P1 の件数なら問題ない。D-160 追記2「解消済みも表示の全件読みはリリース前に見直す」の範囲）
8. 清掃詳細ログは読み込み中に表ごと消える作りなので、見出しを押すとフォーカスが外れる（施設マスタも同じ。今回は直さない）

## 8. 画面で確かめること（fixtures）

★確かめ方★: `USE_FIXTURES=true npm run build` → `USE_FIXTURES=true npx next start -p 3100` で開く（★:3000 のサーバーは触らない★）。終わったら :3100 を止める。

- 清掃詳細ログ: 7列それぞれ ▲ → ▼ → 既定（日付は ▲ ↔ 既定。既定のとき日付に ▼）。並べたまま CSV を出し、CSV の行の順が画面と同じ。2ページ目にも並べ替えが効く。
  ★見出しのそろえ（左・中央・右）が今と変わっていない★。「判定」の「?」が押せて、見出しからはみ出さない。
  ダッシュボードの縮小表示の見出しが押せないこと。JP/EN 切り替えで見出しが1行に収まり、列からはみ出さない（`th.scrollWidth <= th.clientWidth`）
- エラー一覧: 6列それぞれ同じ確認。編集中に見出しが押せない。
  プルダウンで「予定外」→ ★予定外の行（fixtures の FX-ERR-04・FX-ERR-12（どちらも NULL）・FX-ERR-20（0）の3行）だけ★になり、件数も変わる。JP/EN で見出しとプルダウンの幅が動かない
- 施設ロボットマスタ: 今までどおり ▼ → ▲ → 既定、見た目も同じ

## 9. 共通ルール

- 期待値表（§3）を先にテストにする。数字・順番を変えない。合わないと思ったら実装を変えずに監督に報告
- `git add -A` 禁止。★コミットはしない（監督がまとめてコミットする）★
- 触らない: `docs/decisions.md`・`docs/handoff/指示書AY_*`・`docs/handoff/設計_制御盤ログの取り出し_*`・`components/features/errors/ErrorContentPicker.tsx`・`lib/errors/error-content-label.ts` とそのテスト・`prisma/`
- DB に書き込まない。サーバー（:3000）を止めない
- 完了したら tsc・lint（触ったファイル）・vitest 全件の件数と、段階ごとの変更ファイルを報告する

## 10. 決定の記録（監督が `docs/decisions.md` に後で書く。Cursor は書かない）

- 並べ替える列（清掃詳細ログ7列・エラー一覧6列）と、施設名・事前チェック・紐づく予定・詳細を外した理由
- 1回目は `asc`（悪いほうが上）。施設マスタは 1回目 `desc` のまま
- `sort` 省略は `null`。2状態の列の desc は `null` に畳む。`sort` が null なら並べ替えない（fixtures の既定順を変えないため）
- 清掃結果の順位表（§2-2）は監督の案（表示の並びだけ。判定には使わない）
- 「紐づく予定」は並べ替えではなく3択の絞り込み（予定の表を引かないので軽い）。先頭の表示は「予定：すべて」（ユーザー回答「後者」）
- 点検（2026-11-04）: 4観点のレビューで38件の指摘、35件を反映（棄却3件: 固定行どうしの順の追加行・読み込み中のフォーカス・行番号の1行ずれ）

---

## 11. 追補 — 実装のレビュー指摘（2026-11-04。Cursor の1回目の実装に対して）

監督の確認結果: tsc・lint・vitest 全件（154ファイル・3778件）は通過。fixtures の画面（:3100）で §8 の確認はほぼ通った（並びの切り替え・▼▲⇅・CSV のリンク・ダッシュボードに出ない・「予定外」で3件・編集中に押せない・施設マスタは今までどおり・JP/EN ではみ出さない）。
4観点のコードレビューで27件の指摘、検証して21件が本物（重複・指示書どおり・範囲外の6件は棄却）。★下の全件を直すこと★。直したら §8 をもう一度通し、tsc・lint・vitest 全件の件数を報告する。

### 11-1. 必ず直す（画面が崩れている）

- **R1（high）`components/ui/SortableTh.module.css`・`SortableTh.tsx`**: `children` があるとき（清掃詳細ログの「判定」だけ）、`.thSortableWithChildren { display: inline-flex }` が ★`<th>` そのもの★ に付いている。
  監督が画面で実測: 「判定」の th が `display: inline-flex` になり、幅 69px（列は 96px）。th が表のセルでなくなり、見出しの下線が列の一部にしか引かれない。
  → th には display を付けない。`children` があるときだけ th の中に `<span className={styles.thInner}>`（`display: inline-flex; align-items: center; gap: 2px; max-width: 100%`）を置き、その中にボタンと `children` を入れる。そろえは th の `text-align` に任せる。
  直したら「判定」の th が `display: table-cell`・幅が列と同じ・中央そろえ・`th.scrollWidth <= th.clientWidth` を確かめる

### 11-2. 直す（仕様・契約・テストの縛り）

- **R2（medium）`lib/clean-logs/sort.ts:18-19`**: 清掃結果の順位の「出発失敗＝1」「表に無い値＝5」が数字の直書き（§2-2 違反）。
  → `lib/config/index.ts` の `CLEAN_TASK_STATUS_SORT_RANK` の隣に `CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED = 1`・`CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN = 5` を置き（または1つのオブジェクトにまとめ）、sort.ts はそれを読む。docstring に 1〜8 の表を全部書く
- **R3（medium）`lib/clean-logs/__tests__/sort.test.ts`**: 順位の 0（清掃開始前＝6）・1（清掃中＝7）・負の値（不明＝5）を縛るテストが無い。
  → 表S1 は変えずに別の `it` を足す。`task_status` = 0, 1, 2, 3, 4, 5, 6, 7, -1, null と結果待ちの行を、わざと既定順と違う順で渡し、
  asc: null → 5 → 2 → {3, 6} → {7, -1} → 0 → 1 → 4 → 結果待ち、desc: 4 → 1 → 0 → {7, -1} → {3, 6} → 2 → 5 → null → 結果待ち（{} の中は既定順のまま。desc でも逆にならない）
- **R4（medium）`lib/error-reports/__tests__/query.test.ts:474` 付近**: 表E1 の入力 `BH_KEYS` が既定順と同じ並びなので「同じ値どうしは既定順」を確かめられていない。
  → 入力を既定順と違う順（例 `[BH_E5, BH_E4, BH_E10, BH_E3, BH_E2, BH_E1]`）にして E1-1〜E1-17 すべてに渡す。★期待値（表E1）は変えない★
- **R5（medium）`app/api/errors/__tests__/route.test.ts`・`lib/db/__tests__/fixture-error-reports.test.ts`**: ルート → データ層まで `sort`・`order`・`schedule` が届くことを確かめるテストが無い（既定値を足しただけ）。
  → route.test.ts に `include_resolved=true&schedule=unscheduled&sort=serial_number&order=desc` のリクエストを1件（データ層に届いた query か items の順と `meta.total`）、`sort=facility_name` の 422 を1件。
  fixture-error-reports.test.ts に `db.errorReports({ ...BASE, include_resolved: true, schedule: "unscheduled" })` → FX-ERR-04・FX-ERR-12・FX-ERR-20 の3件
- **R6（medium）`d-watch-api.yaml` の `/clean-logs` の `sort`（:552 付近）**: 清掃結果の順位表が書かれていない（「悪い順↔良い順」だけ）。
  → `task_status` の行に「出発失敗 → 5 清掃異常終了 → 2 清掃中断 → 3・6 清掃キャンセル（同じ順位）→ 表に無い値（不明(n)）→ 0 清掃開始前 → 1 清掃中 → 4 清掃正常終了。表示の並びだけで判定には使わない」。`grade` の行に「結果待ちは grade が × でも下に固定」
- **R7（medium）`d-watch-api.yaml` の `/clean-logs/export` の 422（:645 付近）**: `sort`/`order` の不正値が書かれていない。
  → 「`sort`/`order` の不正値（details はパラメータ名と受け付ける値の一覧。/clean-logs と同じ形）」を足し、行数超過の `{ total, max }` と形が違うことも並べて書く
- **R8（low）`d-watch-api.yaml` の `/errors` の `schedule`（:1029 付近）**: 「空文字・空白だけは省略と同じ（すべて）」を足す
- **R9（low）`d-watch-api.yaml:497`・`:961` 付近**: 「§下の」は意味が通らない → 「下のパラメータ `sort` / `order` の説明に従う」に直す
- **R10（low）`app/api/clean-logs/__tests__/sort-route.test.ts:30-32`**: `result=all` で8行を全部通しているので「`meta.total` は絞り込み後の件数」を縛れていない。
  → 8行より少なくなる絞り込み（例 `result=problem`）と `sort=grade&order=asc&per_page=2&page=2` のケースを足す
- **R11（low）`lib/clean-logs/__tests__/query.test.ts:662-674`・`lib/error-reports/__tests__/query.test.ts:363` 付近**: 422 の行が `ok === false` しか見ていない。
  → S2-5・S2-6・E2-4・E2-5 に `expect(parsed.status).toBe(422)`。E2-6 で sort/order/schedule それぞれの `details` の形（キーがパラメータ名・中身が受け付ける値の一覧）。`order=`・`order=%20`・`sort=%20` が省略と同じになるケース
- **R12（low）`lib/__tests__/table-sort.test.ts`**: §1-3 の2つの遷移が縛られていない。
  → `nextSortState({key:"grade",order:"desc"}, "scheduled_at", {first:"asc",twoState:true})` → `{scheduled_at, asc}`、
  `nextSortState({key:"monitoring",order:"asc"}, "d_security", {first:"desc",twoState:false})` → `{d_security, desc}` の2件
- **R13（low）`lib/clean-logs/__tests__/sort.test.ts` と `bh-s1-fixture.ts`**: 表S1 の8行が2か所に書かれている。
  → sort.test.ts も `bhS1RowsDefaultOrder()` を import して使う（`const [R4, R1, R2, R5, R3, R8, R6, R7] = bhS1RowsDefaultOrder();`）。期待値は変えない
- **R14（low）`app/api/clean-logs/export/__tests__/route.test.ts:151`**: `it()` の中の `await import()` はこのファイルの作法（冒頭 :3-7）違反 → ファイル冒頭で静的に import する

### 11-3. 直す（書き方・コメント）

- **R15（low）`lib/clean-logs/sort.ts:72-103`・`lib/error-reports/query.ts:110` 付近**:
  (1) completion_rate の分岐の意味の無い `if (aTier !== 0) return null;` を消す。
  (2) 両方 null の行どうしは、主キーの比較の前に明示的に 0 を返す（`null < null` や `null - null` に頼らない）。
  (3) switch の `default` を `{ const _exhaustive: never = sort; return _exhaustive; }` にする（キーを足して case を書き忘れたら tsc が止める）。sort.ts の2つと error-reports の1つ。
  (4) コメントを足す: 結果待ちを先に見る理由（罠3: grade は "×" のまま）、desc は主キーだけ反転し reverse しない（§2-1）、完了率の固定行の段（0/1/2）の意味
- **R16（low）docstring**: `app/api/clean-logs/export/route.ts` の冒頭に「sort/order も受け付け、CSV は画面と同じ並び」。`sortCleanLogs()` の直前に「上限の判定の後（捨てる行を並べ替えない）・入れ忘れると CSV だけ既定順」。
  `parseCleanLogQuery()`・`parseErrorReportQuery()` の docstring に「空文字・空白は省略と同じ／不正値は 422（入力値をエコーしない）／2状態の列の desc は sort null に畳む／sort が null でも order は検証する」。
  `ErrorReportQuery` の3フィールドに JSDoc（「省略時 null（既定順）」「省略時 asc」「null = すべて」）
- **R17（low）`components/ui/SortableTh.module.css`**: 施設マスタから移したときに理由のコメントが落ちた。`.sortButton`・`.sortMark`・`.thSortable` の上に戻す — ★`font: inherit` を外さない（外すと日本語が中華字形に落ちる。CLAUDE.md §0-3）★・印の幅を固定する理由（⇅ と ▼▲ で文字が動かない）・余白 6px の理由。
  `CleanLogTable.module.css:15` 付近と `ErrorsScreen.module.css:252` 付近の列幅の説明に「並べ替える見出しは SortableTh（左右 6px＋間隔 3px＋印 9px）」を1行足す
- **R18（low）押せないボタンの見た目**（監督の判断）:
  `.sortButton:hover .sortMark { opacity: 1 }` を `.sortButton:hover:not(:disabled) .sortMark` にする（押せないのに濃くならないように）。
  ★編集中に並べ替える6列の見出しの文字だけが薄くなり、ほかの3列と濃さがまだらになる★ → ★見出しの文字は薄くしない★。押せないときは印（⇅▲▼）だけを薄くし、カーソルを `default` にする

### 11-4. 棄却した指摘（直さない）

- 並べ替える見出しの余白 6px で、左・右そろえの列の文字が下のセルより 6px 外にずれる → §5 段階4 の指示どおり（承知のうえ）
- 重複4件（th の inline-flex・`await import`・表S1 の2か所・CSS コメント）→ R1・R14・R13・R17 にまとめた
- `d-watch-api.yaml` が js-yaml で読み込めない（`bad indentation`）→ ★今回の変更より前（HEAD `38e8f5d`）からある★。範囲外。別の作業として監督が扱う

---

## 12. 追補2 — 並べ替えの印を白に・清掃詳細ログにも「予定あり／予定外」の絞り込み（2026-11-04）

§11 の修正は監督が確認済み（tsc・lint・vitest 154ファイル・3788件が通過、fixtures の画面で §8 を再確認）。その上に次の2つを足す。

### 12-0. ユーザーの指示（原文）と回答

- 「並び替えのアイコンがわかりにくいので白い△で表示してください」
  - 監督の問い「白い△はどれか」への回答: ★「塗りのまま色だけ白」★ — 並べていない列は薄い △、昇順 ▲・降順 ▼ は塗りのまま白
- 「詳細ログもエラー一覧と同じで予定外・予定ありでできるようにしてください」

### 12-1. 並べ替えの印（`components/ui/SortableTh.*`。3画面共通）

| 状態 | 今 | ★変更後★ |
|---|---|---|
| 並べていない列 | ⇅（`--tx-m`・不透明度 0.5） | ★△（U+25B3。白抜き）・色 `--sort-mark`・不透明度 0.5★ |
| `asc` | ▲（`--tx`） | ★▲・色 `--sort-mark`・不透明度 1★ |
| `desc` | ▼（`--tx`） | ★▼・色 `--sort-mark`・不透明度 1★ |
| マウスを重ねた（押せるときだけ） | 並べていない印が不透明度 1 | 同じ（変えない） |
| 押せない（エラー一覧の編集中） | 印だけ 0.35・見出しの文字は薄くしない・カーソル default | 同じ（変えない。§11 R18） |

- ★色はトークン `--sort-mark` を新しく作る★（`app/globals.css`）:
  - ダーク（`:root, [data-theme="dark"]`）: ★`#ffffff`★（ユーザー指示の「白」）
  - ライト（`[data-theme="light"]`）: ★`#1e293b`（ライトの `--tx` と同じ値）★ — ライトの表の背景は `--sf` = `#ffffff` なので、白にすると印が背景に溶けて消える（コントラスト 1.0:1）。★監督の判断★
  - ★`.sortMark` に `#fff` を直書きしないこと★（ライトで消える）。テーマは `data-theme` 属性だけで切り替わる（`prefers-color-scheme` は使っていない）
- ★印の字のフォントをそろえる★: 今の ▲▼ は Geist Fallback（Arial）で描かれ、△ と ⇅ は Arial に無いので日本語フォント（Windows では Yu Gothic UI）で描かれる（監督が Windows のフォントの cmap で確認）。
  このままだと並べていない △ と並べた ▲▼ で三角の大きさ・太さ・高さがそろわない。
  → ★日本語フォントの並びを `app/globals.css` のトークンにして1か所にまとめる★（CLAUDE.md §0-3「globals.css のフォントスタックを正とする」。並びの写しを CSS モジュールに作らない）:
  - `app/globals.css` に、テーマのブロックとは別に `:root { --font-ja: "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", "Yu Gothic", "Noto Sans JP", Meiryo, sans-serif; }` を置く（フォントはテーマによらないので、ライトのブロックには書かない）
  - body の `font-family` を `var(--font-geist-sans), var(--font-ja);` にする（並びの中身は今と同じなので body の見た目は変わらない）。:55-57 のコメントは残す
  - `SortableTh.module.css` の `.sortMark, .sortMarkActive` に `font-family: var(--font-ja);` を付ける。理由をコメントに書く（▲▼ を Geist Fallback（Arial）ではなく △ と同じ日本語フォントで描かせるため。並びの正は globals.css の `--font-ja`）
  - ★日本語フォントを外さない・中国語フォントを足さない★（CLAUDE.md §0-3）
- ★幅 9px の箱（`width: 9px`・`font-size: 9px`・`text-align: center`）は残す★（印が変わっても見出しの文字が動かない）
- `aria-sort`・`aria-label`・`title` は変えない（印は `aria-hidden`）
- 詳細ログの行を開く印（`CleanLogTable.tsx` の chevron ▼▲。`--tx-m`・10px）は★変えない★
- コメントの ⇅ を直す: `SortableTh.module.css`（2か所）、`lib/table-sort.ts:46`（「▲▼⇅」→「▲▼△」）

承知のうえの点（直さない）:
- 施設マスタは1回目が `desc` なので、△（上向き）を押すと ▼ になる。詳細ログ・エラー一覧は △ → ▲
- 詳細ログの「判定」の見出しの印 △ は、判定バッジの △（同じ字 U+25B3）と同じ形になる（色と大きさで見分ける。ユーザーが選んだ形）

### 12-2. 清掃詳細ログの「予定あり／予定外」の絞り込み

エラー一覧（§4）と同じ3択を、清掃詳細ログの API・CSV・画面に足す。

**意味**（エラー一覧とは中身が違う。YAML にもそう書く）:
- `unscheduled`（予定外）＝ ★`CleanLogItem.unscheduled` が true の行★（予定外タスク。`clean_task_reports.clean_timetable_id` が 0 か NULL。判定はデータ層で `scheduledTimetableId()` 済み）
- `scheduled`（予定あり）＝ ★それ以外の行★（予定の表＝本清掃の行。レポートの無い未実施・結果待ちの行も入る）。★`clean_timetable_id` の値では決めない★ — `clean_timetable_id` が 0 以外なのに予定の表に無いレポートは、今までどおり一覧に出ない（予定ありにも予定外にも入らない。エラー一覧の「予定あり」はこの形も含むので、中身が違う）
- 省略・空文字・空白だけ ＝ すべて

**共通にするもの（★1か所にまとめる★。2画面で規則がずれないように。既知の罠「表示と関門は同じ道具を通す」）**:
- 新規 `lib/api/schedule-filter.ts`（`lib/api/master-id.ts` と同じ置き場所・同じ書き方の純粋関数）:
  - `export const SCHEDULE_FILTERS = ["scheduled", "unscheduled"] as const`（★この順★。details の文言に使う）
  - `export function parseScheduleFilter(searchParams: URLSearchParams): QueryParseResult<ScheduleFilter | null>`
    — `searchParams.get("schedule")?.trim() || null`。配列に無い値は `fail(422, "validation_failed", "scheduleの値が不正です", { schedule: ["scheduled / unscheduled のいずれかを指定してください"] })`。★入力値をエコーしない★。大文字小文字は区別する（`Scheduled` は 422）。
    判定は今の `lib/error-reports/query.ts:187` と同じく `SCHEDULE_FILTERS.includes(raw as ScheduleFilter)`（★キャストを外すと `as const` の配列では TS2345★）。
    呼び出し側の受け方は `parseMasterIdFilters()` と同じ: `const schedule = parseScheduleFilter(searchParams); if (!schedule.ok) return schedule;`、戻り値の query には `schedule: schedule.query`
  - `export function matchesScheduleFilter(isScheduled: boolean, filter: ScheduleFilter | null): boolean`
    — `filter === null` なら true、`"scheduled"` なら `isScheduled`、`"unscheduled"` なら `!isScheduled`。switch に `never` の網羅チェック
  - ★引数は「予定あり＝true」で固定★。呼び出し側: エラー一覧は `key.scheduled`、清掃詳細ログは ★`!r.unscheduled`★（向きが逆。取り違えても型エラーにならないので、両方向をテストで縛る）
- `lib/types/index.ts`: `ErrorScheduleFilter` を ★`ScheduleFilter`★ に名前を変える（使っているのは `lib/error-reports/query.ts`・`lib/types/index.ts`・`ErrorsScreen.tsx` だけ。別名は残さない。★名前を変えたら同じ段階で3ファイルの参照も新しい名前に直す★）。注記を「GET /errors と GET /clean-logs・/clean-logs/export の `schedule`」に直す
- `lib/error-reports/query.ts`: `ERROR_SCHEDULE_FILTERS`（:36）は消し、:186-191 の検証を `parseScheduleFilter()` に（:218 は `schedule: schedule.query`）、:280-281 を `matchesScheduleFilter(key.scheduled, query.schedule)` に置き換える。
  ★検証の順（sort → order → schedule）・message・details の形は変えない★ → エラー一覧の既存テスト（E2-1・E2-2・E2-5・E2-6、`route.test.ts`、`fixture-error-reports.test.ts`）は★1文字も変えずに通ること★
- 新規 `components/ui/ScheduleFilterSelect.tsx`: `<select>` と3つの選択肢・`aria-label`／`title`（「紐づく予定」）を持つ部品。
  props は `value: ScheduleFilter | null`、`onChange: (next: ScheduleFilter | null) => void`、`className: string`（★CSS は持たない★。詳細ログの `.select` は余白 4px 8px、エラー一覧は 6px 10px と違うので、各画面の `styles.select` と固定幅のクラスを渡す）。
  選択肢: 値 `""` ＝「予定：すべて」、`"scheduled"` ＝「予定あり」、`"unscheduled"` ＝「予定外」（★文言は §4 のまま。変えない★）。`t()` の取り方は今の `ErrorsScreen.tsx` に合わせる
  - `ErrorsScreen.tsx` の今の `<select>`（:608-621 付近）をこの部品に置き換える（見た目・位置・`<fieldset>` の中は変えない）。画面ローカルの型 `ScheduleFilterState` は不要になれば消す

**清掃詳細ログ側**:
- `lib/clean-logs/query.ts`:
  - `CleanLogQuery` に `schedule: ScheduleFilter | null`（JSDoc「省略・空白は null＝すべて」）
  - `parseCleanLogQuery()`: ★sort・order の検証の後★に `parseScheduleFilter()`。失敗ならそれを返す。docstring に schedule を足す
  - `filterCleanLogs()`: `if (!matchesScheduleFilter(!r.unscheduled, query.schedule)) return false;`（機種・シリアルの後。result との AND）
  - ★`CleanLogDbQuery`（データ層）には足さない★（`db.cleanLogs()` はダッシュボードも使う。データ層で行を落とすとダッシュボードの数え方に波及する。絞り込みの正は `filterCleanLogs()`）。`lib/types/index.ts` の `CleanLogDbQuery` の「ここに入れないもの」に schedule を書き足す
- `app/api/clean-logs/route.ts`・`export/route.ts`: 本体は変えない（`filterCleanLogs()` を通るので一覧・`meta.total`・CSV・CSV の行数上限の判定のすべてに効く）。冒頭の手順コメントに「予定あり／予定外」を足す
- `components/features/detail-log/DetailLogScreen.tsx`:
  - state `scheduleFilter: ScheduleFilter | null`（既定 null）
  - `queryString` の `useMemo` で `if (scheduleFilter) params.set("schedule", scheduleFilter)`、★依存配列にも入れる★（入れ忘れると選んでも取り直さず、CSV のリンクも変わらない。例外は出ない）
  - ★置く位置: ツールバー左の結果ボタン「全件」の直後★（絞り込みのボタンと並べる。監督の判断。エラー一覧は文字入力の後）。
    ★ツールバーは flex-wrap で、プルダウンを左右どちらに置いても1行の幅は同じだけ（約166px）増える★。ホールディングスのプルダウンが出るとき（fixtures でも出る）は、ビューポートがおよそ 1440px より狭いと右の組（施設名・機種・シリアル・CSV）が2行目の左端に落ちる（1366px では落ちる。変更前は1行）。
    ★承知のうえ。CSS で1行に押し込まない（`.toolbar` の flex-wrap・幅・余白は変えない）★
  - `className={`${styles.select} ${styles.scheduleFilterSelect}`}`、変えたら★同じ操作の中で `setPage(1)`★（`useEffect` で後追いしない）
  - 並べ替えの状態と同じく URL には載せない
- `DetailLogScreen.module.css`: `.scheduleFilterSelect`（エラー一覧の `.scheduleFilterSelect` と同じ: 幅 160px 固定・`overflow: hidden`・`text-overflow: ellipsis`・`white-space: nowrap`）。JP/EN でツールバーが動かないこと（D-162）
- `d-watch-api.yaml`（★先に直す。CLAUDE.md §6-1★）:
  - `/clean-logs` の parameters（`order` の後、`Page` の前）と `/clean-logs/export` の parameters（`order` の後）に `schedule`（`enum: [scheduled, unscheduled]`、`default` は書かない）。説明: 上の「意味」・省略と空文字・空白だけはすべて・未知の値は 422
  - 両方の 422 の説明に「`schedule` の不正値（details はパラメータ名と受け付ける値の一覧。sort/order と同じ形）」
  - `/errors` の `schedule` の説明に「清掃詳細ログの `schedule` とは予定ありの中身が違う」と1行
- `lib/i18n.ts`: ★足すキーは無い★（「予定：すべて」「予定あり」「予定外」「紐づく予定」は既にある）

### 12-3. 期待値表（★監督が確定。テストはこれをそのまま使う★）

**表P — `parseScheduleFilter()`（新規 `lib/api/__tests__/schedule-filter.test.ts`）と `parseCleanLogQuery()`（`lib/clean-logs/__tests__/query.test.ts`）**

| # | クエリ | 結果 |
|---|---|---|
| P-1 | なし | `null` |
| P-2 | `schedule=` | `null` |
| P-3 | `schedule=%20` | `null` |
| P-4 | `schedule=scheduled` | `"scheduled"` |
| P-5 | `schedule=unscheduled` | `"unscheduled"` |
| P-6 | `schedule=%20unscheduled%20` | `"unscheduled"`（前後の空白を落とす。エラー一覧と同じ） |
| P-7 | `schedule=all`・`schedule=main`・`schedule=Scheduled`・`schedule=UNSCHEDULED` | 422。message「scheduleの値が不正です」、details ★`{ schedule: ["scheduled / unscheduled のいずれかを指定してください"] }`（`toEqual` で丸ごと）★。本文に入力値が出ない |
| P-8 | `sort=bad&schedule=bad`（`parseCleanLogQuery()`） | 422 で details のキーは ★`sort` だけ★（先に sort を見る） |

**表M — `matchesScheduleFilter()`（同じテストファイル）**

| # | isScheduled | filter | 結果 |
|---|---|---|---|
| M-1 | true | null | true |
| M-2 | false | null | true |
| M-3 | true | `"scheduled"` | true |
| M-4 | false | `"scheduled"` | false |
| M-5 | true | `"unscheduled"` | false |
| M-6 | false | `"unscheduled"` | true |

**表F — GET /clean-logs（`app/api/clean-logs/__tests__/sort-route.test.ts` に足す。行は `bhS1RowsDefaultOrder()` の8行。予定外は `rp-9001`（×）の1行だけ）**

| # | クエリ | `meta.total` | items の `row_key`（この順） |
|---|---|---|---|
| F-1 | `result=all&schedule=unscheduled` | 1 | `rp-9001` |
| F-2 | `result=all&schedule=scheduled` | 7 | `rp-9001` を除いた7行。順は `result=all`（schedule なし）の結果から `rp-9001` を抜いたものと同じ |
| F-3 | `result=problem&schedule=scheduled` | 2 | `tt-102`・`tt-103`（`result=problem` の結果から `rp-9001` を抜いた順） |
| F-4 | `result=problem&schedule=unscheduled` | 1 | `rp-9001` |
| F-5 | `result=critical&schedule=unscheduled` | 1 | `rp-9001` |
| F-6 | `result=all&schedule=scheduled&sort=grade&order=asc&per_page=2&page=1` | 7 | `result=all&sort=grade&order=asc` の並びから `rp-9001` を抜いた先頭2行 |
| F-7 | `schedule=all` | — | 422（details のキーは `schedule`） |
| F-8 | `result=all&schedule=` | 8 | schedule なしと同じ（空文字は「すべて」。ルートを通しても 422 にならない） |

- F-2・F-3・F-6 の「抜いた順」は、テストの中で schedule なしの応答から `rp-9001` を `filter` して作ってよい（数字を手で書き写さない）。★件数（7・2）は直書きで縛る★

**表X — GET /clean-logs/export（`app/api/clean-logs/export/__tests__/route.test.ts` に足す。同じ8行）**

| # | クエリ | CSV のデータ行 |
|---|---|---|
| X-1 | `result=all&schedule=unscheduled` | 1行。予定時刻の列が「予定外」 |
| X-2 | `result=all&schedule=scheduled` | 7行。予定時刻の列に「予定外」が★1つも無い★ |
| X-3 | `schedule=bad` | 422（JSON。details のキーは `schedule`） |

**エラー一覧（回帰）**: 既存のエラー一覧のテストは★変えずに★全部通ること。`matchesScheduleFilter(key.scheduled, …)` の向きは既存の `fixture-error-reports.test.ts`（FX-ERR-04・12・20 の3件）が縛っている

### 12-4. 既存テストの扱い

- `lib/db/__tests__/fixture-clean-logs.test.ts:20-37` の `memoryQuery()` は `CleanLogQuery` の全フィールドを書いているので、`schedule: null` を足す（★入れ忘れると tsc と next build で落ちるうえ、vitest も落ちる★ — schedule が undefined だと `matchesScheduleFilter()` は `filter === null` を通らずに never の分岐へ入り、undefined（偽）を返す。そのため `filterCleanLogs()` が全行を落とし、#2（:52-69）の `expect(sql).toEqual(memory)` が赤になる。★段階4では query.ts の変更と同じ手順の中で足し、そのあとで vitest を回す★）
- それ以外の既存テストの期待値は変えない（schedule 省略＝すべてなので件数・並びは変わらない）。変えたくなったら実装を変えずに監督に報告

### 12-5. 既知の罠（§7 に追加）

9. ★向きが逆★: エラー一覧は `key.scheduled`（true＝予定あり）、清掃詳細ログは `r.unscheduled`（true＝予定外）。`matchesScheduleFilter()` には★必ず「予定あり＝true」を渡す★
10. `schedule` を result と同じ書き方（`?? "all"` で受ける）にすると `schedule=` が 422 になり、エラー一覧と規則が分かれる。`parseScheduleFilter()` だけを使う。清掃詳細ログ側は `query.test.ts` の表P（P-2・P-3・P-6）と表F の F-8 が縛る
11. fixtures の清掃詳細ログの予定外は2行だけ（`rp-9200000000000000001`＝施設1・◎、`rp-9200000000000000002`＝施設11・×・清掃キャンセル）。★画面の既定（△×のみ）で「予定外」を選ぶと1件★、「全件」で2件。1件しか出ないのは不具合ではない。★既定（△×のみ）は1ページしかないので、ページを戻す確認は「全件」で行う（12-6）★
12. ステージングでは清掃詳細ログ 636件のうち 596件が予定外、予定ありは 40件（2026-11-04 監督が API で数えた。先方投入のデータ。時間で変わる）。予定と実績がまだつながっていないため。「予定あり」が少ないのは不具合ではない
13. △ を描く日本語フォントが無い環境では sans-serif に落ちる。印も body も同じ `--font-ja`（日本語フォントを先に）を使うので、中国語フォントには落ちにくい。★`--font-ja` の並びを短くしない・CSS モジュールに写しを作らない★

### 12-6. 画面で確かめること（fixtures。§8 と同じやり方で :3100）

- 3画面: 並べていない列の印が★薄い白の △★、押すと★白の ▲／▼★（施設マスタは △ → ▼ → ▲ → △）。△ と ▲▼ の大きさ・高さがそろって見える。見出しの文字は動かない（印を変えても `th` の幅が変わらない）
- ライトテーマ（表示設定で切り替え）: 印が★濃い紺★で見える（白で消えていない）
- エラー一覧の編集中: 印だけ薄くなり、見出しの文字は他の列と同じ濃さ（§11 R18 のまま）
- 清掃詳細ログ: 「全件」の右にプルダウン。既定（△×のみ）で「予定外」→ 1件（施設11の×）。「全件」にすると2件。「予定あり」→ 予定外の行（予定の欄が「予定外」）が消える。
  ★ページの戻しは「全件」で確かめる★（既定の△×のみは48件で1ページしかない）: 「全件」（fixtures で283件・6ページ）で2ページ目を開き、プルダウンを「予定あり」（281件）に切り替えると1ページ目に戻ること。
  ★「予定外」への切り替えで確かめないこと★ — 2件で1ページに収まるので、`setPage(1)` を入れ忘れていても `DetailLogScreen.tsx:237-238` の保険（応答の page が総ページ数を超えたら最終ページへ寄せる）で1ページ目に戻って見える（件数は実行日で少し変わるが、全件と予定ありが2ページ以上・予定外が1ページという関係は変わらない）。
  CSV のリンクに `schedule=` が付き、CSV の行も同じに絞られる。
  ★ビューポート 1366px と 1536px の2つの幅で見る★: 1536px ではツールバーが1行。1366px では右の組が2行目に落ちるが不具合ではない（12-2）。
  どちらの幅でも、プルダウン自身の幅（160px）が JP/EN や選んだ値で変わらないこと（左端の位置は、前の結果ボタンの文言の幅だけ JP/EN で数 px 動く。結果ボタンは今も JP/EN で幅が変わるので承知のうえ）
- フォント: `git grep -n "Hiragino" -- app components lib` のヒットが `app/globals.css` の `--font-ja` の1行だけであること。ダーク・ライトとも △ と ▲▼ の大きさ・高さがそろうこと
- エラー一覧のプルダウンは見た目も動き（「予定外」で3件）も今までどおり

### 12-7. 段階（この順で。§9 の共通ルールはそのまま）

1. `d-watch-api.yaml`（12-2 の YAML）
2. `lib/api/schedule-filter.ts`＋テスト（表P・表M）、`lib/types/index.ts`（名前の変更と注記）
3. `lib/error-reports/query.ts` を共通関数に置き換え → ★エラー一覧の既存テストが変えずに通ることを先に確かめる★
4. `lib/clean-logs/query.ts`、ルートのコメント、`fixture-clean-logs.test.ts` の `schedule: null`、表P（P-1〜P-8）・表F・表X のテスト。
   ★表P の P-1〜P-7 は `lib/clean-logs/__tests__/query.test.ts` で `parseCleanLogQuery()` にも `it.each` で当てる★（ほかのパラメータは付けない）。ok のケース（P-1〜P-6）は `parsed.query.schedule` を見る。422 のケース（P-7）は status 422・message「scheduleの値が不正です」・details を `toEqual` で丸ごと見る
5. `components/ui/ScheduleFilterSelect.tsx`、`ErrorsScreen.tsx` の置き換え、`DetailLogScreen.tsx`・`.module.css`
6. `app/globals.css`（`--sort-mark`・`--font-ja` と body の `font-family`）、`SortableTh.tsx`・`.module.css`（印・色・フォント・コメント）、`lib/table-sort.ts:46` のコメント
7. tsc・lint・vitest 全件の件数と、段階ごとの変更ファイルを報告（★報告は日本語★）

### 12-8. 決定の記録（監督が `docs/decisions.md` に後で書く）

- 並べ替えの印: 並べていない列＝薄い △、asc ▲・desc ▼ は塗りのまま。色はダーク＝白、ライト＝`#1e293b`（白は背景に溶けるため。監督の判断）。印の字は日本語フォントでそろえる
- 清掃詳細ログにも `schedule`（エラー一覧と同じ3択・同じ文言）。予定ありの中身は2画面で違う（エラー一覧＝`clean_timetable_id` が 0・NULL 以外、清掃詳細ログ＝予定の表の行）
- 検証と判定は `lib/api/schedule-filter.ts` の1か所、プルダウンは `components/ui/ScheduleFilterSelect.tsx` の1か所（CSS は各画面）
- 置き場所: 清掃詳細ログは「全件」の右（エラー一覧は「解消済みも表示」の左のまま）。1366px 前後の幅では詳細ログのツールバーが2行になる（承知のうえ）
- 日本語フォントの並びを `app/globals.css` の `--font-ja` にまとめ、body と並べ替えの印の両方がそれを使う（並びの写しを作らない）
- 点検（2026-11-04）: 4観点で12件の指摘。反証で確かめて6件が本物（12-4 の理由・ツールバーの折り返し・表P を parseCleanLogQuery にも当てる・フォントの並びの写し・ページの戻しの確かめ方）。棄却6件のうち、書き方の補足になる5件（includes のキャスト・:218 の受け方・名前変更と参照・予定ありの中身・JP/EN の数 px）は本文に一言ずつ足した。棄却: 段階ごとに tsc を通す決まりの追加（§9 は最後だけ求める）
- 却下: 白抜き △▽ にする案・並べた列だけ印を出す案（ユーザーが「塗りのまま色だけ白」を選んだ）。`schedule` をデータ層（SQL）に降ろす案（ダッシュボードに波及するため）

---

## 13. 追補3 — 表示の文言を「予定なし」に統一（2026-11-04。★Cursor ではなく監督側で直した★）

§12 の実装は監督が確認済み（tsc・lint・vitest 155ファイル・3826件、fixtures の画面）。そのあとのユーザー指示:
「文言を予定なし、予定ありに統一して／プルダウンだけじゃなくて、表の表示も、cursorでなくこちらで直して」

- ★画面・CSV に出る「予定外」を「予定なし」に変えた★: プルダウンの選択肢（`components/ui/ScheduleFilterSelect.tsx`）、清掃詳細ログ・ダッシュボードの小さい表・CSV の「予定」列（`CLEAN_LOG_UNSCHEDULED_LABEL`。`lib/format/clean-result.ts`）、清掃面積の注記（`CleanAreaScreen.tsx`）。英訳は既存の「予定なし」"No schedule"
- エラー一覧の「紐づく予定」列はもともと「予定なし」（予定のある行は「本清掃」「事前チェック（N分前）」「予定不明」と中身を出す。変えない）
- ★変えないもの★: API のパラメータ値（`schedule=scheduled / unscheduled`）・型名・関数名・`CleanLogItem.unscheduled`・コメントや仕様書の概念語「予定外タスク」（契約と内部の用語。画面に出る文字だけを変えた）
- §4・§12 の本文の「予定外」（選択肢の文言）は、この §13 で「予定なし」に読み替える
