# 指示書AA — 一覧6画面にページ送りを付ける（201件目以降が画面から見えない問題を直す）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★★**完了（2026-09-11。D-120）**★★ — ★詳細は文末の「実行状態」表★ |
| 実行順 | ★★**指示書Y → 指示書W → 指示書Z の後に実行すること**★★（詳細は下の「実行順」節）。★★特に**指示書Z とは同時に走らせないこと**★★（`components/features/detail-log/` が真正面から衝突します） |
| 対象 | ★★一覧6画面が `per_page=200`（`PAGINATION.maxPerPage`）で全件を要求して全部並べているだけで、★**ページ送りUIが1つも無い**★。★★201件目以降は画面から一生見えません★★★。★本番想定は約360行/日（`docs/spec.md:45`）なので、★**当日1日を「全件」で見ただけで切れます**★★ |
| ★着手前確認★ | ★★**なし**★★ — ★「ページ送りを作る」「2ページ目以降ができたらその数だけ表示選択できるようにする」「3ページ目を押したら有りません、みたいなのはだめです」はユーザー回答（2026-09-11）で**確定済み**★。★★段階0 から通しで実行してよい★★ |
| 取得する決定番号 | ★★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★。★2026-09-07 に `D-068` の二重採番が実際に起きています★。★★**見込み番号はここに書きません** — 指示書W と指示書Z がそれぞれ番号を取る見込みなので、必ず自分で実測してから採番すること★★ |
| 前提 | **D-050**（段階ごとに検証してからコミット）/ **D-053**（グループ・会社の絞り込みは未実装）/ **D-054**（`summary` は対応状況フィルタの**前**に計算する。★本書の禁止事項8 の根拠★）/ **D-092 項3-3**（`db.cleanLogs()` の全件取得がボトルネックになりうる。★本書の「性能について」節★）/ `CLAUDE.md` §5（モックが画面の正）§6-2（期待値表→テスト→実装）§7（閾値のハードコード禁止 / `git add -A` 禁止）§9.1〜§9.6 |
| 段階数 | ★**8段階**★（段階0〜段階7）。★コミットは段階1 / 2 / 3 / 4 / 5 / 7 の**6つ**★ |
| DB接続 | ★★**しません**★★。★本書は fixtures（`USE_FIXTURES=true`）だけで完結します★ |
| サーバ変更 | ★★**ありません**★★。★API 6本は `page` / `per_page` を受け取り `meta: { total, page, per_page }` を返す実装が**既に入っています**★ |

---

## ★★冒頭に読むべきこと — 何が起きているか★★

### 1. ★一覧6画面すべてが同じ作りになっている（★監督が実測済み。着手時に自分でも確認すること★）★

| 画面 | ファイル | 現在の件数表示 | レスポンス型 |
|---|---|---|---|
| 清掃詳細ログ F-02 | `components/features/detail-log/DetailLogScreen.tsx` | ★有り★ `{total}件（表示 N件）` | `ListResponse<CleanLogItem>` |
| アラート（事前チェック）F-03 | `components/features/alerts/AlertsScreen.tsx` | ★**無し**★ | `SummaryListResponse<PrecheckItem, PrecheckSummary>` |
| D-Secログ F-06 | `components/features/ds-logs/DsLogScreen.tsx` | ★**無し**★ | `SummaryListResponse<DsDevice, DsDeviceSummary>` |
| D-Secアラート F-07 | `components/features/ds-alerts/DsAlertsScreen.tsx` | ★**無し**★ | `SummaryListResponse<DsAlert, DsAlertSummary>` |
| 施設ロボットマスタ F-08 | `components/features/facilities/FacilitiesScreen.tsx` | ★有り★ `全{n}施設` | `ListResponse<Facility>` |
| スケジュール管理 F-09 | `components/features/schedules/SchedulesScreen.tsx` | ★**無し**★ | `ListResponse<Schedule>` |

★★6画面とも `params.set("per_page", String(PAGINATION.maxPerPage));` で **常に200件を要求し、返ってきた分を全部並べているだけ**★★です。

★★着手時に自分で実測すること★★（★行番号は変わる前提。grep で取ること★）:

```bash
grep -rn 'params.set("per_page"' components/features/
grep -rn 'body.meta' components/features/
```

### 2. ★★何が壊れているのか★★

- ★★`per_page` の上限は 200（`PAGINATION.maxPerPage`）★★。★★**201件目以降は画面に出す手段が存在しない**★★
- ★本番想定は **約360行/日**（`docs/spec.md:45`「データ量: 約360行/日、年間約13万行、保持2年」）★ →
  ★★**清掃詳細ログで当日1日を「全件」にしただけで切れます**★★
- ★日付範囲の上限は31日（`MAX_DATE_RANGE_DAYS`）★ →
  ★★**指定できる範囲（31日 ≒ 11,000行）のほうが、表示できる範囲（200行）より広い**★★。
  ★ユーザーは「31日ぶん見られる」と思って操作しますが、実際には先頭200件しか見ていません★
- ★★しかも**切れたことが画面のどこにも出ません**★★。★清掃詳細ログだけは `{total}件（表示 N件）` で気づけますが、
  残り5画面は総件数すら出していないので★★黙って消えます★★

### 3. ★★モックには前例が無い（★UIを新規に作ることをユーザーが承認済み★）★★

- ★`d-watch-mock.jsx` にページ送りのUIは**1つもありません**★
- ★前例と言えるのはレポート画面（F-04）の「他{n}件を表示 →」「全{n}施設を表示 →」だけ★
  （`lib/i18n.ts` に対訳あり。★あれは `limit` を増やす方式で、ページ送りではありません★）
- ★★`CLAUDE.md` §5 は「モック（`d-watch-mock.jsx`）が画面レイアウト・文言・遷移の正」と定めています★★ →
  ★★**本書はその例外です**★★。★★ユーザーの明示の承認（2026-09-11「ページ送りは作成してください」）に基づき、
  モックに無いUIを新規に作ります★★。★★この経緯を `docs/decisions.md` に必ず残すこと（段階7）★★

### 4. ★★ユーザーが確定させた要件（★2026-09-11。原文★）★★

> ★ページ送りは作成してください★
> ★2ページ目以降ができたらその数だけ表示選択できるようにしてください★
> ★3ページ目を押したら有りません、みたいなのはだめです★

★★＝ **実際に存在するページだけを選択肢に出す。存在しないページ番号を1つも描画しない。**★★
★★これが本書の合否基準です★★（段階1 の不変条件テストで機械的に固定します）。

---

## ★★採る方針（★理由ごと理解してから実装すること★）★★

| # | 決めたこと | 理由 |
|---|---|---|
| ★1★ | ★★**ページ番号方式**（1 2 3 … の番号ボタン）★★ | ★ユーザー要件「2ページ目以降ができたらその数だけ表示選択できるように」＝**何ページあるか見えて、直接飛べる**こと★。★「もっと見る」方式ではページ数が見えないので要件を満たさない★ |
| ★2★ | ★★**共通部品を1つ作り、6画面すべてに入れる**★★ | ★★API側が6本とも `page` / `per_page` / `meta` に対応済み★★。★2画面だけに入れると作りが不揃いになり、次に触る人がどちらを真似ればいいか分からなくなる★ |
| ★3★ | ★★**1ページあたりの件数は `PAGINATION.defaultPerPage`（現在 50）から読む**★★ | `CLAUDE.md` §7（★閾値のハードコード禁止★）。★★`50` を画面に直書きしないこと★★ |
| ★4★ | ★★**総ページ数 = `Math.ceil(meta.total / per_page)`**★★。★総ページ数が1以下ならボタンを出さない★ | ★1ページしかないのにボタンを出すと、押せないUIが並ぶだけ★ |
| ★5★ | ★★**ページ数が多いときは先頭・末尾・現在の前後を出し、間を「…」で省略する**★★ | ★223ページ分のボタンは並べられない（年間13万行 ÷ 50 = 2,600ページ）★ |
| ★6★ | ★★**「…」は `button` にしない**★★。★★**描画する数字は必ず実在するページ**★★ | ★★ユーザー要件「3ページ目を押したら有りません、みたいなのはだめです」★★ |
| ★7★ | ★★**件数表示を6画面すべてに出す。形は「全{total}件中 {from}〜{to}件」に統一**★★ | ★現状バラバラ（清掃詳細ログ `{total}件（表示 N件）` / 施設 `全{n}施設` / 残り4画面は無し）★。★モックの「{n}件 / 全{n}件」とも食い違っている★ → ★★1つに揃える★★ |
| ★8★ | ★★**`meta.total` を使う。`summary.total` を絶対に使わない**★★ | ★アラート / D-Secログ / D-Secアラートの3本は `summary` を**フィルタ前**に計算している（D-054）★。★★`app/api/prechecks/route.ts` のコメントが「meta.total と summary.total は意味が違う」と明記しています★★ |
| ★9★ | ★★**CSV は全件のまま。画面に注記を出す**★★ | ★`app/api/clean-logs/export/route.ts` の冒頭コメント「page / per_page は無視する」が契約★。★★変えないこと★★ |
| ★10★ | ★★**ページ番号を URL に持たせない**（React の state だけで持つ）★★ | ★既存6画面はフィルタも検索もURLに持っていない（全部 `useState`）★。★URL同期を入れると `useSearchParams` の Suspense 境界が要り、6画面の構造を変えることになる★ → ★★本書の範囲を超える。申し送り①に回す★★ |

### ★★却下した案（★同じ案を再提案しないために、理由ごと残します★）★★

| 案 | 却下理由 |
|---|---|
| ★`PAGINATION.maxPerPage` を 200 から 10000 に上げるだけ★ | ★★**問題を先送りするだけ**★★。★年間13万行では結局切れるし、1万行を1画面に描くとブラウザが固まる★ |
| ★無限スクロール★ | ★★ユーザー要件「その数だけ表示選択できるように」を満たさない★★（何ページあるか見えない・特定のページに戻れない）。★翌朝確認型の運用では「昨日見た位置に戻る」が要る★ |
| ★「もっと見る」で追記していく方式（レポート画面と同じ）★ | ★同上。★★総ページ数が見えない★★★。★レポート画面のあれは「上位10件＋全部」の2状態であって、ページ送りではない★ |
| ★サーバ側にカーソルページングを足す★ | ★★`d-watch-api.yaml` の変更が要る★★。★★`page` / `per_page` / `meta` で既に足りている★★ので、契約を変える理由が無い |
| ★ページ送りと同時に SQL 絞り込みも入れる★ | ★★下の「性能について」節を読むこと。**同時にやると壊れます**★★ |

---

## ★★性能について — 必ず読むこと★★

★★ページ送りを付けても、DB からは**毎リクエスト全件読みます**★★。

- ★`DataSource` は引数を取らない契約になっており、`prismaSource.cleanLogs()` は**全期間・全件の `findMany()`**★
- ★コード自身が警告を書いています★（★着手時に実測すること★）:

  ```bash
  grep -rn "13万" lib/db/
  # → lib/db/index.ts / lib/db/clean-log-mapping.ts の2箇所に
  #   「年間13万行（docs/spec.md:45）の実データではここがボトルネックになりうる」
  ```

- ★絞り込み・並べ替え・ページ切り出しはすべて**API ルートの中で JavaScript の配列操作**として走っています★
  （`filterCleanLogs()` → `sorted.slice(start, start + perPage)`）

★★つまり本書で解決するのは「見えない行が見えるようになる」ことだけで、★性能は1ミリも改善しません★★★。
★★むしろページを繰るたびに全件取得が走るので、体感は悪化する方向です★★。

★★これは別課題（SQL に絞り込みを降ろす改修）として切り離します★★。

> ★★**ページ送りと性能改善を同時にやらないこと**★★ —
> ★連続日数の計算（`consecutive_problem_days`）が**全期間を走査**して数えているため、
> 「期間で絞って取る」改修を入れると★★連続日数が同時に壊れます★★★。
> ★2つが同じコミットに入ると、どちらが壊したのか切り分けられなくなります★。
> ★本書は `lib/db/` に1行も触りません★（禁止事項5）。

---

## ★調査済みの事実（★現物で裏を取ってから使うこと。行番号は必ず実測すること★）★

### 1. ★設定値★

```ts
// lib/config/index.ts（★着手時に grep -n "PAGINATION" lib/config/index.ts で位置を取ること★）
export const PAGINATION = {
  defaultPerPage: 50,
  maxPerPage: 200,
} as const;
```

★★本書で `maxVisiblePages` を1つ足します（段階1）★★。★それ以外は変えません★。

### 2. ★API 6本はすべて対応済み★

| API | ページ切り出しの場所 | `meta` |
|---|---|---|
| `/api/clean-logs` | `filtered.slice(start, start + perPage)` | 有り |
| `/api/prechecks` | `sorted.slice(...)` | 有り（★`summary` と併記。コメントで違いを明記済み★） |
| `/api/ds-devices` | `sorted.slice(...)` | 有り（同上） |
| `/api/ds-alerts` | `sorted.slice(...)` | 有り（同上） |
| `/api/facilities` | `paginateFacilities()`（`lib/facilities/query.ts`） | 有り |
| `/api/schedules` | `sorted.slice(...)` | 有り |

★★6本とも `parse*Query()` が★`page = Math.max(1, toInt(searchParams.get("page"), 1))`★
★`perPage = Math.min(PAGINATION.maxPerPage, Math.max(1, toInt(..., PAGINATION.defaultPerPage)))`★
という**同一の形**で実装済みです★★。★★サーバは1行も変えません★★。

★確認コマンド★:

```bash
grep -rn 'searchParams.get("page")' lib/
grep -rn "meta: { total, page, per_page: perPage }" app/api/
```

### 3. ★★`meta.total` と `summary.total` は意味が違う（★踏むと総ページ数がずれます★）★★

`app/api/prechecks/route.ts` の既存コメント（★原文。着手時に実測すること★）:

> ★meta.total   = 一覧（対応状況フィルタ後）の総件数。ページング計算用★
> ★summary.total = 日付範囲で絞った当日の全件（対応状況フィルタの影響を受けない）★
> ★status=all のときだけ一致する。status=open 等で絞ると meta.total < summary.total になりうる★

★★`/api/ds-devices` / `/api/ds-alerts` にも同趣旨のコメントがあります★★（D-054）。

★★アラート / D-Secログ / D-Secアラートの3画面は、現在 `body.summary` しか読んでおらず `body.meta` を**捨てています**★★ →
★★段階4 で `meta.total` を持つ state を新設すること★★。★`summary.total` を流用しないこと★。

### 4. ★i18n の作法（★方式A: プレースホルダ + replace★）★

`lib/i18n.ts` に既存の解説コメントがあります（★原文。着手時に実測すること★）:

> ★方式A（プレースホルダ + replace）★推奨★★
> ★`t("他{n}件を表示 →").replace("{n}", String(n))`★
> ★語順が言語ごとに違っても対訳側で吸収できるため、こちらを推す★

★★本書も方式A を使います★★。★★方式B（前後を分割して連結）は使わないこと★★。

★既存の前例★: `"他{n}件を表示 →"` / `"全{n}施設を表示 →"` / `"全{n}施設"` / `"{n}分前"` / `"{n}日以上連続"` / `"第{n}回チェック"`

★★注意: 本書のキーは**プレースホルダが3つ**（`{total}` / `{from}` / `{to}`）で、既存の `{n}` 1つより多い★★。
★`.replace()` を3回つなげること★。★★`replaceAll` は不要（各プレースホルダは1回しか出ない）★★。

### 5. ★★テストの実行環境（★ここを間違えるとテストが1つも走りません★）★★

```ts
// vitest.config.ts
test: {
  environment: "node",
  include: ["lib/**/*.test.ts", "app/**/*.test.ts"],
}
```

★★`components/**` は include に入っておらず、`environment` は `node`（jsdom ではない）で、
React Testing Library も入っていません★★ →

- ★★**ページ番号の並びを組み立てるロジックは `lib/pagination/` の純粋関数に置くこと**★★。
  ★そこならテストが走ります★
- ★★**コンポーネントのテストを書こうとして jsdom / @testing-library を `package.json` に足さないこと**★★（禁止事項10）。
  ★依存追加は本書の範囲外です★

### 6. ★空表示は6画面とも既にある（★0件のときページ送りを出さない根拠★）★

| 画面 | 空表示 |
|---|---|
| 清掃詳細ログ | `CleanLogTable` の中に「該当するログはありません」 |
| アラート | 「該当するアラートはありません」 |
| D-Secログ | 「該当するデバイスはありません」 |
| D-Secアラート | 「該当するアラートはありません」 |
| 施設 | 「該当する施設はありません」 |
| スケジュール | 「該当するスケジュールはありません」 |

★★だから `total === 0` のときは部品ごと出さない（件数表示もページ送りも出さない）★★。
★「全0件中 0〜0件」と空表示が二重に出るのを避けます★。

### 7. ★フィルタを変えたらページを1に戻す（★前例あり★）★

`components/features/reports/ReportsScreen.tsx` は★期間変更・検索実行のたびに `setPerfShowAll(false)` を呼んでいます★
（★3箇所。着手時に `grep -n "setPerfShowAll(false)" components/features/reports/ReportsScreen.tsx` で実測すること★）。

★★同じ発想です★★ — ★★フィルタを変えたのにページ3に留まっていると、件数が減ったときに
**存在しないページに居座って空表示になります**★★。★これはユーザーが名指しで禁じた状態です★。

★★清掃詳細ログ / D-Secログ / 施設 / スケジュールの検索欄は debounce（300ms）があります★★ →
★★**debounce の反映時にもページを戻すこと**★★（段階3 の 3-4 に具体的な書き方を示します）。

### 8. ★落とし穴（★踏むとテストが通ったまま壊れます★）★

| # | 落とし穴 |
|---|---|
| ★1★ | ★★`Math.ceil(0 / 50)` は `0`、`Math.ceil(total / 0)` は `Infinity`★★。★★`perPage` が0以下のときは総ページ数を0にすること★★（防御。純粋関数のテストに入れる） |
| ★2★ | ★★`total === 0` のとき `from` を `1` にしないこと★★。★`(1-1)*50+1 = 1` になるので、素直に書くと「全0件中 1〜0件」が出ます★ |
| ★3★ | ★★`to` は `Math.min(page * perPage, total)`★★。★`items.length` を使わないこと★（★ローディング中や失敗時に `items` が空になり、件数だけ0に見える★） |
| ★4★ | ★★「…」が**1ページだけ**を隠す形にしないこと★★（`1 … 3 4 5` の「…」が 2 だけを隠している状態）。★数字1つぶんの幅に「…」を出すのは、押せる番号が減るだけで損★。★段階1 の不変条件テストで機械的に禁じます★ |
| ★5★ | ★★フィルタ変更時に `setPage(1)` を `useEffect` で後追いすると、**古いページのままフェッチが1回余計に飛びます**★★。★★state を変えるその場で一緒に戻すこと★★（3-4 参照） |
| ★6★ | ★★`page` を `queryString` の `useMemo` の依存に入れ忘れると、ボタンを押しても取り直しません★★（★`useCallback` の依存は `queryString`（★`DetailLogScreen.tsx` だけは `[queryString, rangeDays]` の2本。`rangeDays` は日付範囲の検証に要るので消さないこと★）なので、`queryString` が変わらないと `fetch*` も変わらない★） |
| ★7★ | ★★清掃詳細ログの CSV リンクは `queryString` をそのまま `href` に入れています★★。★`page` が混ざりますが、`export` は `page` / `per_page` を無視する契約なので**動作は変わりません**★。★★リンクを分岐させないこと（余計な差分になります）★★ |
| ★8★ | ★★`d-watch-api.yaml` を見て「`page` が無い」と思わないこと★★ — ★着手時に `grep -n "per_page\|page" d-watch-api.yaml \| head -30` で実測すること★。★★もし本当に無ければ、それは**別の不整合**なので、本書を止めて確認を求めること（推測で YAML を書き換えないこと）★★ |
| ★9★ | ★★`components/ui/` の既存部品（`Pill` / `FilterButton` / `GradeBadge` / `StatCard` / `ProgressBar`）は**どれも `useTheme()` を使っていません**★★。★色は CSS 変数（`var(--ac)` / `var(--bd)` / `var(--tx-m)`）か props の `color`★。★★本書の部品は文言のために `useTheme()` を使います（理由は 2-1 に明記）★★ |

---

## ★禁止事項（この指示書の全段階に効きます）★

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★**`git add -A` を使うこと**★★ | `CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★★必ずファイルパスを明示すること★★ |
| ★2★ | ★★**`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / 他の指示書 / `.claude/worktrees/**` に触ること**★★ | ★★すべて別作業のもの★★。★★読むのは自由。編集・削除・コミットへの同梱は禁止★★ |
| ★3★ | ★★**件数・ページ数を直書きすること**（`50` / `200` / `7` / `Math.ceil(total / 50)` 等）★★ | `CLAUDE.md` §7。★★`lib/config/index.ts` の `PAGINATION` から読むこと★★ |
| ★4★ | ★★**`summary.total` を総ページ数の計算に使うこと**★★ | D-054。★`summary` はフィルタ前の集計で、一覧の件数とは**別物**★。★★総ページ数がずれます★★ |
| ★5★ | ★★**存在しないページ番号を描画すること**★★ | ★★ユーザーが名指しで禁じた状態★★。★段階1 の不変条件テストで固定します★ |
| ★6★ | ★★**`lib/fixtures/` を1行でも変更すること**★★ | `CLAUDE.md` §9.5。★★「2ページ目が出ないから件数を増やす」は禁止★★（検証方法は段階6 に書いてあります） |
| ★7★ | ★★**`d-watch-api.yaml` / `app/api/**` / `lib/*/query.ts` / `lib/db/**` / `lib/types/index.ts` を変更すること**★★ | ★★**変える必要がありません**★★ — ★API は6本とも対応済み★。★★変えたくなったら、それは本書の設計が違うサインなので、**止めて確認を求めること**★★ |
| ★8★ | ★★**レポート画面（`components/features/reports/**`）とダッシュボード（`components/features/dashboard/**`）に手を入れること**★★ | ★レポートは `limit` 方式で別の作り（申し送り②）★。★ダッシュボードは `per_page=5` の**意図的な上位5件**★ |
| ★9★ | ★★**点検記録（F-05）を対象に含めること**★★ | ★画面もAPIも未実装。★★後回しと決定済み★★★ |
| ★10★ | ★★**`package.json` に依存を足すこと**（jsdom / @testing-library / ページャライブラリ等）★★ | ★★純粋関数＋CSS Module で足ります★★。★依存追加は本書の範囲外★ |
| ★11★ | ★★**`docs/decisions.md` / `docs/handoff.md` を段階7 より前に編集すること**★★ | ★★別担当（指示書W / 指示書Z）が番号を取る可能性があります★★。★段階7 でも「自分の追記だけを足す」こと★ |
| ★12★ | ★★**`PAGINATION.defaultPerPage` を 50 以外にしたままコミットすること**★★ | ★段階6 の検証で一時的に小さくすることを**許可します**が、★★戻し忘れると本番の1ページ件数が変わります★★★ |

---

## 作業の順序（★崩さないこと★）

★段階0（前提の確認と実測。★読むだけ・コミットなし★）
→ 段階1（★`lib/pagination/` の純粋関数。★★期待値表 → テスト → 実装★★）
→ 段階2（★`components/ui/Pagination.tsx` と i18n。★画面にはまだ入れない★）
→ 段階3（★清掃詳細ログ F-02 に入れて作法を確定させる★）
→ 段階4（★summary 系3画面 F-03 / F-06 / F-07★）
→ 段階5（★ListResponse 系2画面 F-08 / F-09★）
→ 段階6（★6画面の実測検証。★コミットなし★）
→ 段階7（記録と push）★

★★段階1・2・3・4・5・7 でそれぞれ1コミット（計6コミット）★★。
★★段階ごとに `npx tsc --noEmit` / `npm run lint` / `npm test` を実行し、通ってからコミットすること★★（D-050）。

> ★★**期待値表 → テスト → 実装 の順序を崩さないこと**★★（`CLAUDE.md` §6-2 / §9.4）。
> ★★段階1 は「期待値表を書く → テストを書く → **落ちることを確認する** → 実装する」★★。

> ★★**なぜこの順番なのか**★★
> - ★段階1 を独立させるのは、★★ページ番号の並びは純粋関数でテストが完結する★★から。
>   ★ここだけで止めても何も壊れない★
> - ★段階2 を独立させるのは、★★部品を作った時点では画面の見た目が1ピクセルも変わらない★★から。
>   ★レビューで「部品の設計」と「画面への適用」を分けて見られる★
> - ★段階3 を1画面だけにするのは、★★最初の1画面で作法（state の持ち方・ページを1に戻す場所・置き場所）を確定させる★★ため。
>   ★★6画面を一度に書くと、間違った作法が6箇所に増えます★★
> - ★段階4 と段階5 を分けるのは、★★summary 系3画面は `meta` を**捨てている**ので新しい state が要る★★から
>   （★ListResponse 系2画面とは作業が違う★）

> ★★**段階の並びは実装者が判断して入れ替えてよい**★★（★上は推奨順です★）。
> ★★ただし動かせない制約が2つあります★★ —
> ★★① 段階1（純粋関数とテスト）を段階2 より先にやること★★
> ★★② 段階3（1画面目）を段階4・5（横展開）より先にやること★★

---

## ★★実行順 — 指示書Y・W・Z の後★★

| # | 指示書 | 順序 | ★衝突するファイル★ |
|---|---|---|---|
| ★1★ | `docs/handoff/指示書Y_DATETIMEを日本時間として読む.md` | ★本書より**先**★ | `lib/db/` / `lib/format/` / `scripts/` — ★★本書とは重ならない★★ |
| ★2★ | `docs/handoff/指示書W_連続問題施設を実DBから作る.md` | ★本書より**先**★ | ★★**`lib/i18n.ts`**★★ / `d-watch-api.yaml` / `lib/types/index.ts` / `components/features/reports/ReportsScreen.tsx` — ★★`lib/i18n.ts` が**真正面から衝突**します★★ |
| ★3★ | `docs/handoff/指示書Z_エリア別完了率を実装する.md` | ★本書より**先**★ | ★★**`components/features/detail-log/`**★★ — ★★本書の段階3 と**同じディレクトリ**★★ |
| ★4★ | ★★**本書（指示書AA）**★★ | ★★**最後**★★ | — |

★★着手時にやること★★:

```bash
# 3本の実行状態を実測する（本書作成時点の記録: Y=完了 / W=未着手 / Z=起草中）
grep -n "実行状態" -A 3 docs/handoff/指示書Y_*.md | head -20
grep -n "実行状態" -A 3 docs/handoff/指示書W_*.md | head -20
ls docs/handoff/ | grep "指示書Z"
git status --short   # ★他の担当の未コミット差分が無いことを確認する★
```

★★3本のいずれかが未完了なら、**本書に着手しないこと**★★。
★★特に指示書Z と同時に走らせると `components/features/detail-log/` で確実に衝突します★★。

★★指示書Y / W / Z は ★**読むだけ。編集しないこと**★★（禁止事項2）。

---

## 段階0 — 前提の確認と実測（★読むだけ・コミットなし★）

### 0-1. 読むもの

| # | 対象 | 何を見るか |
|---|---|---|
| 1 | `CLAUDE.md` §5 / §6 / §7 / §9.1〜§9.6 | ★モックが正であること（★本書はその例外★）／期待値表→テスト→実装／ハードコード禁止／ディレクトリ構成★ |
| 2 | `lib/config/index.ts` の `PAGINATION` | ★`defaultPerPage` / `maxPerPage` の現在値を実測すること★ |
| 3 | `vitest.config.ts` | ★★`include` が `lib/**` と `app/**` だけであることを自分の目で確認すること★★ |
| 4 | `lib/i18n.ts` の「数値が文中に入るラベル」節 | ★方式A の作法★ |
| 5 | `components/ui/` の5部品と `.module.css` | ★★命名・props の形・CSS 変数の使い方を真似ること★★ |
| 6 | `app/api/prechecks/route.ts` の `meta` / `summary` コメント | ★禁止事項4 の根拠★ |
| 7 | `app/api/clean-logs/export/route.ts` の冒頭コメント | ★「page / per_page は無視する」★ |
| 8 | 6画面のファイル | ★★state の持ち方・debounce・フィルタのUIを1つずつ数え上げること（0-2）★★ |

### 0-2. ★★各画面の「ページを1に戻すべき操作」を数え上げる（★必須。ここを漏らすと落とし穴5 を踏みます★）★★

★★着手時に自分で数えること★★（★下は監督の実測。★★変わっている前提で確認すること★★★）:

| 画面 | ページを1に戻すべき操作 |
|---|---|
| 清掃詳細ログ F-02 | `dateFrom` / `dateTo` の変更 / 判定フィルタ3ボタン（`problem` / `all` / `critical`）/ ★★`facilityName`・`model`・`serial` の**debounce 反映時**★★ |
| アラート F-03 | `dateFrom` / `dateTo` / 対応状況3ボタン（`open` / `all` / `resolved`） |
| D-Secログ F-06 | ★★`q` の**debounce 反映時**★★ / ステータスフィルタ |
| D-Secアラート F-07 | 種別フィルタ / 対応状況フィルタ |
| 施設 F-08 | ★★`q` の**debounce 反映時**★★ |
| スケジュール F-09 | ★★`q` の**debounce 反映時**★★ |

★★ソート操作のUIは現状どの画面にもありません★★（★並べ替えはサーバ固定★）。
★★将来ソートUIが付いたら、そこでも `setPage(1)` すること★★ — ★段階7 の申し送りに書くこと★。

### 0-3. ★★現状の件数を控える（★あとで「合っている」ことを示す証拠になります★）★★

```bash
# ★fixtures の件数を実測する（★数字を推測しないこと★）★
npx tsx -e "import('./lib/fixtures/index.ts').then(m=>console.log(Object.entries(m).map(([k,v])=>[k,Array.isArray(v)?v.length:typeof v])))"
```

★上が動かない場合は、各画面を開いて総件数を目で控えること★。
★★控えるもの★★: 6画面それぞれの「フィルタ既定状態での総件数」。
★★段階6 で「全{total}件中 …」の total がこの数と一致すること★★を確認します。

### この段階の完了条件

- [ ] ★実行順の3本（Y / W / Z）が完了していることを確認した★
- [ ] ★`git status --short` がクリーン★
- [ ] ★0-2 の「ページを1に戻すべき操作」を自分で数え上げ、手元に控えた★
- [ ] ★0-3 の件数を控えた★
- [ ] ★★`vitest.config.ts` の `include` が `lib/**` と `app/**` だけであることを確認した★★
- [ ] ★コミット無し★

---

## 段階1 — ★ページ番号の並びを組み立てる純粋関数★（1コミット）

### 1-1. ★★置き場所★★

| ファイル | 内容 |
|---|---|
| `lib/pagination/page-items.ts` | ★★純粋関数3本と型★★ |
| `lib/pagination/__tests__/page-items.test.ts` | ★★期待値表そのままのテスト★★ |
| `lib/config/index.ts` | ★`PAGINATION` に `maxVisiblePages` を1つ足すだけ★ |

★★`lib/types/index.ts` に型を置かないこと★★ — ★あそこは `d-watch-api.yaml` のスキーマ名に揃える場所（`CLAUDE.md` §9.2）で、
`PageItem` は API 契約の型ではありません★。

★★import は `@/lib/pagination/page-items` と直接書くこと★★（★`lib/clean-logs/query.ts` 等と同じ作法。barrel は作らない★）。

### 1-2. ★設定値の追加★

```ts
// lib/config/index.ts の PAGINATION に足す
export const PAGINATION = {
  defaultPerPage: 50,
  maxPerPage: 200,
  /**
   * ★ページ番号ボタンを省略せずに全部並べる上限★
   *
   * これを超えたら先頭・末尾・現在の前後だけを出し、間を「…」で省略する。
   * ★★奇数かつ 7 以上であること★★（`buildPageItems()` が左右対称に切り出すため。
   * テストで固定している）。
   *
   * ★なぜ設定値にするのか★: `CLAUDE.md` §7（閾値のハードコード禁止）。
   *   画面幅の都合で並べる数を変えたくなったとき、直す場所をここ1箇所にする。
   *
   * ★消費者★: `lib/pagination/page-items.ts` の `buildPageItems()` のみ。
   */
  maxVisiblePages: 7,
} as const;
```

### 1-3. ★★期待値表（★これをそのままテストにすること。実装より先に書く★）★★

#### (A) `totalPageCount(total: number, perPage: number): number`

| # | total | perPage | ★期待★ | 意図 |
|---|---|---|---|---|
| A-1 | 0 | 50 | ★0★ | ★★0件は0ページ（1ページではない）★★ |
| A-2 | 1 | 50 | 1 | 最小 |
| A-3 | 50 | 50 | ★1★ | ★★ちょうど per_page の倍数★★ |
| A-4 | 51 | 50 | ★2★ | ★★端数あり（+1件で2ページ）★★ |
| A-5 | 100 | 50 | ★2★ | ★★ちょうど倍数★★ |
| A-6 | 101 | 50 | ★3★ | ★端数あり★ |
| A-7 | 33 | 50 | 1 | ★fixtures 相当（ローカルでは1ページ）★ |
| A-8 | 360 | 50 | ★8★ | ★★本番の1日ぶん（`docs/spec.md:45`）★★ |
| A-9 | 11150 | 50 | ★223★ | ★大量ページ★ |
| A-10 | 10 | 5 | 2 | perPage が変わっても効く |
| A-11 | 10 | 0 | ★0★ | ★★防御。`Infinity` を返さないこと（落とし穴1）★★ |
| A-12 | 10 | -1 | ★0★ | ★防御★ |
| A-13 | -1 | 50 | ★0★ | ★防御★ |

#### (B) `pageRange(total, page, perPage): { from: number; to: number }`

| # | total | page | perPage | ★期待★ | 意図 |
|---|---|---|---|---|---|
| B-1 | 0 | 1 | 50 | ★{from:0, to:0}★ | ★★0件で `from:1` にしないこと（落とし穴2）★★ |
| B-2 | 1 | 1 | 50 | {1, 1} | 最小 |
| B-3 | 50 | 1 | 50 | {1, 50} | ちょうど1ページ |
| B-4 | 51 | 1 | 50 | {1, 50} | 1ページ目 |
| B-5 | 51 | 2 | 50 | ★{51, 51}★ | ★★端数の最終ページ★★ |
| B-6 | 100 | 2 | 50 | {51, 100} | ちょうど倍数の最終ページ |
| B-7 | 360 | 8 | 50 | ★{351, 360}★ | 本番相当 |
| B-8 | 51 | ★3★ | 50 | ★{51, 51}★ | ★★存在しないページを渡されたら最終ページに丸める★★ |
| B-9 | 51 | ★0★ | 50 | ★{1, 50}★ | ★0以下は1ページ目に丸める★ |

#### (C) `buildPageItems(page: number, totalPages: number): PageItem[]`

★★表記★★: 数字は `{ kind: "page", page: n }`、`…` は `{ kind: "ellipsis" }`。
★★現行 `maxVisiblePages = 7` での期待値です★★。

| # | page | totalPages | ★期待★ | 意図 |
|---|---|---|---|---|
| C-1 | 1 | ★0★ | ★`[]`★ | ★★0件★★ |
| C-2 | 1 | 1 | `[1]` | ★1ページ（★部品側では出さない。1-5 参照★）★ |
| C-3 | 1 | 2 | `[1, 2]` | ★★2ページ（ユーザー要件の最小ケース）★★ |
| C-4 | 2 | 2 | `[1, 2]` | 現在ページが末尾 |
| C-5 | 1 | 7 | `[1,2,3,4,5,6,7]` | ★★省略しない上限ちょうど★★ |
| C-6 | 4 | 7 | `[1,2,3,4,5,6,7]` | 現在ページが中央 |
| C-7 | 1 | 8 | ★`[1,2,3,4,5, …, 8]`★ | ★★省略が始まる最初のケース★★ |
| C-8 | 4 | 8 | `[1,2,3,4,5, …, 8]` | 先頭寄りの境界 |
| C-9 | ★5★ | 8 | ★`[1, …, 4,5,6,7,8]`★ | ★★末尾寄りに切り替わる境界★★ |
| C-10 | 8 | 8 | `[1, …, 4,5,6,7,8]` | 現在ページが末尾 |
| C-11 | 1 | 9 | `[1,2,3,4,5, …, 9]` | — |
| C-12 | ★5★ | 9 | ★`[1, …, 4,5,6, …, 9]`★ | ★★中央（「…」が2つ出る最小ケース）★★ |
| C-13 | 9 | 9 | `[1, …, 5,6,7,8,9]` | — |
| C-14 | 1 | ★223★ | ★`[1,2,3,4,5, …, 223]`★ | ★★大量ページ・先頭★★ |
| C-15 | 4 | 223 | `[1,2,3,4,5, …, 223]` | 先頭寄りの境界 |
| C-16 | 5 | 223 | ★`[1, …, 4,5,6, …, 223]`★ | 中央に切り替わる境界 |
| C-17 | ★112★ | 223 | ★`[1, …, 111,112,113, …, 223]`★ | ★★大量ページ・中央★★ |
| C-18 | 219 | 223 | `[1, …, 218,219,220, …, 223]` | 末尾寄りに切り替わる直前 |
| C-19 | ★220★ | 223 | ★`[1, …, 219,220,221,222,223]`★ | ★末尾寄りの境界★ |
| C-20 | ★223★ | 223 | ★`[1, …, 219,220,221,222,223]`★ | ★★大量ページ・末尾★★ |
| C-21 | ★0★ | 5 | `[1,2,3,4,5]`（page=1 扱い） | ★範囲外を丸める★ |
| C-22 | ★99★ | 5 | `[1,2,3,4,5]`（page=5 扱い） | ★範囲外を丸める★ |

#### (D) ★★不変条件テスト（★これがユーザー要件の合否そのものです★）★★

★★`for (totalPages = 1; totalPages <= 300; totalPages++)` × `for (page = 1; page <= totalPages; page++)` を
総当たりして、以下を**すべて**満たすこと★★:

| # | 不変条件 |
|---|---|
| ★D-1★ | ★★`kind === "page"` の値がすべて `1 <= n <= totalPages` に入る★★ ← ★★**「3ページ目を押したら有りません」の禁止**★★ |
| D-2 | ★必ず `1` を含む★ |
| D-3 | ★必ず `totalPages` を含む★ |
| D-4 | ★必ず `page`（現在ページ）自身を含む★ |
| D-5 | ★数字は狭義単調増加（重複なし・逆転なし）★ |
| ★D-6★ | ★★「…」が飛ばすページ数は**必ず2以上**★★（★1ページだけを「…」で隠さない。落とし穴4★） |
| D-7 | ★「…」が連続しない★ |
| D-8 | ★先頭と末尾は必ず数字（「…」で始まらない・終わらない）★ |
| D-9 | ★要素数は `totalPages <= maxVisiblePages` なら `totalPages`、それ以外は**常に `maxVisiblePages`**★（★幅がガタつかない★） |
| D-10 | ★`PAGINATION.maxVisiblePages` が奇数かつ7以上★（★設定値そのものへのアサーション★） |

### 1-4. ★実装（★アルゴリズムの定義★）★

```
W    = PAGINATION.maxVisiblePages   // 7
half = (W + 1) / 2                  // 4

buildPageItems(page, totalPages):
  if totalPages <= 0: return []
  cur = clamp(page, 1, totalPages)

  if totalPages <= W:
      return [1 .. totalPages]

  if cur <= half:
      // 先頭寄り: 1..(W-2), …, totalPages
      return [1 .. W-2] + [ELLIPSIS] + [totalPages]

  if cur >= totalPages - (half - 1):
      // 末尾寄り: 1, …, (totalPages-(W-3))..totalPages
      return [1] + [ELLIPSIS] + [totalPages-(W-3) .. totalPages]

  // 中央: 1, …, cur-1, cur, cur+1, …, totalPages
  side = (W - 5) / 2                // 1
  return [1] + [ELLIPSIS] + [cur-side .. cur+side] + [ELLIPSIS] + [totalPages]
```

★★`7` / `4` / `5` / `2` を関数の中に直書きしないこと★★ — ★★すべて `W` から導くこと★★（禁止事項3）。

型:

```ts
export type PageItem =
  | { kind: "page"; page: number }
  | { kind: "ellipsis"; key: string };   // key は React の key 用（"gap-left" / "gap-right" 等）
```

★★`kind: "ellipsis"` に `page` を持たせないこと★★ — ★持たせると「…を押したら何ページに飛ぶ？」という
存在しない問いが生まれます★。★★型で押せなくしておくのが一番確実です★★。

### この段階の完了条件

- [ ] ★★期待値表（A 13件 + B 9件 + C 22件 + D 10条件）を**先に**書き、テストにした★★
- [ ] ★★テストを実装前に走らせて**落ちることを確認した**★★
- [ ] ★実装後、`npm test` が全パス★
- [ ] ★`lib/pagination/page-items.ts` に `7` / `50` / `200` の直書きが無い★
- [ ] ★`npx tsc --noEmit` = 0 / `npm run lint` = 0★
- [ ] ★コミット（★ファイルパスを明示すること★）:
      `git add lib/config/index.ts lib/pagination/page-items.ts lib/pagination/__tests__/page-items.test.ts`★

---

## 段階2 — ★共通部品 `Pagination` と i18n★（1コミット）

★★この段階では画面の見た目は1ピクセルも変わりません★★（★部品を作るだけ★）。

### 2-1. ★部品の設計★

| ファイル | 内容 |
|---|---|
| `components/ui/Pagination.tsx` | 部品本体 |
| `components/ui/Pagination.module.css` | ★CSS Module（★既存5部品と同じ作法★）★ |

```tsx
"use client";

interface PaginationProps {
  /** ★★`meta.total` を渡すこと。`summary.total` を渡さないこと★★（D-054 / 禁止事項4） */
  total: number;
  /** 現在ページ（1始まり） */
  page: number;
  /** ★1ページあたりの件数。`PAGINATION.defaultPerPage` を渡す。直書き禁止★ */
  perPage: number;
  onPageChange: (page: number) => void;
}

export function Pagination({ total, page, perPage, onPageChange }: PaginationProps) { … }
```

★★描画の規則★★:

| 条件 | 描画 |
|---|---|
| `total === 0` | ★★`null`（何も出さない）★★ — ★各画面の空表示に任せる（調査6）★ |
| ★総ページ数が1★ | ★★件数表示だけ出す。ページ送りのボタンは出さない★★ |
| ★総ページ数が2以上★ | ★件数表示 + 「前へ」+ 番号 + 「…」+「次へ」★ |

★★細部★★:

- ★件数表示は左、ボタンは右★（`display:flex; justify-content:space-between; align-items:center;`）
- ★★「…」は `<span aria-hidden="true">` で出す。`<button>` にしないこと★★（禁止事項5）
- ★現在ページのボタンは `disabled` かつ `aria-current="page"`★（★押しても何も起きない・スクリーンリーダーに現在地が伝わる★）
- ★「前へ」は1ページ目で `disabled`、「次へ」は最終ページで `disabled`★
- ★★`onPageChange` に渡す値は必ず `1 <= n <= totalPages`★★（★部品の中で `clamp` すること★）
- ★色は CSS 変数（`var(--ac)` / `var(--bd)` / `var(--tx-m)` / `var(--tx-d)`）★。
  ★★`THEME_TOKENS` を import しないこと★★（★`components/ui/` の既存部品はどれも使っていない。落とし穴9★）
- ★★`useTheme()` は **`t` の取得のためだけに**使う★★。
  ★★理由: 件数表示の文言を6画面に散らすと `.replace()` の連鎖が6箇所に増える★★。
  ★★`components/ui/` で `useTheme()` を使う最初の部品になるので、その旨を docstring に1行書くこと★★
- ★`"use client"` を付けること★（★`useTheme()` を使うため★）

### 2-2. ★i18n に足すキー（★方式A★）★

```ts
// lib/i18n.ts の EN に足す
// ---- 一覧のページ送り（指示書AA）----
// ★プレースホルダが3つある（既存の {n} 1つより多い）。replace を3回つなげる★
//   t("全{total}件中 {from}〜{to}件")
//     .replace("{total}", String(total))
//     .replace("{from}", String(from))
//     .replace("{to}", String(to))
"全{total}件中 {from}〜{to}件": "{from}–{to} of {total}",
前へ: "Prev",
次へ: "Next",
// ★CSVは表示中のページではなく全件（app/api/clean-logs/export/route.ts が page/per_page を無視する契約）★
"CSVは絞り込み条件に一致する全件を出力します（表示中のページだけではありません）":
  "CSV exports all rows matching the filters, not just the current page",
```

★★`前へ` / `次へ` が既存キーと衝突しないことを `grep -n "前へ\|次へ" lib/i18n.ts` で確認すること★★
（★監督の実測では0件★）。

### 2-3. ★★やってはいけないこと★★

- ★★件数表示の文言を各画面で組み立てること★★（★部品の中1箇所に閉じ込める★）
- ★★`page` / `perPage` を部品の内部 state にすること★★（★★親が持つ。部品は表示と通知だけ★★）
- ★★部品の中で `fetch` すること★★（★★純粋な表示部品にすること★★）

### この段階の完了条件

- [ ] ★`components/ui/Pagination.tsx` / `.module.css` を作った★
- [ ] ★`total === 0` で `null` を返す★
- [ ] ★総ページ数1でボタンが出ない★
- [ ] ★「…」が `<button>` になっていない★
- [ ] ★`lib/i18n.ts` に4キーを足した（★既存キーを1つも消していない★）★
- [ ] ★部品に `50` / `200` / `7` の直書きが無い★
- [ ] ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- [ ] ★コミット: `git add components/ui/Pagination.tsx components/ui/Pagination.module.css lib/i18n.ts`★

---

## 段階3 — ★清掃詳細ログ F-02 に入れる（★ここで作法を確定させる★）★（1コミット）

★★対象: `components/features/detail-log/DetailLogScreen.tsx`★★
（★★指示書Z が同じディレクトリを触ります。指示書Z の完了を確認してから着手すること★★）

### 3-1. `per_page` と `page` を送る

```diff
- params.set("per_page", String(PAGINATION.maxPerPage));
+ params.set("per_page", String(PAGINATION.defaultPerPage));
+ params.set("page", String(page));
```

★★`page` を `useMemo` の依存配列に入れること★★（★★落とし穴6。入れ忘れるとボタンを押しても取り直しません★★）。

### 3-2. state を1つ足す

```ts
const [page, setPage] = useState(1);
```

★★`total` は既に `useState(0)` で持っています★★（`setTotal(body.meta.total)`）。★そのまま使うこと★。

### 3-3. ★★存在しないページに取り残されたときの保険★★

`fetchLogs()` の成功時に:

```ts
setItems(body.items);
setTotal(body.meta.total);
// ★★フィルタ外の要因（データ更新など）で総ページ数が減り、存在しないページに
//   取り残された場合の保険。最終ページに寄せて取り直す★★
const pages = totalPageCount(body.meta.total, body.meta.per_page);
if (pages > 0 && body.meta.page > pages) setPage(pages);
```

★★`pages > 0` の判定を落とさないこと★★（★0件のときに `setPage(0)` して無限ループになります★）。

### 3-4. ★★ページを1に戻す（★落とし穴5 を踏まない書き方★）★★

★★**`useEffect` で後追いしないこと**★★。★★state を変えるその場で一緒に戻すこと★★。

★debounce のあるもの（施設名・機種・シリアル）★ — ★★タイムアウトのコールバックの中で一緒に戻す★★:

```ts
useEffect(() => {
  const id = window.setTimeout(() => {
    setDebouncedFacility(facilityName);
    setPage(1);          // ★★debounce の反映と同時に戻す（React が1回の再描画にまとめる）★★
  }, 300);
  return () => window.clearTimeout(id);
}, [facilityName]);
```

★debounce の無いもの（日付・判定フィルタ）★ — ★ハンドラの中で一緒に戻す★:

```tsx
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
onClick={() => { setGradeFilter("problem"); setPage(1); }}
```

★★0-2 で数え上げた操作を**1つ残らず**潰すこと★★。

### 3-5. フッタを差し替える

```diff
- <div className={styles.footer}>
-   {total}件
-   {items.length < total ? `（表示 ${items.length}件）` : ""}
- </div>
+ <Pagination
+   total={total}
+   page={page}
+   perPage={PAGINATION.defaultPerPage}
+   onPageChange={setPage}
+ />
```

★★`items.length` を件数表示に使わないこと★★（落とし穴3）。

### 3-6. ★CSV の注記★

★CSVボタンの近く（`toolbarRight` の下、または CSV ボタンの直後）に小さく出すこと★:

```tsx
<span className={styles.csvNote}>
  {t("CSVは絞り込み条件に一致する全件を出力します（表示中のページだけではありません）")}
</span>
```

★★CSVリンクの `href` は `queryString` のまま変えないこと★★（落とし穴7）。

### この段階の完了条件

- [ ] ★★清掃詳細ログで2ページ目に移動でき、内容が変わる★★（★検証方法は段階6-1★）
- [ ] ★フィルタ・検索を変えると1ページ目に戻る（★debounce 経由も含む★）★
- [ ] ★件数表示が「全{total}件中 {from}〜{to}件」になっている★
- [ ] ★CSVの注記が出ている★
- [ ] ★`PAGINATION.maxPerPage` の参照がこのファイルから消えている★
- [ ] ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- [ ] ★コミット（★`components/features/detail-log/` 配下で自分が触ったファイルだけを明示★）★

---

## 段階4 — ★summary 系3画面（F-03 / F-06 / F-07）★（1コミット）

★★対象★★:
`components/features/alerts/AlertsScreen.tsx` /
`components/features/ds-logs/DsLogScreen.tsx` /
`components/features/ds-alerts/DsAlertsScreen.tsx`

★★この3画面は現在 `body.summary` しか読まず、`body.meta` を捨てています★★ → ★★`total` の state を新設すること★★。

```ts
const [total, setTotal] = useState(0);
…
setItems(body.items);
setSummary(body.summary);
setTotal(body.meta.total);      // ★★summary.total ではない★★（禁止事項4）
```

★★`setTotal(body.summary.total)` と書いたら間違いです★★ — ★★`summary` は対応状況フィルタの**前**に
計算されているので（D-054）、`status=open` で絞ると `summary.total > meta.total` になり、
**存在しないページのボタンが出ます**★★（★★ユーザーが名指しで禁じた状態★★）。

★あとは段階3 と同じ★:
- `per_page` を `defaultPerPage` に、`page` を送る（★`useMemo` の依存に `page` を入れる★）
- 3-3 の保険
- 0-2 で数え上げた操作すべてで `setPage(1)`
- 一覧の末尾（★空表示の外側★）に `<Pagination …/>` を置く

★★D-Secログ（F-06）だけの注意★★:
★備考のインライン編集があり、`commitNote()` が `setItems()` で**その行だけ差し替えています**★
（★全件再取得を避けるため。既存コメント参照★）。
★★備考の保存で `page` を触らないこと★★（★保存のたびに1ページ目に飛ばされたら使い物になりません★）。

★★また、`DsLogScreen.tsx` の docstring に
「一覧は最大 `PAGINATION.maxPerPage` 件並ぶため」という**古くなる記述**があります★★ →
★★`PAGINATION.defaultPerPage` 件に直すこと★★（★★grep で実測して直すこと。行番号は変わります★★）。

### この段階の完了条件

- [ ] ★3画面とも `body.meta.total` を読んでいる★
- [ ] ★★`grep -rn "summary.total" components/features/` にページ送り由来の参照が**1件も無い**★★
      （★`StatCard` の表示に使っているものは残ってよい★）
- [ ] ★D-Secログの備考保存でページが飛ばない★
- [ ] ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- [ ] ★コミット（3ファイル + 必要なら CSS を明示）★

---

## 段階5 — ★ListResponse 系2画面（F-08 / F-09）★（1コミット）

★★対象★★:
`components/features/facilities/FacilitiesScreen.tsx` /
`components/features/schedules/SchedulesScreen.tsx`

### 5-1. 施設ロボットマスタ F-08

★`total` の state は既にあります★（`setTotal(body.meta.total)`）。

```diff
- <div className={styles.footer}>
-   {t("全{n}施設").replace("{n}", String(total))}
- </div>
+ <Pagination total={total} page={page} perPage={PAGINATION.defaultPerPage} onPageChange={setPage} />
```

★★`全{n}施設` が使われなくなります★★ →

```bash
grep -rn '全{n}施設"' --include=*.tsx --include=*.ts . | grep -v node_modules | grep -v "\.claude/"
```

★★上が `lib/i18n.ts` の定義行だけになったら、キー `"全{n}施設": "{n} facilities total"` と
その直上の解説コメント（`t("全{n}施設").replace(...)` の例が書かれた3行）を削除すること★★。
★★`"全{n}施設を表示 →"` は**別のキー**（レポート画面が使用中）なので**残すこと**★★。

### 5-2. スケジュール管理 F-09

★★`total` の state がありません★★ → ★新設して `body.meta.total` を入れること★。
★既存コメント「fixtures は13件だが、ページャの無い画面なので上限で一度に取り切る」を★★書き換えること★★
（★★もうページャがあります★★）。

### この段階の完了条件

- [ ] ★2画面ともページ送りと件数表示が出る★
- [ ] ★施設の `全{n}施設` を「全{total}件中 …」に置き換え、未使用になったキーを削除した★
- [ ] ★★`"全{n}施設を表示 →"` を消していない★★
- [ ] ★スケジュールの古いコメントを直した★
- [ ] ★★`grep -rn "PAGINATION.maxPerPage" components/features/` が
      `components/features/reports/ReportsScreen.tsx` の ★★2件（JSDoc の `:38` とコード `:42` の `SHOW_ALL_LIMIT`）だけ★★ になっている
      （★1件ではない。実測済み。★★コメント行を消しにいかないこと★★ — reports に差分を出すと禁止事項8 と完了条件16 に違反する）
- [ ] ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- [ ] ★コミット（2ファイル + `lib/i18n.ts` を明示）★

---

## 段階6 — ★6画面の実測検証（★コミットなし★）★

### 6-1. ★★ローカルでは2ページ目が出ない（★対処を必ず読むこと★）★★

★★`lib/fixtures/` の件数は数十件（監督の実測では清掃詳細ログで33件）しかなく、
`defaultPerPage = 50` では**どの画面も1ページで収まります**★★。

★★対処（★この順で試すこと★）★★:

| # | 方法 | 注意 |
|---|---|---|
| ★1★ | ★★`lib/config/index.ts` の `defaultPerPage` を**一時的に `5`** にして6画面を確認する★★ | ★★確認後に必ず `50` に戻し、`git diff lib/config/index.ts` が★**`maxVisiblePages` の追加ぶんだけ**★になることを確認すること★★（禁止事項12） |
| 2 | ★API を直接叩いて `meta` を確認する★ | ★`curl "http://localhost:3000/api/clean-logs?date_from=…&date_to=…&result=all&per_page=5&page=2"`★。★認証・IP制限の回避方法は `docs/handoff.md` / D-101 項2（`ALLOW_DIRECT_ACCESS`）を参照★ |
| 3 | ★大量ページの見た目は段階1 のテストで固定済み★ | ★★223ページの並びを画面で作る必要はありません★★ |

★★禁止★★: ★★`lib/fixtures/` に行を足して件数を増やすこと★★（禁止事項6）。

### 6-2. ★★6画面の検証手順（★1画面ずつ、全部やること★）★★

★各画面について、以下を順に確認して結果を控えること★:

| # | 確認 |
|---|---|
| ★1★ | ★画面を開くと**件数表示**が出る（「全{total}件中 1〜N件」）★ |
| ★2★ | ★★`total` が 0-3 で控えた件数と**一致する**★★ |
| ★3★ | ★`defaultPerPage` を小さくすると**ページ送りのボタンが出る**★ |
| ★4★ | ★★描画されているページ番号が**すべて実在する**（最大の番号を押しても空にならない）★★ |
| ★5★ | ★2ページ目を押すと**内容が変わり**、件数表示が「51〜100件」のように追随する★ |
| ★6★ | ★最終ページの `to` が `total` と一致する★ |
| ★7★ | ★★フィルタ／検索を変えると**1ページ目に戻る**★★（★2ページ目に居る状態で試すこと★） |
| ★8★ | ★★debounce のある検索欄でも戻る★★（★文字を打って300ms待つ★） |
| ★9★ | ★0件になる条件で絞ると、★★ページ送りも件数表示も消え、空表示だけが出る★★★ |
| ★10★ | ★1ページに収まるとき、★★件数表示は出るがボタンは出ない★★★ |
| ★11★ | ★ダーク／ライト両方で崩れない★ |
| ★12★ | ★EN に切り替えて「{from}–{to} of {total}」「Prev」「Next」が出る★ |

★★画面ごとの追加確認★★:

| 画面 | 追加で見ること |
|---|---|
| 清掃詳細ログ F-02 | ★CSVの注記が出ている★ / ★★CSVを実際に落として、**表示中のページではなく全件**入っていること★★ / ★行展開（アドバイス）がページ移動後も動く★ |
| アラート F-03 | ★★`StatCard` の3枚（未解消/解消済み/合計）が**ページ移動で変わらない**★★（★`summary` はページに依存しない★） |
| D-Secログ F-06 | ★★備考を編集して保存しても**ページが飛ばない**★★ / ★`StatCard` がページ移動で変わらない★ |
| D-Secアラート F-07 | ★`StatCard` がページ移動で変わらない★ |
| 施設 F-08 | ★★`全{n}施設` が消え、新しい形になっている★★ |
| スケジュール F-09 | ★件数表示が新規に出ている★ |

### 6-3. ★回帰していないこと★

| # | 確認 |
|---|---|
| 1 | ★★レポート画面（F-04）に差分が無い★★（`git diff --stat components/features/reports/`） |
| 2 | ★★ダッシュボード（F-01）に差分が無い★★ |
| 3 | ★`git diff --stat` に `lib/fixtures/` / `app/api/` / `lib/db/` / `d-watch-api.yaml` / `prisma/schema.prisma` / `lib/types/index.ts` が**出てこない**★ |
| 4 | ★`npm test` のテスト数が着手前より**増えている**★ |

### この段階の完了条件

- [ ] ★6画面 × 12項目の確認をすべて実施し、結果を控えた★
- [ ] ★★`defaultPerPage` が `50` に戻っている★★
- [ ] ★6-3 の回帰確認がすべて通った★
- [ ] ★コミット無し★

---

## 段階7 — 記録と push（1コミット）

### 7-1. `docs/decisions.md` に追記（★★着手直前に実測した空き番号を使うこと★★）

★★書くこと★★:

| # | 内容 |
|---|---|
| ★1★ | ★★モックに無いUIを新規に作った経緯と、それが `CLAUDE.md` §5 の例外であること★★。★★ユーザーの明示の承認（2026-09-11）を原文で引用すること★★ |
| ★2★ | ★「実在するページだけを描画する」を**不変条件テスト**で固定したこと（段階1 の D-1〜D-10）★ |
| 3 | ★省略の規則（`maxVisiblePages = 7`、先頭寄り／中央／末尾寄りの3分岐）と、なぜ幅を固定したか★ |
| 4 | ★件数表示を「全{total}件中 {from}〜{to}件」に統一し、`{total}件（表示 N件）` と `全{n}施設` を廃止したこと★ |
| ★5★ | ★★`meta.total` を使い `summary.total` を使わない理由（D-054 の帰結）★★ |
| ★6★ | ★★**ページ送りを付けても DB からは全件読む。性能は改善しない**こと。別課題として切り離したこと★★ |
| ★7★ | ★却下した4案（`maxPerPage` を上げる / 無限スクロール / もっと見る / カーソルページング）と理由★ |
| 8 | ★対象外にしたもの（レポート / ダッシュボード / 点検記録）と理由★ |

### 7-2. `docs/handoff.md` に追記（★番号は実測すること★）

★★`grep -n "^## 20" docs/handoff.md | tail -3` で最終の追記番号を実測してから足すこと★★。

### 7-3. 本書の実行状態を更新

★冒頭表の「実行状態」と文末の「実行状態」表を★★完了★★に更新し、
★実行日 / 決定番号 / コミットハッシュ / 実測値（`tsc` / `lint` / テスト数）★を書くこと。

### この段階の完了条件

- [ ] ★`docs/decisions.md` に実測した番号で追記した（★7-1 の8点すべて★）★
- [ ] ★`docs/handoff.md` に追記した★
- [ ] ★本書の実行状態を更新した★
- [ ] ★コミット: `git add docs/decisions.md docs/handoff.md docs/handoff/指示書AA_一覧にページ送りを付ける.md`★
- [ ] ★push★

---

## ★★申し送り★★

★★以下は本指示書では直しません。★★ ★決定事項に列挙して残すこと★。

| # | 論点 | 状態 |
|---|---|---|
| ★★①★★ | ★★**DB から全件読む構造は変わらない**★★ — ★`DataSource` が引数を取らない契約で、`prismaSource.cleanLogs()` は全期間・全件 `findMany()`★。★コード自身が「年間13万行でボトルネックになりうる」と警告している（`lib/db/index.ts` / `lib/db/clean-log-mapping.ts`）★。★★ページを繰るたびに全件取得が走るので、体感はむしろ悪化する方向★★ | ★★別課題（SQL に絞り込みを降ろす改修）として切り出すこと★★。★★**ページ送りと同時にやらないこと**★★ — ★★連続日数の計算が全期間を走査しているため、同時に壊れます★★。★★合わせて「ページ番号を URL に持たせるか（`useSearchParams` + Suspense 境界）」も未決★★ |
| ★★②★★ | ★★**レポート画面の「全{n}施設を表示 →」が200件で切れる潜在バグ**★★ — ★`components/features/reports/ReportsScreen.tsx` の `SHOW_ALL_LIMIT = PAGINATION.maxPerPage`（=200）★。★★施設が350ある顧客では「全350施設を表示 →」を押しても200件しか出ません★★。★件数表示は `totalCount`（=350）を出すので、★★数字と中身が食い違います★★★ | ★★本書では直さない★★（禁止事項8）。★★別指示書で「レポート画面もページ送りに寄せる」か「`SHOW_ALL_LIMIT` を撤廃してサーバ側で全件返す」かを決めること★★。★★P1のドン・キホーテ1社で施設が何件になるかを先に確認すること★★（★200を超えないなら急がない★） |
| ★★③★★ | ★★**CSV と画面の件数が食い違う**★★ — ★CSV は絞り込み条件に一致する**全件**、画面は**1ページぶん**★。★本書では画面に注記を出すだけ（段階3-6）★ | ★★`export` に件数上限が無いことも未確認★★ — ★★年間13万行を1本のCSVに吐く挙動を、実データが入る前に一度確かめること★★ |
| ★★④★★ | ★★**fixtures は数十件しかなく、ローカルでは2ページ目が出ない**★★（★清掃詳細ログで33件★） | ★★検証は `defaultPerPage` を一時的に `5` にして行い、**必ず 50 に戻すこと**★★（段階6-1）。★★`lib/fixtures/` に行を足さないこと★★（禁止事項6）。★★大量ページの見た目は段階1 の期待値表（223ページ）で固定済み★★ |
| ⑤ | ★**1ページあたりの件数を切り替えるUI（20/50/100）は作っていない**★ | ★要望が出たら `perPage` を state 化して `Pagination` に選択UIを足す★。★★そのとき `PAGINATION.maxPerPage`（200）を超えないこと（サーバがクランプする）★★ |
| ⑥ | ★**ソートUIが付いたときの `setPage(1)`**★ — ★現状どの画面にもソートUIが無い（並べ替えはサーバ固定）★ | ★★将来ソートUIを足す人は、そこでも `setPage(1)` すること★★ |
| ⑦ | ★**点検記録（F-05）は画面もAPIも未実装**★ | ★実装するときは最初から `Pagination` を入れること★ |

---

## 完了条件（全体）

| # | 条件 |
|---|---|
| ★1★ | ★`lib/pagination/page-items.ts` に `totalPageCount()` / `pageRange()` / `buildPageItems()` があり、`PageItem` の `ellipsis` が `page` を持っていない★ |
| ★2★ | ★★段階1 の期待値表（A 13 + B 9 + C 22 + D 10）がすべてテストになっている★★ |
| ★3★ | ★★不変条件テストが `totalPages = 1..300` × 全ページを総当たりし、★**存在しないページ番号が1つも描画されない**★ことを固定している★★ |
| ★4★ | ★★「…」が飛ばすページ数が必ず2以上であることがテストで固定されている★★ |
| ★5★ | ★`PAGINATION.maxVisiblePages` が追加され、奇数かつ7以上であることがテストで固定されている★ |
| ★6★ | ★`components/ui/Pagination.tsx` があり、`total === 0` で `null`、総ページ数1でボタンを出さない★ |
| ★7★ | ★★「…」が `<button>` になっていない★★ |
| ★8★ | ★★6画面すべてに件数表示（「全{total}件中 {from}〜{to}件」）とページ送りが入っている★★ |
| ★9★ | ★★`grep -rn "PAGINATION.maxPerPage" components/features/` が `reports/ReportsScreen.tsx` の★2件（JSDoc の `:38` とコード `:42`）だけ★（★1件ではない。実測済み。コメント行を消しにいかないこと★）★★ |
| ★10★ | ★★6画面すべてが `body.meta.total` を使っており、`summary.total` を総ページ数に使っていない★★ |
| ★11★ | ★★フィルタ・検索（debounce 経由を含む）を変えると6画面すべてが1ページ目に戻る★★ |
| ★12★ | ★清掃詳細ログにCSVの注記が出ており、CSVの中身は全件のまま★ |
| ★13★ | ★`lib/i18n.ts` に4キーを足し、未使用になった `"全{n}施設"` を削除した（★`"全{n}施設を表示 →"` は残っている★）★ |
| ★14★ | ★★`50` / `200` / `7` がコンポーネントにも純粋関数にも直書きされていない★★ |
| ★15★ | ★★`lib/fixtures/` / `app/api/**` / `lib/db/**` / `lib/*/query.ts` / `d-watch-api.yaml` / `lib/types/index.ts` / `prisma/schema.prisma` に差分が無い★★ |
| ★16★ | ★★`components/features/reports/**` / `components/features/dashboard/**` に差分が無い★★ |
| ★17★ | ★★`package.json` / `package-lock.json` に差分が無い★★ |
| ★18★ | ★★`lib/config/index.ts` の差分が `maxVisiblePages` の追加だけ（`defaultPerPage` が 50 に戻っている）★★ |
| ★19★ | ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス（★テスト数は着手前より増えている★）★ |
| ★20★ | ★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / 他の指示書 / `.claude/worktrees/**` に触っていない★ |
| ★21★ | ★★`docs/decisions.md`（★実測した空き番号★）と `docs/handoff.md` に記録した。★モック例外の承認経緯と、性能が改善しないことを明記した★★★ |
| ★22★ | ★6コミット（段階1 / 2 / 3 / 4 / 5 / 7）で push した★ |

---

## 実行状態

| 項目 | 内容 |
|---|---|
| 状態 | ★★**完了**★★（★段階0〜7 をすべて実行。★段階0〜3 と段階4〜7 は別のエージェントが分担★★） |
| 実行日 | 2026-09-11 |
| 決定番号 | ★★D-120★★（`docs/decisions.md:7762`）/ `docs/handoff.md` ★追記40★（`:3363`） |
| コミット | 段階1 `fd4ca5b` / 段階2 `fcdcb07` / 段階3 `9e9704c` / ★段階4 `78fbfac`★ / ★段階5 `77693c4`★ / 段階7（記録）。★★push は未実施（監督確認待ち）★★ |
| 実測値 | ★テスト 48ファイル / 1137件 全パス★（★段階1 で +48件。段階4・5 は画面コードのみでテスト追加なし★）/ `npx tsc --noEmit` = ★0★ / `npx eslint .` = ★0★ / `grep -rn "PAGINATION.maxPerPage" components/features/` = ★`reports/ReportsScreen.tsx` の2件（`:38` JSDoc / `:42` コード）だけ★ |
| ★段階6 について★ | ★★ブラウザでの目視は行っていない★★（★dev server がアクセストークンのログインを要求し、認証情報の入力は行わない方針のため★）。★代わりに APIルートの `GET` を直接呼び、6画面ぶんの `meta` と「描画される最大のページ番号を押しても空にならないこと」を実測した★。★★目視が要る項目は `docs/handoff.md` 追記40 項6 に列挙してある★★ |
| ★`defaultPerPage` について★ | ★★`lib/config/index.ts` は本書の対象外ファイルなので、段階6-1 の一時変更は**行っていない**★★（`defaultPerPage` は `50` のまま）。★代わりにクエリの `per_page=5` で同等の確認を行った★ |
