対象は ★D-Secログ(F-06) / D-Secアラート(F-07) / 施設ロボットマスタ(F-08) / スケジュール(F-09) / システム管理(F-10)★ の5画面です。 残り5画面(ダッシュボード / 清掃詳細ログ / アラート / レポート / 清掃面積)は ★docs/データの流れ_画面1-5_20260917.md の担当★で、本書には含みません。
「この画面のこの列は、DBのどの列から来て、どの関数を通って、どんな型になって、 どう表示されるのか」を、★実物の値つきで★1本の縦線として追えるようにしたもの。 抽象的な設計説明ではなく、★file:line と実際の値★だけで構成している。
各列は次の6段で書く。段が無い場合(変換が無い等)はその段を省略する。
【DBの列】 テーブル.カラム
型: 〈Prisma型 / @db 型〉(prisma/schema.prisma:NNN)
意味: 〈DDL上の意味・値域〉
↓ 取得
【取得】 lib/db/index.ts:NNN(prismaSource)/ lib/fixtures/xxx.ts(fixtureSource)
↓ 変換
【変換】 lib/format/... または lib/db/...:NNN
関数シグネチャを実際に写す
↓ 型
【型】 XxxItem.field: 型(lib/types/index.ts:NNN)
↓ 絞込
【絞込】 lib/xxx/query.ts:NNN(純粋関数)/ app/api/xxx/route.ts:NNN
↓ 表示
【画面】 components/features/xxx/XxxScreen.tsx:NNN → 画面に出る文字列
【値の例】 〈出所つきの実値〉
CLAUDE.md §6-6)本書に出る具体的な値には、必ず次の3つのどれかの印を付けます(★画面1〜5 の文書と同じ印★)。 ★印の無い数字は本書には書きません★。
| 印 | 意味 | 出所 |
|---|---|---|
| ★シード由来★ | ★当方が作ったサンプルデータの値★。★DBの値ではない★ | lib/fixtures/*.ts / scripts/seed-local-db.ts |
| ★構造由来★ | ★型・DDL・API契約から読み取れる値★(データではなく器) | prisma/schema.prisma / d-watch-api.yaml / lib/config/index.ts |
| ★記録済み実測★ | ★docs/decisions.md / docs/handoff/ に日付つきで記録済みの実測値★ |
決定番号・ファイル名と日付を併記する |
★「実測」と書くときは出所を必ず併記する★(CLAUDE.md §6-6)。 出所を書けない数字は「実測」と呼びません。 ★先方投入の値は本書に1件もありません★ — 次項のとおり、そもそも先方由来の行が確認されていないためです。
133.125.238.144 / clean_watch は本番DBそのもの★(D-148 項1。docs/decisions.md:12007-12009)。 ★検証用の別DBではない★。docs/handoff/テストDBのデータ出所_20260917.md:76-98。2026-09-17)。 ★先方由来の行は1行も見つかっていない★(同 :98)。| 役割 | 実体 | 備考 |
|---|---|---|
| データソース切替 | lib/db/index.ts:2476 — export const db: DataSource = USE_FIXTURES ? fixtureSource : prismaSource; |
本書の5画面が使うのは fixtureSource(:481-519)/ prismaSource(:1427-1702) |
| ページング既定値 | PAGINATION = { defaultPerPage: 50, maxPerPage: 200 }(lib/config/index.ts:325-327)★構造由来★ |
画面は PAGINATION.defaultPerPage を送る |
| 一覧の共通形 | ListResponse<T> = { items, meta }(lib/types/index.ts:84-87)/ SummaryListResponse<TItem,TSummary> = { summary, items, meta }(:89-93) |
Meta = { total, page, per_page }(:20-24) |
| 分類の絞り込み | group_id / customer_id → parseMasterIdFilters()(lib/api/master-id.ts:42-66)→ resolveFacilityIdsByMasters()(lib/facilities/resolve-by-master.ts:88-93)→ filterRowsByFacilityIds()(lib/facilities/filter-by-ids.ts:48-55) |
★null=絞らない / []=0件★ |
| 日時の JST 補正 | jstIsoFromDb(date: Date): string(lib/format/date.ts:95-97)= toJstIso(fromDbJst(date)) |
★DATETIME は JST の壁時計値★。次項 |
| 日時の表示整形 | formatMd(iso)(lib/format/date.ts:8-12)/ formatHm(iso)(:15-17) |
文字列の切り出しのみ。TZ 変換しない |
DB: control_reports.reported_at = 2026-09-11 02:00:00 ← ★日本時間の壁時計値★(TeamWorks 第8回 A2)
↓ @prisma/adapter-mariadb が `new Date(`${value}Z`)` で ★UTC とみなす★
JS: Date(2026-09-11T02:00:00Z) ← ★真の瞬間より9時間先★
↓ fromDbJst(date) … lib/format/date.ts:67-69
JS: Date(2026-09-10T17:00:00Z) ← 真の瞬間
↓ toJstIso(date) … lib/format/date.ts:37-46
API: "2026-09-11T02:00:00+09:00" ← ★DBの生値と桁が一致するのが正解★
↓ formatMd + formatHm
画面: "9/11 02:00"
★この2段(fromDbJst → toJstIso)を1つの式に潰さないこと★(lib/format/date.ts:88-89)。 ★補正しないと深夜2時の清掃が翌朝でも「未来」と判定される★(同 :59-62)。
画面: app/(dashboard)/ds-logs/page.tsx:1-5 → components/features/ds-logs/DsLogScreen.tsx API: GET /api/ds-devices(app/api/ds-devices/route.ts:31-119)/ PATCH /api/ds-devices/{serial_number}/note(app/api/ds-devices/[serial_number]/note/route.ts:44-133) クエリ層: lib/ds-devices/query.ts データ層: lib/db/index.ts:1427-1475(prisma)/ lib/fixtures/ds-devices.ts(fixtures)
| 種別 | 名前 | 出所 |
|---|---|---|
| カード | 合計 / 正常 / 異常 | summarizeDsDevices() |
| 列 | シリアル / 施設名 / 接続状態 / 最終通信 / 備考 | DsDevice の5フィールド |
| ツールバー | ホールディングス / 屋号 / 検索入力 / 全て / 異常 / 正常 | クエリパラメータ |
【DBの列】 controls.status
型: Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:209)★構造由来★
意味: ★0 = 正常 / 1 = 異常★(D-065 Q1-2。docs/decisions.md:483)
★★3状態目(disconnected)は存在しない★★ — 「異常」=「生死報告が届いていない」
なので、通信断と異常は同一(lib/types/index.ts:831-836)
↓ 取得
【取得】 lib/db/index.ts:1433-1436
prisma.control.findMany({ where: { deleted_at: null }, include: { facility: true } })
★deleted_at IS NULL でのみ絞る。inactive では絞らない★(:1430-1432。D-086)
↓ 変換
【変換】 lib/db/ds-status.ts:20-26
export function connectionStatusFromControlStatus(status: number): DsConnectionStatus
status === 0 → "ok"
status === 1 → "error"
それ以外 → ★throw Error★(黙って握りつぶさない。:23-25)
↓ 型
【型】 DsDevice.connection_status: DsConnectionStatus
= "ok" | "error"(lib/types/index.ts:837 / :846)★構造由来★
↓ 絞込
【絞込】 lib/ds-devices/query.ts:93-107
filterDsDevicesByStatus(items: DsDevice[], status: DsDeviceStatusFilter): DsDevice[]
DsDeviceStatusFilter = "all" | "error" | "ok"(lib/types/index.ts:840)
↓ 並替
【並替】 lib/ds-devices/query.ts:118-130 sortDsDevices(items: DsDevice[]): DsDevice[]
CONNECTION_STATUS_ORDER = { error: 0, ok: 1 }(:109-112)→ ★異常が先★
同順位は serial_number 昇順(D-055)
↓ 表示
【画面】 components/features/ds-logs/DsLogScreen.tsx:30-34
function connectionStatusLabel(status: DsConnectionStatus): string
status === "ok" ? "正常" : "異常"
:36-39 connectionStatusColor(c, status) → status==="ok" ? c.ok : c.ng
:426-431 <Pill text={t(...)} color={...} />
【値の例】 ★構造由来★ 0 → "ok" → 「正常」(緑 #22c55e / light #16a34a。lib/theme.ts:12 / :31)
★構造由来★ 1 → "error" → 「異常」(赤 #ef4444 / light #dc2626。lib/theme.ts:16 / :35)
★シード由来★ DS-001 は "error"(lib/fixtures/ds-devices.ts:30)、DS-002 は "ok"(:22)
★記録済み実測★ `controls` は11行(docs/handoff/テストDBのデータ出所_20260917.md:91。2026-09-17。
★当方のシード由来★ — `password` が全行 `"seed-local-placeholder-password"`)
★★危ない★★: connectionStatusFromControlStatus() は ★0/1 以外で例外を投げる★ (lib/db/ds-status.ts:23-25)。★実データで status = 2 が来たら一覧全体が 500 になる★。 これは意図した設計(黙って握りつぶさない)だが、★D-065 Q1-2 の「0/1 だけ」が 実データで崩れたときの被害は一覧の全滅★である。
【DBの列】 control_reports.reported_at
型: DateTime @default(now()) @db.Timestamp(0)(prisma/schema.prisma:365)★構造由来★
↓ 取得
【取得】 lib/db/index.ts:1440-1444
prisma.controlReport.groupBy({
by: ["control_id"],
where: { control_id: { in: controls.map(c => c.id) } },
_max: { reported_at: true },
})
★行は運ばない(制御盤ごとの最大値だけ取る)★(:1437-1439)
↓ 突合
【突合】 lib/db/index.ts:1448
new Map(latest.map(g => [String(g.control_id), g._max.reported_at]))
★★キーは String() で揃える(★罠1★)★★ — BigInt をそのままキーにすると
`1n` / `"1"` / `1` が別キーになり、★全制御盤が「心拍0件」に見えて例外になる★
:1451-1458 心拍0件なら throw(「controls(id=N) に control_reports が1件もありません」)
↓ 変換
【変換】 lib/format/date.ts:95-97 jstIsoFromDb(date: Date): string
★TIMESTAMP だが DATETIME と同じ9時間ずれをすることを実測済み★
(lib/db/index.ts:1465-1467。指示書Y 段階3)
↓ 型
【型】 DsDevice.last_reported_at: string(ISO8601 +09:00。lib/types/index.ts:847)
↓ 表示
【画面】 DsLogScreen.tsx:41-43
function formatDateTime(iso: string): string { return `${formatMd(iso)} ${formatHm(iso)}`; }
:432 <td className={styles.tdDate}>{formatDateTime(item.last_reported_at)}</td>
【値の例】 ★シード由来★ DS-001: at(0,"03:42") → 実行日の "YYYY-MM-DDT03:42:00+09:00" → 画面「9/17 03:42」
(lib/fixtures/ds-devices.ts:31 / 生成は lib/fixtures/_helpers.ts:33-35)
★記録済み実測★ `control_reports` は14行、`serial_number` が `DS-001`〜
= ★当方の lib/fixtures/ds-devices.ts 由来★
(docs/handoff/テストDBのデータ出所_20260917.md:89。2026-09-17)
★★危ない★★: _max: { reported_at: true } を orderBy: { id: "desc" }, take: 1 に 書き換えたくなるが、★id の大小順と reported_at の大小順が一致する保証はどこにも無い★ (lib/db/index.ts:1421-1424)。★確認が取れるまで MAX(reported_at) を使うこと★。
【DBの列】 ★存在しない★(D-065 Q1-1。docs/decisions.md:482)
↓ 取得(Prisma側)
【取得】 lib/db/index.ts:1472 note: null ← ★常に null を返す★(:1469-1471 に明記)
↓ 実値の取得
【ストア】 lib/settings/ds-device-notes.ts
保存先: data/settings/ds-device-notes.json(:53-55 defaultDeviceNotesStorePath())
★/data は .gitignore:50 で除外★= ★コミットされず git のバックアップ対象外★(:20-22)
型: export type DeviceNotes = Record<string, string>(:44)
★空の備考はキーごと存在しない★(:40-43)
export async function readDeviceNotes(): Promise<DeviceNotes>(:124-126)
↓ マージ
【マージ】 app/api/ds-devices/route.ts:81-84
rows = rows.map(device => ({ ...device, note: notes[device.serial_number] ?? device.note }))
★独自ストアの値を優先する★(:71-77)— 逆にすると保存したそばから古い値に戻る
★フィルタ・ソート・サマリより前にマージする★(:78-80)
↓ 型
【型】 DsDevice.note: string | null(lib/types/index.ts:848)
↓ 表示
【画面】 DsLogScreen.tsx:86-132 NoteCell(インライン編集)
:123 {device.note ?? "-"} ← 未入力は "-"
:99-115 編集中は <textarea defaultValue={device.note ?? ""} maxLength={DEVICE_NOTE_MAX_LENGTH}
autoFocus onBlur={e => onCommit(e.target.value)} />
★非制御(:101-103)★ / ★確定は onBlur だけ(:84)★
【値の例】 ★シード由来★ DS-001: "現地確認済み。制御盤の電源ユニット交換を手配中"
(lib/fixtures/ds-devices.ts:32)
★構造由来★ 上限 200文字(DEVICE_NOTE_MAX_LENGTH。lib/config/index.ts:504)
★記録済み実測★ `data/settings/ds-device-notes.json` の現在の中身は `{}`(空。2026-09-17 時点。
★当方のローカル作業ディレクトリの実体★)
画面 DsLogScreen.tsx:258-296 commitNote(device: DsDevice, note: string): Promise<void>
:263 if (note === (device.note ?? "")) return; ← 変えていないなら叩かない
:271-278 apiFetch<DsDeviceNoteResponse>(
`/api/ds-devices/${encodeURIComponent(serial)}/note`,
{ method: "PATCH", headers: {...}, body: JSON.stringify({ note }) })
★DsDeviceNoteResponse は YAML にインライン定義で名前が無いため lib/types に置かず
画面に持つ(:47-57)★ { serial_number: string; note: string | null }
↓
API app/api/ds-devices/[serial_number]/note/route.ts:44-133
:54 parsePathSerialNumber(serialNumberRaw) ← ★① 形式★
:68-74 typeof note !== "string" → 422
:79-86 note.length > 200 → 422 ← ★② 長さ(トリム前で数える。:76-78)★
:98 await db.dsDeviceExists(serialNumber) ← ★③ 実在★
★★検証順序(形式 → 長さ → 実在)を変えないこと★★(:53)
:111-113 !exists → 404 not_found
:117 saved = await saveDeviceNote(serialNumber, note)
:132 return NextResponse.json({ serial_number: serialNumber, note: saved })
↓
ストア lib/settings/ds-device-notes.ts:137-155
export async function saveDeviceNote(serial: string, note: string): Promise<string | null>
trimmed === "" → delete notes[serial]; return null ← ★空文字送信=削除★
それ以外 → notes[serial] = trimmed; return trimmed
↓
画面 DsLogScreen.tsx:281-283
★返ってきた値で1行だけ差し替える(送った文字列ではない。:279-280)★
★保存後に fetchDevices() を呼ばない(:254-256)★ — 表がローディングに入れ替わり
編集していた行の位置を見失う
★★メインDBには一切書いていない★★ — 書き込み許可は robot_error_reports / control_error_histories の4カラムだけ(CLAUDE.md §3 / §7)で、 デバイスの備考はそのいずれでもない(lib/settings/ds-device-notes.ts:14-18)。
lib/db/index.ts:1509-1518
async dsDeviceExists(serialNumber: string)
prisma.control.findMany({ where: { deleted_at: null, serial_number: serialNumber },
select: { serial_number: true } })
return rows.some(row => row.serial_number === serialNumber) ← ★★JS の === で決める★★
★DBの既定の照合順序は大小文字を区別しない(ci)★(:1491)。SQL の一致をそのまま答えにすると:
ds-001 が DS-001 に当たって 404 → 200 → 保存キーはリクエストの生文字列 "ds-001"(lib/settings/ds-device-notes.ts) → 一覧は notes["DS-001"] で引く → ★★保存されたのに一生表示されない孤児行が残る★★
★findFirst ではなく findMany★(:1499-1500)/ ★take は付けない★(:1502)。
【計算】 lib/ds-devices/query.ts:147-152
export function summarizeDsDevices(items: DsDevice[]): DsDeviceSummary
interface DsDeviceSummary { total: number; ok: number; error: number }(:132-137)
【呼ぶ順】 app/api/ds-devices/route.ts
:92 rows = filterRowsByFacilityIds(rows, facilityIds) ← ★分類は summary の前★
:96 const summary = summarizeDsDevices(rows) ← ★q / status の前★
:97-100 filtered = filterDsDevicesByStatus(filterDsDevicesByQ(rows, q), status)
:101 sorted = sortDsDevices(filtered)
:108 const total = sorted.length ← ★meta.total★
【意味】 meta.total = 一覧(q・status 後)の件数。★ページング計算用★
summary.total = 分類で絞ったあとの全デバイス数。★q・status の影響を受けない★
(:104-107。絞り込みが無いときだけ一致する)
【画面】 DsLogScreen.tsx:306-308 StatCard に summary.total / summary.ok / summary.error
:176 const [total, setTotal] = useState(0) ← ★meta.total を入れる★(:168-175 に警告)
:452-457 <Pagination total={total} perPage={PAGINATION.defaultPerPage} .../>
【値の例】 ★シード由来★ dsDevices は12件、うち "error" は DS-001/005/009/010/012 の ★5件★、
"ok" は ★7件★(lib/fixtures/ds-devices.ts:16-116 を数えた)
→ 既定表示(status=all)で summary = { total: 12, ok: 7, error: 5 }、meta.total = 12
→ status=error に切り替えると summary は不変で meta.total だけ 5 になる
★★危ない★★: summary.total をページャに渡すと ★summary.total > meta.total になり、 存在しないページのボタンが出る★(D-054 / DsLogScreen.tsx:168-175)。 部品側にも同じ警告がある(components/ui/Pagination.tsx:33-39)。
DsLogScreen.tsx:200-211
const queryString = useMemo(() => {
const params = new URLSearchParams();
if (debouncedQ) params.set("q", debouncedQ);
params.set("status", statusFilter);
appendClassificationParams(params, classification.draft); // lib/masters/options.ts:218-229
params.set("per_page", String(PAGINATION.defaultPerPage));
params.set("page", String(page));
return params.toString();
}, [debouncedQ, statusFilter, classification.draft, page]);
:192-198)setPage(1)★(:332-334 / :376-377 ほか)appendClassificationParams() は ★>= 1 のときだけ set する★(lib/masters/options.ts:222-227) — ★group_id=0 を漏らさないため★(0 は「所属なし」のセンチネル。D-011)実際に出る URL の例 ★構造由来★: /api/ds-devices?q=%E5%B7%9D%E4%B8%8B&status=error&customer_id=2&per_page=50&page=1
lib/ds-devices/query.ts:83-90
export function filterDsDevicesByQ(items: DsDevice[], q: string): DsDevice[]
includesFoldName(item.facility_name, q) ← ★記号(中点・長音・空白・ハイフン類)も無視★
|| includesFoldPlain(item.serial_number, q) ← ★大文字小文字のみ★
★シリアルで記号を除くと桁の境界が消える("DS-001" 形式)★(:79-81。D-2)
画面: app/(dashboard)/ds-alerts/page.tsx:1-5 → components/features/ds-alerts/DsAlertsScreen.tsx API: GET /api/ds-alerts(app/api/ds-alerts/route.ts:33-92) クエリ層: lib/ds-alerts/query.ts データ層: lib/db/index.ts:1532-1561(prisma)/ lib/fixtures/ds-alerts.ts(fixtures)
★更新系(PATCH /ds-alerts/{id}/status)は作っていない★(app/api/ds-alerts/route.ts:7-9)。
| 種別 | 名前 |
|---|---|
| カード | 未対応 / 対応中 / 対応済み |
| アラートカード内 | 種別バッジ / シリアル / 施設名 / 清掃エリア名 / 検知日時 / メモ / 対応状況バッジ |
| ツールバー | ホールディングス / 屋号 / 種別セレクト / 未対応・対応中 / 全件 / 対応済み |
【DBの列】 ★存在しない★ — control_error_histories には種別の列が無い
(テーブル定義: prisma/schema.prisma:378-396。列は id / facility_id / section_id /
control_id / status / comment / occurred_at / responded_at / resolved_at のみ)★構造由来★
↓ 取得
【取得】 lib/db/index.ts:1537-1544
prisma.controlErrorHistory.findMany({
where: { control: { deleted_at: null } }, ← ★control_error_histories に deleted_at が無い★(:1535)
include: { control: true, facility: true, section: true } })
↓ 変換
【変換】 lib/db/index.ts:1548 alert_type: "disconnected" ← ★★ハードコード★★
理由: control_error_histories は制御盤異常(生死報告なし)しか持たない(D-067 項1)
↓ 型
【型】 DsAlert.alert_type: DsAlertType = "security_off" | "disconnected"
(lib/types/index.ts:851 / :861)★構造由来★
↓ 絞込
【絞込】 lib/ds-alerts/query.ts:87-93
filterDsAlertsByType(items: DsAlert[], type: DsAlertTypeFilter): DsAlert[]
DsAlertTypeFilter = "all" | "security_off" | "disconnected"(lib/types/index.ts:855)
↓ 表示
【画面】 DsAlertsScreen.tsx:31-34
const TYPE_LABEL: Record<DsAlertType, string> =
{ security_off: "セキュリティ無効", disconnected: "通信断" }
:36-38 function typeColor(c, type) { return type === "disconnected" ? c.wn : c.ng; }
:259 style={{ borderLeftColor: typeColor(c, item.alert_type) }} ← カード左端の線
:264 <Pill text={t(TYPE_LABEL[item.alert_type])} color={typeColor(c, item.alert_type)} />
【値の例】 ★構造由来★ "disconnected" → 「通信断」/ ★アンバー★ #f59e0b(dark。lib/theme.ts:14)
/ #d97706(light。lib/theme.ts:33)
★構造由来★ "security_off" → 「セキュリティ無効」/ 赤 #ef4444(lib/theme.ts:16)
★シード由来★ 12件中 security_off が6件・disconnected が6件(lib/fixtures/ds-alerts.ts:19-179)
★記録済み実測★ `control_error_histories` は5行(当方のシード由来。
`comment` が「回線業者に確認中」= `lib/fixtures/ds-alerts.ts:57`。
docs/handoff/テストDBのデータ出所_20260917.md:92。2026-09-17)
docs/decisions.md:11412。2026-09-16)disconnected しか出ないため画面は全部アンバーになり、赤が1色も出ない★ (docs/decisions.md:11423):11424)security_off が出ない理由は lib/db/index.ts:1519-1531: ★security_statuses / security_status_histories に id / device_serial / resolution_status / responded_at / resolved_at / memo に対応する列が無い★(D-090 項3-9)。 ★security_statuses.reported_at は @updatedAt なので detected_at も取れない★(D-090 項3-10)。 ★これは意図した挙動であり実装漏れではない★(:1529)。/dashboard のセキュリティバナーが出なくなる(:1527-1529)。【DBの列】 control_error_histories.status
型: Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:383)★構造由来★
意味: ★0 = 未対応 / 1 = 対応中 / 2 = 対応済★(D-065 Q1-3。docs/decisions.md:484)
★★書き込み許可4カラムの1つ★★(D-042。CLAUDE.md §3)
↓ 変換
【変換】 lib/db/ds-status.ts:36-49
export function resolutionStatusFromControlErrorStatus(status: number): DsAlertStatus
0 → "pending" / 1 → "in_progress" / 2 → "done" / それ以外 → ★throw★
逆変換(seed 用): controlErrorStatusFromResolutionStatus()(:52-60)
↓ 型
【型】 DsAlert.resolution_status: DsAlertStatus = "pending" | "in_progress" | "done"
(lib/types/index.ts:852 / :872)★構造由来★
↓ 絞込
【絞込】 lib/ds-alerts/query.ts:96-110
filterDsAlertsByStatus(items: DsAlert[], status: DsAlertStatusFilter): DsAlert[]
"open" → resolution_status !== "done" ← ★open = 未対応 + 対応中★
"done" → === "done"
"all" → 全部
DsAlertStatusFilter = "open" | "all" | "done"(lib/types/index.ts:857)
★画面の既定は "open"★(DsAlertsScreen.tsx:63)
↓ 表示
【画面】 DsAlertsScreen.tsx:277-291 3分岐で <span className={styles.statusBadge} ...>
"done" → 「対応済み」(styles.statusDone)
"in_progress" → 「対応中」(styles.statusInProgress)
"pending" → 「未対応」(styles.statusPending)
【値の例】 ★シード由来★ 12件の内訳: pending 6件(id 1,2,5,7,10,11)/ in_progress 4件(3,6,8,12)/
done 2件(4,9)(lib/fixtures/ds-alerts.ts:19-179)
→ summary = { pending: 6, in_progress: 4, done: 2 }
→ 既定(status=open)の meta.total = 10
★★D-079 項2 の「resolution_status の3値は2状態化の対象外」★★(docs/decisions.md:2133 / :3549-3551)— DsConnectionStatus は D-065 で3→2状態に縮めたが(D-076)、★こちらは3値のまま残す★。 理由は ★control_error_histories.status(0/1/2)に直結しているから★(docs/decisions.md:3551)。 ★2つの「status」を同じ話だと思って一緒に潰さないこと★。
| 画面の要素 | DBの列 | 変換 | 型 | 値の例 | |
|---|---|---|---|---|---|
| シリアル | controls.serial_number(prisma/schema.prisma:202。@db.VarChar(100))★構造由来★ |
lib/db/index.ts:1549 row.control.serial_number |
DsAlert.device_serial: string(lib/types/index.ts:862) |
★シード由来★ "DS-001"(lib/fixtures/ds-alerts.ts:23) |
|
| 施設名 | facilities.facility_name(:129。@db.VarChar(400))★構造由来★ |
lib/db/index.ts:1551 |
facility_name: string(:864) |
★シード由来★ "メガバリューマート中央本店"(lib/fixtures/ds-alerts.ts:25) |
|
| 清掃エリア名 | sections.section_name(:171。@db.VarChar(100))★構造由来★ |
lib/db/index.ts:1552 |
section_name: string(:870) |
★シード由来★ "1F売り場"(lib/fixtures/ds-alerts.ts:26)。★fixtures 側は暫定ダミー★(lib/types/index.ts:865-868) |
|
| 検知日時 | control_error_histories.occurred_at(:385。@db.DateTime(0))★構造由来★ |
jstIsoFromDb()(lib/db/index.ts:1553) |
detected_at: string(:871) |
★シード由来★ at(0,"03:42") → 画面「9/17 03:42」 |
|
| メモ | control_error_histories.comment(:384。@db.Text、nullable)★構造由来★ |
lib/db/index.ts:1559 そのまま |
`memo: string \ | null(:875`) |
★シード由来★ "回線業者に確認中"(lib/fixtures/ds-alerts.ts:57)。★null なら行ごと出さない★(DsAlertsScreen.tsx:274) |
| (画面に無い) | responded_at / resolved_at(:386 / :387。nullable)★構造由来★ |
row.x ? jstIsoFromDb(row.x) : null(lib/db/index.ts:1556-1557) |
`string \ | null(:873-874`) |
★型とAPIにはあるが F-07 の画面には出ていない★ |
lib/ds-alerts/query.ts:113-120
export function sortDsAlerts(items: DsAlert[]): DsAlert[]
detected_at の降順(文字列比較)。同値なら id 昇順(D-052 と同じ規則)
★ISO8601 の固定長文字列なので辞書順=時刻順★
lib/ds-alerts/query.ts:136-145
export function summarizeDsAlerts(items: DsAlert[]): DsAlertSummary
interface DsAlertSummary { pending: number; in_progress: number; done: number }(:122-126)
app/api/ds-alerts/route.ts
:64 const scoped = filterRowsByFacilityIds(rows, facilityIds) ← ★分類は summary の前★
:69 const summary = summarizeDsAlerts(scoped) ← ★type / status の前★
:70-73 filtered = filterDsAlertsByStatus(filterDsAlertsByType(scoped, type), status)
:81 const total = sorted.length
★★危ない★★: summary をフィルタ後に計算すると ★既定の status=open のせいで 「対応済み」が常に0★になる(app/api/ds-alerts/route.ts:66-68。F-03 で実際に起きた事故。D-054)。
画面: app/(dashboard)/facilities/page.tsx:1-5 → components/features/facilities/FacilitiesScreen.tsx API: GET /api/facilities(app/api/facilities/route.ts:32-81)★GET のみ★ クエリ層: lib/facilities/query.ts データ層: lib/db/index.ts:1611-1662(prisma)/ lib/fixtures/facilities.ts(fixtures)
★登録・更新ボタンは置かない★(FacilitiesScreen.tsx:18-22)— モックにはあるが onClick が無い飾りで、実装すると「押せるのに何も起きないUI」になる(D-053)。 ★summary は無い(items と meta だけ)★(app/api/facilities/route.ts:8-9)。
施設名 / 都道府県 / 住所 / 電話番号 / 責任者名 / 担当者名 / メール / ★監視★ / ★D-Sec★ / ロボット(メーカー・機種・シリアル + 未対応バッジ) (FacilitiesScreen.tsx:126-135 が見出し、:141-185 が本体)
【DBの列】 facilities.contract
型: String @default("0000000000") @db.VarChar(10)(prisma/schema.prisma:139)★構造由来★
意味: ★★桁位置がサービスに対応する★★(D-080。CLAUDE.md §4.6)
★1桁目(index 0)= D-Security / 2桁目(index 1)= D-Watch★
契約済み(有効)なサービスの桁を "1" にする
★3桁目以降は将来のサービス用の予備と読める(定義書に明記なし。P1では読まない)★
↓ 桁位置の定義
【設定】 lib/config/index.ts:633-636
export const CONTRACT_DIGITS = { dSecurity: 0, dWatch: 1 } as const
★桁位置の数字をこのファイル以外に直書きしないこと★(:631)
↓ 変換(★唯一の消費者★)
【変換】 lib/format/contract.ts:26-31
export function parseContract(contract: string): ContractFlags
interface ContractFlags { monitoring_enabled: boolean; d_security_enabled: boolean }(:13-16)
d_security_enabled: contract[CONTRACT_DIGITS.dSecurity] === "1"
monitoring_enabled: contract[CONTRACT_DIGITS.dWatch] === "1"
★文字の完全一致だけで判定する(:8)★
★桁が足りない文字列・空文字・10桁超でも例外を投げない★(:23-24)—
存在しない桁は `undefined === "1"` が false になり自然に「契約なし」扱い
逆変換(seed 用): buildContract(flags): string(:36-41)
↓ 呼び出し
【取得】 lib/db/index.ts:1632
const { monitoring_enabled, d_security_enabled } = parseContract(row.contract);
↓ 型
【型】 Facility.monitoring_enabled: boolean(lib/types/index.ts:955。宣言は :948-954)
Facility.d_security_enabled: boolean(:960。宣言は :956-959)
↓ 表示
【画面】 FacilitiesScreen.tsx:148-154 監視列
{facility.monitoring_enabled ? <span className={styles.markOn}>○</span>
: <span className={styles.markOff}>ー</span>}
:155-161 D-Sec列(markOnSec / markOff)
【値の例】 ★構造由来★ "0000000000" → { monitoring: false, dSecurity: false } → 「ー / ー」
★構造由来★ "1000000000" → { monitoring: false, dSecurity: true } → 「ー / ○」
★構造由来★ "0100000000" → { monitoring: true, dSecurity: false } → 「○ / ー」
★構造由来★ "1100000000" → { monitoring: true, dSecurity: true } → 「○ / ○」
★シード由来★ 施設1「メガバリューマート中央本店」= 両方 true(lib/fixtures/facilities.ts:53-54)
★シード由来★ 施設11「バリューマート川下店」= D-Securityのみ(:253-254)
★シード由来★ 施設12「フレッシュタウン湖畔店」= 未設定(:276-277)
★記録済み実測★ `facilities` は12行で、★`contract` を含む全列が lib/fixtures/facilities.ts と一致★
= ★当方のシード由来★(docs/handoff/テストDBのデータ出所_20260917.md:90。2026-09-17)
// ❌ 絶対にやってはいけない
Number("0000000000") === 0 // ★例外を出さずに 0 になる★
facility.contract === 3 // ★絶対に真にならない★
★帰結★: ★全施設が「契約なし」になりメニューが全部消えるが、例外もテスト失敗も出ない★ (CLAUDE.md §4.6 / lib/format/contract.ts:4-6 / lib/types/index.ts:952-953)。
★もう1つ★: customers.contract は ★削除依頼済みで参照禁止★(D-010)。 prisma/schema.prisma の model Customer(:94-120)に ★そもそも列が無い★ (lib/db/index.ts:1583-1584 に明記)。
★もう1つ★: clean_schedules にも contract 列がある(prisma/schema.prisma:563。 @default("0000000000") @db.VarChar(10))★構造由来★ が、★アプリは一切読んでいない★ (lib/db/index.ts:1678-1701 の schedules() に contract が出てこない)。 ★facilities.contract のみが正★(CLAUDE.md §4.6)なので現状は正しいが、 ★同じ名前・同じ型の列が2つあることは覚えておくこと★。
【DBの列】 facilities.addr1 型: String @db.VarChar(800)(prisma/schema.prisma:133)★構造由来★
facilities.addr2 型: String? @db.VarChar(800)(:134。★nullable★)★構造由来★
↓ 変換
【変換】 lib/format/address.ts:7-10
export function formatAddress(addr1: string, addr2: string | null): string
if (!addr2) return addr1;
return `${addr1}${addr2}`; ← ★区切り文字を付けない★(:4-5)
↓ 呼び出し
【取得】 lib/db/index.ts:1648 address: formatAddress(row.addr1, row.addr2)
↓ 型
【型】 Facility.address: string(lib/types/index.ts:943。★null にならない★)
↓ 絞込
【絞込】 lib/facilities/query.ts:108 includesFoldPlain(item.address, q)
★住所は大文字小文字のみ折りたたむ(記号は残す)★(:93-96)—
★ハイフンは番地の区切りなので、除くと "23" が "1-2-3" に誤ヒットする★
↓ 表示
【画面】 FacilitiesScreen.tsx:143 <td className={styles.tdMuted}>{facility.address}</td>
【値の例】 ★シード由来★ "東京都○○区1-2-3"(lib/fixtures/facilities.ts:48)
★構造由来★ addr1="東京都○○区1-2-3" / addr2=null → "東京都○○区1-2-3"
★構造由来★ addr1="東京都○○区1-2-3" / addr2="○○ビル5F" → "東京都○○区1-2-3○○ビル5F"
【DBの列】 robots.serial_number 型: String @db.VarChar(20)(prisma/schema.prisma:231)★構造由来★
devices.maker_id 型: Int @db.UnsignedTinyInt(:82。★0〜255★)★構造由来★
★makers テーブルは提供されない★(D-057 A-1。:79-81)
devices.model_name 型: String @db.VarChar(100)(:83)★構造由来★
↓ 取得
【取得】 lib/db/index.ts:1617-1630
prisma.facility.findMany({ where: { deleted_at: null },
include: { robots: { where: { deleted_at: null }, include: { device: true } },
customer: { include: { group: true } } } })
★customer / group は deleted_at で絞らない★(:1626-1627)—
絞ると論理削除された屋号に属する施設が静かに落ちる
↓ 変換
【変換】 lib/db/index.ts:1655-1660
robots: row.robots.map(robot => ({
id: Number(robot.id),
maker: MAKER_NAMES[robot.device.maker_id] ?? String(robot.device.maker_id), ← :1657
model: robot.device.model_name,
serial_number: robot.serial_number,
}))
MAKER_NAMES: Record<number, string> = { 1: "PUDU", 2: "Keenon" }(lib/config/index.ts:597-600)
★★未知の番号は String(maker_id) をそのまま返す。"不明" 等に潰さない★★(:593-595。D-077 と同じ思想)
↓ 型
【型】 RobotSummary { id: number; maker: string; model: string; serial_number: string }
(lib/types/index.ts:917-922)
Facility.robots: RobotSummary[](:961)
↓ 表示
【画面】 FacilitiesScreen.tsx:162-185
<Pill text={robot.maker} color={c.pr} /> ← :165(青 #3b82f6。lib/theme.ts:10)
<span className={styles.robotModel}>{robot.model}</span> ← :166
{!isSupportedModel(robot.maker, robot.model) && <Pill text={t("未対応")} color={c.wn} />} ← :179-181
<span className={styles.robotSerial}>{robot.serial_number}</span> ← :182
↓ 未対応判定
【判定】 lib/format/robot-model.ts:36-45
export function isSupportedModel(maker: string, model: string): boolean
normalize(v) = v.trim().toUpperCase()(:28-30)
ROBOT_MODELS.some(e => e.supported && normalize(e.maker)===M && normalize(e.model)===Mo)
ROBOT_MODELS(lib/config/index.ts:620-625)★構造由来★:
{ maker:"PUDU", model:"CC1", supported: true }
{ maker:"PUDU", model:"MT1", supported: false }
{ maker:"Keenon", model:"C30", supported: false }
{ maker:"Keenon", model:"C40", supported: false }
【値の例】 ★シード由来★ 施設1: maker "PUDU" / model "CC1" / serial "8111R5628060011"
(lib/fixtures/facilities.ts:56)→ 未対応バッジ ★出ない★
★構造由来★ maker_id=1 / model_name="MT1" → 「PUDU」「MT1」+ ★「未対応」バッジ★
★構造由来★ maker_id=9(MAKER_NAMES に無い)→ ★"9" がそのまま Pill に出る★ + 未対応バッジ
★シード由来★ 施設7 は ★3台★(id 107 / 108 / 109。lib/fixtures/facilities.ts:134-136)
★シード由来★ 施設12 は ★0台★(robots: []。:278)
★記録済み実測★ `robots` は14行、`devices` は1行(maker_id=1 / model_name="CC1")
= ★当方由来★(docs/handoff/テストDBのデータ出所_20260917.md:87 / :96。2026-09-17)
lib/i18n.ts:161 に 未対応: "Unsupported" がある。 ★D-Secアラート(F-07)の「未対応」(=まだ対応していない)★ も同じキーを引くので、 ★英語表示にすると Unsupported(非対応)と出続ける★。 ★ユーザー判断は「そのまま(直さない)」★(D-143 §3 #1。docs/decisions.md:11408。 理由は「英語を配布するか未定」)。★英語を配布すると決めた時点で必ず直す必要がある★(同 :11417-11418)。
isSupportedModel() は ★CLAUDE.md §9.6(判定結果はAPIから受け取る)の例外ではない★ — DFA側の静的な対応表を引くだけで、APIレスポンスに依存する判定ではない (FacilitiesScreen.tsx:172-177 に理由が書いてある)。
【DBの列】 facilities.phone String? @db.VarChar(16)(prisma/schema.prisma:135)★構造由来★
facilities.manager_name String? @db.VarChar(100)(:137)★構造由来★
facilities.charger_name String? @db.VarChar(100)(:138)★構造由来★
facilities.email String? @db.VarChar(100)(:136)★構造由来★
↓ 型
【型】 Facility.phone / manager_name / charger_name / email: string | null
(lib/types/index.ts:944-947)
↓ 表示
【画面】 FacilitiesScreen.tsx:29-31
function orDash(value: string | null): string { return value ?? "-"; }
:144-147 orDash(facility.phone) / (manager_name) / (charger_name) / (email)
★空セルにすると列がずれて見える★(:24-25)
【値の例】 ★シード由来★ 施設10「フレッシュタウン大和店」は phone / manager_name / charger_name / email が
★全部 null★(lib/fixtures/facilities.ts:228-231)→ 画面は「- - - -」
★シード由来★ 施設12 は charger_name のみ null(:274)
★申し送り★: facilities.email は @db.VarChar(100) だが facility_contacts.email は varchar(255) のままで食い違っている (prisma/schema.prisma:60-61 のコメント。2026-09-09 現行DDL)★構造由来★。
【DBの列】 facilities.customer_id BigInt @db.UnsignedBigInt(prisma/schema.prisma:124。NOT NULL)★構造由来★
customers.group_id BigInt @default(0) @db.UnsignedBigInt(:96。★NOT NULL★)★構造由来★
★0 = ホールディングスなしのセンチネル★(D-011)
↓ 変換
【変換】 lib/format/group-id.ts:77-79
export function groupIdFromDb(groupId: number): number | null
return groupId === GROUP_ID_NONE ? null : groupId;
GROUP_ID_NONE = 0(lib/config/index.ts:651)★構造由来★
★★Number() で BigInt を落としてから渡すこと★★(:73-75)—
★`0n === 0` は false★ なので BigInt のままだと静かに外れる
↓ 呼び出し
【取得】 lib/db/index.ts:1636 const group_id = groupIdFromDb(Number(row.customer.group_id));
:1646 group_name: group_id === null ? null : (row.customer.group?.group_name ?? null)
★「所属なし」のときは名前も null★(:1643-1645)
↓ 型
【型】 Facility.customer_id: number(lib/types/index.ts:931。★null にならない★)
Facility.group_id: number | null(:939)
Facility.group_name: string | null(:941)
↓ 使われ方
【用途】 ★F-08 の表には出ていない★(FacilitiesScreen.tsx:126-135 に列が無い)。
★分類の絞り込み(他画面のセレクト)と /customers・/groups の選択肢のために持っている★
【値の例】 ★シード由来★ 施設1: customer_id=1 / group_id=1 / group_name="バリューホールディングス"
(lib/fixtures/facilities.ts:43-46)
★シード由来★ 施設12: customer_id=4 / ★group_id=null / group_name=null★(:265-269)
★記録済み実測★ 本番DBの `customers` は1行(`customer_name = "テスト顧客001"`)/
`groups` は1行(`group_name = "テストグループ001"`)=
★旧版の当方シード★(docs/handoff/テストDBのデータ出所_20260917.md:94-95。2026-09-17)
★★危ない★★: ★=== 0 をルート・fixtures・DataSource に散らさないこと★ — 1箇所直し忘れると ★「グループ0」という存在しないホールディングスが画面に出る★ (lib/format/group-id.ts:54-57。★例外もテスト失敗も出ない★)。
lib/facilities/query.ts:102-116 filterFacilitiesByQ(items: Facility[], q: string): Facility[]
★OR は q の内部だけ★:
includesFoldName(facility_name, q) ← 記号も無視
|| includesFoldName(prefecture, q) ← 記号も無視
|| includesFoldPlain(address, q) ← 大文字小文字のみ
|| robots.some(r => includesFoldPlain(r.serial_number|maker|model, q))
★robots が空配列でも落ちない★(:98-100。some() は空配列に false)
lib/facilities/query.ts:141-162 filterFacilitiesByFields(items, query): Facility[]
facility_name / serial / address は ★AND★(:121-122)
★この3つはどの画面からも送られていない(API直叩き専用)★(:137-139)
lib/facilities/query.ts:174-176 sortFacilities(items): Facility[] → ★id 昇順★
★安定化のためだけの規則で優先度ではない★(:167-172)
lib/facilities/query.ts:190-201 paginateFacilities(items, page, perPage): ListResponse<Facility>
★meta.total は「フィルタ後・ページング前」の件数★(:181-186)
★★危ない★★: 分類の突き合わせは ★facility_id ではなく id★ (app/api/facilities/route.ts:62-67 / lib/facilities/filter-by-ids.ts:70-77)— ★このAPIが返すのは「施設の行そのもの」だから★。
画面: app/(dashboard)/schedules/page.tsx:1-5 → components/features/schedules/SchedulesScreen.tsx API: GET /api/schedules(app/api/schedules/route.ts:35-89)★GET のみ★ クエリ層: lib/schedules/query.ts データ層: lib/db/index.ts:1664-1702(prisma)/ lib/fixtures/schedules.ts(fixtures) 整形: lib/format/schedule.ts
★完全な参照専用★(SchedulesScreen.tsx:13-18)。★summary は無い★(app/api/schedules/route.ts:7-9)。
シリアル / 施設名 / ★曜日★ / ★時間★ / ★有効★(SchedulesScreen.tsx:128-132 / :143-157)
【DBの列】 clean_schedules.weeks
型: String @db.VarChar(7)(prisma/schema.prisma:556)★構造由来★
意味: ★「日月火水木金土」の7桁。清掃する曜日が "1"★
(lib/fixtures/schedules.ts:6-10 / lib/format/schedule.ts:4-5)
★★起点は日曜★★ — この起点をフロントと2箇所に持たないこと(同 :5)
↓ 取得
【取得】 lib/db/index.ts:1694-1695
weekdays: row.weeks, ← ★7桁をそのまま API にも載せる★
weekdays_label: weekdaysLabel(row.weeks),
★カラム名が違う(DB は weeks / API は weekdays)★(lib/schedules/query.ts:21-22)
↓ 変換
【変換】 lib/format/schedule.ts:17-28
export function weekdaysLabel(weeks: string): string
const WEEKDAY_CHARS = ["日","月","火","水","木","金","土"] as const(:8)
switch (weeks) {
case "1111111": return "毎日";
case "0111110": return "平日のみ";
case "1000001": return "土日のみ";
}
const days = WEEKDAY_CHARS.filter((_, i) => weeks[i] === "1");
return days.length > 0 ? days.join("") : "なし";
★桁が足りない文字列でも例外を投げない★(:15)
★該当0なら "なし"(空文字を返すと不具合と区別できない)★(:14)
↓ 型
【型】 Schedule.weekdays: string(7桁。lib/types/index.ts:969-970)
Schedule.weekdays_label: string(:971)
↓ 表示
【画面】 SchedulesScreen.tsx:145-146
{/* サーバー生成のラベルをそのまま出す。7桁の weekdays は見ない */}
<td className={styles.td}>{item.weekdays_label}</td>
★weekdays(7桁)を画面で解釈しないこと★(:28-31)
【値の例】 ★構造由来★ "1111111" → 「毎日」(特例)
★構造由来★ "0111110" → 「平日のみ」(特例)
★構造由来★ "1000001" → 「土日のみ」(特例)
★構造由来★ "0101010" → 「月水金」(一般。特例に当たらない)
★構造由来★ "1010101" → 「日火木土」(一般)
★構造由来★ "0000000" → 「なし」
★構造由来★ "010" → 「月」(桁不足でも落ちない)
★シード由来★ id23 = "0101010" → "月水金"(lib/fixtures/schedules.ts:88-89)
★シード由来★ id25 = "0111110" → "平日のみ"(:113-114)
★シード由来★ id30 = "1010101" → "日火木土"(:177-178)
weekdaysLabel() は7桁ビット列 128通りを扱う★★"1111111" / "0111110" / "1000001")。filter + join の一般処理★で、★「日火木土」のような読みにくいラベルが出る★。:15)。 例: "abcdefg" → 「なし」、"2222222" → 「なし」。【DBの列】 clean_schedules.start_time 型: String @db.VarChar(4)(prisma/schema.prisma:558)★構造由来★
★HHMM 形式。0000〜2359★(TeamWorks 回答 B-5。:557)
clean_schedules.end_time 型: String @db.VarChar(4)(:561)★構造由来★
★★時が 24〜29 を取りうる★★(:559-560。例: "2830" は翌日4:30)
★日跨ぎで翌6:00 まで設定可能にするための表現★
★HH:MM として素朴にパースすると壊れる★
↓ 変換
【変換】 lib/format/schedule.ts:35-42
export function formatStartTime(hhmm: string): string
hour = Number.parseInt(hhmm.slice(0,2), 10); minute = hhmm.slice(2,4);
if (!(hour >= 0 && hour <= 23)) throw new RangeError(...) ← ★0〜23 のみ★
return `${pad2(hour)}:${minute}`;
lib/format/schedule.ts:51-61
export function formatEndTime(hhmm: string): string
if (hour >= 24 && hour <= 29) return `翌${pad2(hour - 24)}:${minute}`; ← ★24を引く★
if (hour >= 0 && hour <= 23) return `${pad2(hour)}:${minute}`;
throw new RangeError(...) ← ★30時以上は例外★
↓ 呼び出し
【取得】 lib/db/index.ts:1696 start_time: formatStartTime(row.start_time)
lib/db/index.ts:1699 end_time: formatEndTime(row.end_time)
★整形は db.schedules()(データ層)が済ませてから Schedule を返す約束★
(lib/schedules/query.ts:28-29。★クエリ層は一切関与しない★)
↓ 型
【型】 Schedule.start_time: string / end_time: string
(lib/types/index.ts:972-973。★整形済み文字列。D-060★)
↓ 表示
【画面】 SchedulesScreen.tsx:147-150
{/* 整形済みの文字列を並べるだけ(日跨ぎは「23:30 - 00:30」等) */}
<td className={styles.td}>{item.start_time} - {item.end_time}</td>
★end_time をパースしないこと★(:33-36)
【値の例】 ★構造由来★ start "0200" → "02:00" / end "0300" → "03:00" → 画面「02:00 - 03:00」
★構造由来★ end "2430" → ★"翌00:30"★ → 画面「23:30 - 翌00:30」
★構造由来★ end "2830" → ★"翌04:30"★
★構造由来★ end "3000" → ★RangeError(値域外なので静かに変な表示にせず止める)★
★構造由来★ start "2400" → ★RangeError(start は 0000〜2359 のみ)★
★シード由来★ id27(施設5・日跨ぎ)は fixtures 側が ★"00:30"★(lib/fixtures/schedules.ts:141)
★★DB 版は "翌00:30"★★ — ★この1件だけ意図的に不一致で、DB版が正しい★
(docs/decisions.md:2888-2892。D-086 項10。2026-09-08)
fixtures: end_time = "00:30" (lib/fixtures/schedules.ts:141。★D-060 より前の書き方★) シード : end_time = "2430" を DB に書く(scripts/seed-local-db.ts。docs/decisions.md:2890) API(DB) : end_time = "翌00:30"
★fixtures の "00:30" をそのまま DB に書くと「開始より前に終わる」矛盾したデータになる★ (docs/decisions.md:2888-2890)。★USE_FIXTURES を切り替えると id27 の表示だけが変わる★。
【DBの列】 clean_schedules.inactive
型: Int @default(0) @db.UnsignedTinyInt(prisma/schema.prisma:564)★構造由来★
意味: ★★0 = 有効★★(inactive なので反転している)
↓ 変換
【取得】 lib/db/index.ts:1700
active: row.inactive === 0,
★★`Boolean(row.inactive)` と書かないこと★★(:1697-1698 / lib/schedules/query.ts:15-19)
★それでは全件反転する(有効なスケジュールが「無効」として並ぶ)★
↓ 型
【型】 Schedule.active: boolean(lib/types/index.ts:974)
↓ 絞込
【絞込】 ★active では絞り込まない★(lib/schedules/query.ts:130-133 / app/api/schedules/route.ts:67)
★一覧は有効・無効の両方を返す★ — API 側で無効行を落とすと
「登録したはずのスケジュールが消えた」ように見える。YAML にも active パラメータは無い
↓ 表示
【画面】 SchedulesScreen.tsx:138-142 行全体のクラス(無効は styles.rowInactive を追加)
:151-157 <td className={... item.active ? styles.activeOn : styles.activeOff}>
{item.active ? "●" : "○"}
【値の例】 ★構造由来★ inactive=0 → active=true → 「●」
★構造由来★ inactive=1 → active=false → 「○」+ 行が薄く(rowInactive)
★シード由来★ id24(施設6)だけ active=false(lib/fixtures/schedules.ts:104)。他の12件は true
★記録済み実測★ `clean_schedules` は21行(id 21〜32 が fixtures、9001〜9009 がシードの増量分)
= ★当方由来★(docs/handoff/テストDBのデータ出所_20260917.md:84。2026-09-17)
★もう1つの反転の罠★: deleted_at と inactive は別物。 ★deleted_at IS NULL でのみ絞り、inactive では絞らない★(lib/db/index.ts:1668。D-086 項9。 ★これは仕様が未確定な箇所★ — docs/decisions.md:2883-2887)。
【DBの列】 robots.serial_number(prisma/schema.prisma:231。@db.VarChar(20))★構造由来★
★clean_schedules は section_id を持つだけで serial_number は持たない★(:555)
↓ 取得
【取得】 lib/db/index.ts:1667-1677
prisma.cleanSchedule.findMany({ where: { deleted_at: null },
include: { facility: true,
section: { include: { robots: { where: { deleted_at: null } } } } } })
↓ 変換
【変換】 lib/db/index.ts:1683-1691
const robot = row.section.robots[0]; ← ★★[0] 決め打ち★★
if (!robot) throw new Error(
`[db] schedules: clean_schedules(id=${row.id}) の section(id=${row.section_id}) に
ロボットが見つかりません。P1は1清掃エリア=1ロボットの前提です。`);
serial_number: robot.serial_number
↓ 型
【型】 Schedule.serial_number: string(lib/types/index.ts:966)
↓ 絞込
【絞込】 lib/schedules/query.ts:118-125 includesFoldPlain(item.serial_number, q)
★serial は横断検索しない★(:135-137)— 1レコード1シリアルだから
↓ 表示
【画面】 SchedulesScreen.tsx:143 <td className={styles.tdSerial}>{item.serial_number}</td>
【値の例】 ★シード由来★ id21 = "8111R5628060011"(lib/fixtures/schedules.ts:60)
★構造由来★ section にロボットが0台 → ★例外 → 一覧全体が 500★
| 施設12 のロボット | 施設12 のスケジュール | |
|---|---|---|
lib/fixtures/facilities.ts:278 |
★robots: [](0台)★ |
— |
lib/fixtures/schedules.ts:210-219 |
— | ★id33 が存在する(serial 8111R5628060166)★ |
| シード(DB) | — | ★id33 を構造的に除外している★(docs/decisions.md:2880-2882。D-086 項8。実測でスキップは1件) |
★fixtures 単体では矛盾している★(0台の施設にスケジュールがある)が、 ★fixtures 経路は db.schedules() が fixtures 配列をそのまま返すだけ★(lib/db/index.ts:518) なので ★robots[0] の例外を踏まない★。 ★Prisma 経路では id33 が DB に無いので、やはり例外を踏まない★。 ★どちらの経路でも動くが、理由が違う★。★施設12 にロボットを1台足すと fixtures 側だけ挙動が変わる★。
lib/schedules/query.ts:173-175 sortSchedules(items): Schedule[] → ★id 昇順★
★曜日や時刻で並べない★(:167-171)— 「翌04:30」のような整形済み文字列では意図した順序にならない
lib/schedules/query.ts:190-197 paginateSchedules(items, page, perPage): Schedule[]
★ルートで slice を直書きせず関数に出してある★(:180-186)— テストが本番と同じ関数を叩けるように
app/api/schedules/route.ts:80-86
const total = sorted.length; ← meta.total
const items = paginateSchedules(sorted, page, perPage);
body: ListResponse<Schedule> = { items, meta: { total, page, per_page: perPage } }
画面: app/(dashboard)/system/page.tsx:1-5 → components/features/system/SystemScreen.tsx API: GET/PATCH /api/settings/advice(app/api/settings/advice/route.ts)/ GET/PATCH /api/settings/display-limits(app/api/settings/display-limits/route.ts) ストア: lib/settings/advice.ts / lib/settings/display-limits.ts / lib/settings/json-store.ts
★★この画面はメインDBを1行も読まない★★ — db.* の呼び出しが1つも無い。 値の出所は ★lib/config/(既定値)★ と ★data/settings/*.json(DFA独自ストア)★ だけ。
| セクション | 出所 | 編集 |
|---|---|---|
| 事前チェック設定 | PRECHECK_MINUTES_BEFORE(lib/config/index.ts:214) |
★不可★ |
| 完了率の判定閾値 | GRADE_THRESHOLDS(:18-25) |
★不可★ |
| アドバイス エスカレーション閾値 | ESCALATION_THRESHOLDS(:27-32) |
★不可★ |
| ★連続日数の表示上限★ | GET /api/settings/display-limits |
★★可(この画面で唯一)★★ |
| アドバイス文言 | GET /api/settings/advice |
文言のみ可(画面には編集UIなし・表示のみ) |
【設定】 lib/config/index.ts:214
export const PRECHECK_MINUTES_BEFORE = [60, 30] as const ★構造由来★
★1清掃につき2回で固定と確定している★(ユーザー回答 2026-09-11。:209-212)
↓ 直接 import
【画面】 SystemScreen.tsx:39-43 import { ..., PRECHECK_MINUTES_BEFORE } from "@/lib/config"
:270-279
{PRECHECK_MINUTES_BEFORE.map((minutes, i) => (
<dt>{t("第{n}回チェック").replace("{n}", String(i + 1))}</dt>
<dd>{t("{n}分前").replace("{n}", String(minutes))}</dd>
))}
:264 <span className={styles.lockBadge}>{t("編集不可")}</span>
【値の例】 ★構造由来★ [60, 30] → 「第1回チェック / 60分前」「第2回チェック / 30分前」
★ここだけ API を通さない理由★: 判定に使う閾値で、★編集できない★から (SystemScreen.tsx:6-9 / :11-14)。★動かないトグル・入力欄・保存ボタンは置かない★。
【設定】 lib/config/index.ts:18-25
export const GRADE_THRESHOLDS = { excellent: 90, good: 70, poor: 10 } as const ★構造由来★
★★境界は「上のランクに入る」★★(D-139。2026-09-16)
★× の境界だけは「未満」のまま★(10.0 は △)
↓ 直接 import
【画面】 SystemScreen.tsx:90-98
const GRADE_RULES: { grade: Grade; condition: string }[] = [
{ grade: "◎", condition: `完了率 ${GRADE_THRESHOLDS.excellent}% 以上` },
{ grade: "○", condition: `完了率 ${GRADE_THRESHOLDS.good}% 以上` },
{ grade: "△", condition: `完了率 ${GRADE_THRESHOLDS.poor}% 以上` },
{ grade: "×", condition: `上記未満(完了率 ${GRADE_THRESHOLDS.poor}% 未満)、または未実施` },
];
Grade = "◎" | "○" | "△" | "×"(lib/types/index.ts:17)
:292-299 <GradeBadge grade={rule.grade} /> + 条件文
:300-304 注記「判定は丸める前の生値で行います(表示は小数第1位を四捨五入した整数)。
境界値ちょうどは上位の判定になります。」
【値の例】 ★構造由来★ 「完了率 90% 以上」「完了率 70% 以上」「完了率 10% 以上」
「上記未満(完了率 10% 未満)、または未実施」
★★危ない★★: ★lib/judge/grade.ts の比較(>=)と文言を食い違わせないこと★ (SystemScreen.tsx:87-88)。★旧規則(「超」/ 下位の判定)の文言に戻さないこと★(:27。D-020b は破棄)。
【設定】 lib/config/index.ts:27-32
export const ESCALATION_THRESHOLDS = { level2Days: 2, level3Days: 3 } as const ★構造由来★
↓ 直接 import
【画面】 SystemScreen.tsx:100-103
const ESCALATION_RULES: { level: AdviceLevel; days: number }[] = [
{ level: 2, days: ESCALATION_THRESHOLDS.level2Days },
{ level: 3, days: ESCALATION_THRESHOLDS.level3Days },
];
:317-328 <span className={`${styles.levelBadge} ${LEVEL_CLASS[rule.level]}`}>Lv{rule.level}</span>
<dd>{t("{n}日以上連続").replace("{n}", String(rule.days))}</dd>
LEVEL_CLASS(:72-76): 1→levelInfo / 2→levelWarn / 3→levelCritical
:330-334 注記「『連続』はカレンダー日ではなく、清掃スケジュールが存在する日の連続で
数えます。清掃予定のない日は連続を途切れさせません。」
【値の例】 ★構造由来★ 「Lv2 / 2日以上連続」「Lv3 / 3日以上連続」
【既定値】 lib/config/index.ts:133-140
export const CONSECUTIVE_DAYS_DISPLAY = {
defaultMax: 30,
minMax: ESCALATION_THRESHOLDS.level3Days, ← ★= 3。L3 の日数を下回らせない★
maxMax: 365, ← ★実データ保持2年の半分(:126-129)★
} as const ★構造由来★
★単位は「スケジュール存在日」。カレンダー日ではない★(:131。D-026)
↓ 保存済みの上書き
【ストア】 lib/settings/display-limits.ts
保存先: data/settings/display-limits.json(:49-51 defaultDisplayLimitStorePath())
型: interface DisplayLimitOverrides { consecutive_days_max?: number }(:35-37)
★編集されたキーだけが入る★(:34)
↓ 検証(★読み込み時にも通す★)
【検証】 lib/settings/display-limits.ts:147-174
export function validateConsecutiveDaysMax(value: unknown): ValidationOutcome
type ValidationOutcome = { ok: true; value: number } | { ok: false; reason: string }(:130-132)
typeof value !== "number" → "数値で指定してください"
!Number.isFinite(value) → "有限の数値で指定してください" ← ★isInteger より先に見る(:151-153)★
!Number.isInteger(value) → "整数で指定してください"
value < minMax(3) → "3以上で指定してください"
value > maxMax(365) → "365以下で指定してください"
★Number.isInteger(1e9) は true★ なので巨大値は範囲で弾く(:141-142)
★reason に入力値をエコーしない★(:144-145)
↓ 読み込み
【読込】 lib/settings/display-limits.ts:223-237
export async function loadConsecutiveDaysMax(): Promise<number>
★次の4つすべてで既定値に倒れる(例外にしない)★(:213-221):
1. ファイルが無い / 2. JSON 破損(.bak へ退避して空)/
3. キーが無い / 4. ★値が不正(文字列・小数・範囲外)★
4 のときは settings.value_invalid を warn で1行残す(:183-202)
↓ 1件に組み立て
【組立】 lib/settings/display-limits.ts:245-256
async function consecutiveDaysMaxSetting(): Promise<DisplayLimitSetting>
{ key: "consecutive_days_max", value, default: 30, min: 3, max: 365,
customized: value !== CONSECUTIVE_DAYS_DISPLAY.defaultMax }
★不正な値で既定値に倒れたときは customized にしない★(:253-254)
↓ API
【API】 app/api/settings/display-limits/route.ts:97-102
GET → NextResponse.json({ items: await loadDisplayLimitSettings() })
★force-dynamic は付けていない(Route Handlers は既定でキャッシュされない。:27-36)★
↓ 型
【型】 DisplayLimitSetting(lib/types/index.ts:413-425)
key: DisplayLimitKey(= "consecutive_days_max"。:403)
value: number / default: number / min: number / max: number / customized: boolean
↓ 表示
【画面】 SystemScreen.tsx:62 const CONSECUTIVE_DAYS_MAX_KEY = "consecutive_days_max";
:149-169 fetchDisplayLimit() → body.items.find(i => i.key === KEY) ?? null
:159 setLimitDraft(item === null ? "" : String(item.value))
:359-376 <input type="number" value={limitDraft} min={limit.min} max={limit.max} step={1} />
★min / max を数値で直書きしないこと★(:363-364)
:341-343 {limit?.customized && <span className={styles.customizedBadge}>編集済み</span>}
【値の例】 ★構造由来★ 未保存 → { value: 30, default: 30, min: 3, max: 365, customized: false }
→ 入力欄「30」、★「編集済み」バッジ無し★、★「既定値に戻す」ボタンも出ない★(:386-387)
★構造由来★ 60 を保存済み → { value: 60, ..., customized: true } → バッジ「編集済み」+ リセットボタン
★構造由来★ ファイルに "abc" が手で書かれていた → ★value=30 / customized=false に倒れ、
settings.value_invalid が warn に1行出る★
★構造由来★ data/settings/display-limits.json は ★現時点で存在しない★(未編集。2026-09-17)
画面 SystemScreen.tsx:216-236 saveLimit()
:221-225 raw === "" → ★送らない★("日数を入力してください")
★Number("") === 0 なので「未入力」を「0を指定した」にすり替えない★(:219-220)
:226-232 value === limit.value → ★叩かない★
:233-235 ★画面側では範囲を見ない★(検証の正はサーバ。<input min max> は開発者ツールで外せる)
:185-214 patchLimit(payload: { value: number } | { reset: true })
apiFetch<DisplayLimitSetting>("/api/settings/display-limits",
{ method: "PATCH", headers: {...}, body: JSON.stringify({ key: limit.key, ...payload }) })
:200-201 ★レスポンスの値で state を差し替える(送った値ではない。:179-181)★
↓
API app/api/settings/display-limits/route.ts:104-162
:122-125 isDisplayLimitKey(body.key) が false → 422
:133-136 body.reset === true → resetDisplayLimit(key) → settingFor(key)
★reset を value より先に見る★(:132)
:138-146 validateDisplayLimit(key, body.value) → !ok なら 422 { value: [reason] }
:148-149 saveDisplayLimit(key, outcome.value) → settingFor(key)
:150-160 例外 → 500(★err.message を本文に載せない。保存先の絶対パスが漏れる。:151★)
↓
ストア lib/settings/display-limits.ts:332-339
const DISPLAY_LIMIT_HANDLERS: Record<DisplayLimitKey, DisplayLimitHandler> = {
consecutive_days_max: { validate, save, reset, setting }
}
★★キーを知っているのはこの表だけ★★(:312-330)
:266-279 saveConsecutiveDaysMax(value) → ★保存前にも検証★(:261-264)
:282-289 resetConsecutiveDaysMax() → ★キーごと消す(既定値をコピーして持たない)★
Record<DisplayLimitKey, DisplayLimitHandler> にしてあるのは ★「キーを足したのに処理を足していない」をコンパイルエラーにするため★(:314-320)。 以前の形(ルート側に KNOWN_KEYS を持ち、保存は consecutive_days_max 固定)では、 ★2つ目のキーへの PATCH が consecutive_days_max を書き換える★(:322-327。★例外もテスト失敗も出ない★)。
★isDisplayLimitKey() は hasOwnProperty で見る★(:348-353)— key in obj だと "toString" / "constructor" が「知っているキー」になる。
★L1〜L3(lib/judge/escalation.ts)と R1〜R4(lib/judge/advice.ts)は 上限をかける前の生値で判定する★(lib/settings/display-limits.ts:22-24。CLAUDE.md §4.3 と同じ思想)。 ★だからこれだけ編集を許した★(SystemScreen.tsx:11-14)。
【既定値】 lib/config/advice-rules.ts
type AdviceRuleId = "R1" | "R2" | "R3" | "R4" | "B1" | "B2" | "B3"(:16)★構造由来★
interface AdviceRuleDefinition {
id: AdviceRuleId; phase: "A" | "B"; level: 1|2|3; icon: string;
title: string; text: string; actions: string[]; condition: string; }(:19-31 付近)
DEFAULT_ADVICE_RULES(:35-)
↓ 上書き
【ストア】 lib/settings/advice.ts
保存先: data/settings/advice.json(:44-46)
型: export type AdviceOverrides =
Partial<Record<AdviceRuleId, Partial<EditableAdviceFields>>>(:33-35)
★編集された項目だけが入る★(:32)
↓ 合成
【合成】 lib/settings/advice.ts:110-123
export async function loadAdviceRules(): Promise<Record<AdviceRuleId, AdviceRuleDefinition>>
merged[id] = ov ? { ...base, ...ov } : base;
★未編集のルールは lib/config の既定値がそのまま使われる★(:23-26)—
既定値を後から改善したときに編集していないルールへ自動で反映される
↓ API
【API】 app/api/settings/advice/route.ts:35-56
GET → { items: AdviceSetting[] }
:48-52 customized の計算:
r.title !== DEFAULT.title || r.text !== DEFAULT.text
|| JSON.stringify(r.actions) !== JSON.stringify(DEFAULT.actions)
↓ 型
【型】 AdviceSetting(lib/types/index.ts:381-394)
rule: AdviceRuleId / phase: "A"|"B" / level: 1|2|3 / icon: string /
title: string / text: string / actions: string[] / condition: string / customized: boolean
★編集できるのは title / text / actions のみ★(:379)
↓ 表示
【画面】 SystemScreen.tsx:112-127 fetchAdvice() → apiFetch<AdviceListResponse>("/api/settings/advice")
AdviceListResponse { items: AdviceSetting[] }(:47-49。★YAML に名前が無いので画面に持つ★)
:66-70 LEVEL_LABEL = { 1:"注意", 2:"警告", 3:"重大" }
:78-81 PHASE_LABEL = { A:"Phase A(完了率・連続日数)", B:"Phase B(状態・エラー種別)" }
:438-473 カードの構成:
:441-443 icon / :444 rule / :445-447 levelBadge / :448 phaseBadge /
:449-451 customized バッジ / :453 title / :454 text /
:456-463 actions の <ul> / :465-471 condition + ★編集不可バッジ★
【値の例】 ★構造由来★ R1: icon "⚠" / level 1 /「注意」/ Phase A /
title "出発失敗(R1)"(lib/config/advice-rules.ts:40)/
text "事前チェックのアラート内容を確認してください。セキュリティ未解除/
ロボット異常/充電不足のいずれかが原因です。"(:41)/
actions 3件(:42-46)/ condition "出発失敗(初回)"(:47)/ customized false
★構造由来★ R2: condition "完了率が10%未満"(:61)/ R3: level 2 / icon "🔶"(:66-67)/
R4: level 3 / icon "🔴"(:80-81)
★構造由来★ data/settings/advice.json は ★現時点で存在しない★(未編集。2026-09-17)
→ ★全7件の customized が false★
★★危ない★★: lib/config/advice-rules.ts を画面から直接 import すると ★既定値しか出せず、運用設定で編集済みの文言が反映されない★(SystemScreen.tsx:30-32)。 ★必ず GET /api/settings/advice を通すこと★。
★★注意★★: 画面に ★文言の編集UIは無い★(SystemScreen.tsx:420-476 は表示のみ)。 PATCH /api/settings/advice(app/api/settings/advice/route.ts:58-)は実装済みだが、 ★呼び出す画面がまだ無い★。
| ストア | ファイル | 型 | 使う画面 |
|---|---|---|---|
| D-Sデバイス備考 | data/settings/ds-device-notes.json |
Record<string, string>(lib/settings/ds-device-notes.ts:44) |
★F-06★ |
| アドバイス文言 | data/settings/advice.json |
AdviceOverrides(lib/settings/advice.ts:33-35) |
★F-10★ |
| 表示上限 | data/settings/display-limits.json |
DisplayLimitOverrides(lib/settings/display-limits.ts:35-37) |
★F-10★ |
共通の土台は lib/settings/json-store.ts(createJsonStore<T>({ filePath, empty, onWarn })): ★アトミック差し替え(rename)★ / ★破損時は上書きせず .bak へ退避して空を返す★ / ★Object.create(null) を empty に使ってプロトタイプ汚染を防ぐ★。
★3つとも「全件読み込み → 1件差し替え → 書き戻し」★ (ds-device-notes.ts:142-153 / advice.ts:133-136 / display-limits.ts:273-276)— ★だから上限が要る★(lib/config/index.ts:499-503 / :509-512)。
★/data は .gitignore:50 で除外★= ★コミットされず git のバックアップ対象外★。 ★唯一の実体がサーバ上のファイルなので、破損時に上書きしないことが前提になる★ (lib/settings/ds-device-notes.ts:20-22)。
★ロックは入れていない★(同 :24-28)— 施設20件弱・実質1人の運用のため。 ★同時保存は後勝ち★。★利用者が増えたら json-store.ts 側にロックを入れること★。
| # | 内容 | file:line |
深刻度 |
|---|---|---|---|
| A-1 | ★lib/fixtures/ds-devices.ts の施設12 の名前が施設マスタと違う★ — ds-devices は「メガバリューマート緑野第二店」、facilities は「フレッシュタウン湖畔店」 |
lib/fixtures/ds-devices.ts:111 / lib/fixtures/facilities.ts:264 |
★既知★(D-090 項1。docs/decisions.md:3429)。fixtures 内の表示だけの不整合 |
| A-2 | ★lib/fixtures/ds-alerts.ts の施設12 も同じ食い違い★ |
lib/fixtures/ds-alerts.ts:145 |
★既知★(同上) |
| A-3 | ★fixtures の施設12 はロボット0台なのに、スケジュール id33 が存在する★ | lib/fixtures/facilities.ts:278 vs lib/fixtures/schedules.ts:210-219 |
★★潜在★★ — fixtures 経路は robots[0] を通らないので今は落ちないが、Prisma 経路と同じ構造にしたら lib/db/index.ts:1684-1688 の例外を踏む |
| A-4 | ★id27 の end_time が fixtures("00:30")と DB("翌00:30")で違う★ |
lib/fixtures/schedules.ts:141 / docs/decisions.md:2888-2892 |
★意図的★(D-086 項10。DB版が正)だが ★USE_FIXTURES を切り替えると表示が変わる唯一の箇所★ |
| A-5 | ★clean_schedules.contract という第2の契約列がある★ — 型も既定値も facilities.contract と同一 |
prisma/schema.prisma:563 |
★★危険★★ — アプリは読んでいない(正しい)が、★名前と型が同じなので取り違えやすい★。CLAUDE.md §4.6 は customers.contract にしか言及していない |
| A-6 | ★「未対応」の対訳が2つの意味で衝突★ — F-07 の「まだ対応していない」と F-08 の「非対応機種」が同じ i18n キー | lib/i18n.ts:161(未対応: "Unsupported")/ DsAlertsScreen.tsx:289 / FacilitiesScreen.tsx:180 |
★判断済み(直さない)★(D-143 §3 #1)。★英語を配布する時点で必ず壊れる★ |
| A-7 | ★D-Secログとアラートで「異常」の色基準が違う★ — ログの異常=赤、アラートの通信断=アンバー。実データでは通信断しか出ないので ★アラート画面に赤が1色も出ない★ | DsLogScreen.tsx:36-39 vs DsAlertsScreen.tsx:36-38 / docs/decisions.md:11423-11424 |
★判断済み(アンバー維持)★(D-143 §3 #5) |
| A-8 | ★controls.status が 0/1 以外だと一覧全体が 500★ |
lib/db/ds-status.ts:23-25 → lib/db/index.ts:1464 |
★意図的★(握りつぶさない)だが ★被害範囲は F-06 の全滅★ |
| A-9 | ★心拍(control_reports)が0件の制御盤が1台でもあると F-06 の一覧全体が 500★ |
lib/db/index.ts:1451-1458 |
★意図的★。★備考保存の経路だけは dsDeviceExists() に分離済み★(指示書AH 段階2) |
| A-10 | ★section にロボットが0台のスケジュールが1件でもあると F-09 の一覧全体が 500★ |
lib/db/index.ts:1684-1688 |
★意図的★(P1 は1清掃エリア=1ロボット前提)。★シードが施設12 を構造的に除外することで回避している★ |
| A-11 | ★formatEndTime() は30時以上で例外を投げる★ — DB の値域は24〜29 なので、★"3000" が1件でも入ると F-09 が 500★ |
lib/format/schedule.ts:60 |
★意図的★。★ただし値域の根拠は TeamWorks 回答 B-5 のみ★ |
| A-12 | ★facilities.email は VarChar(100) だが facility_contacts.email は varchar(255) のまま★ |
prisma/schema.prisma:60-61 / :136 |
★申し送り★(2026-09-09 現行DDL) |
| A-13 | ★PATCH /api/settings/advice は実装済みだが呼び出す画面が無い★ |
app/api/settings/advice/route.ts:58- vs SystemScreen.tsx:420-476(表示のみ) |
★意図的★(SystemScreen.tsx:6-9。編集できるのは表示上限1つだけ) |
| A-14 | ★DsAlertsScreen の <Pagination> は items.length === 0 のときもDOMに置かれる★(部品側が null を返して回避) |
DsAlertsScreen.tsx:301-306 / components/ui/Pagination.tsx:61-63 |
★問題なし★(部品側で担保)。★ただし F-06 / F-08 / F-09 は items.length > 0 の内側に置いており、配置が揃っていない★ |
| A-15 | ★DsDeviceNoteResponse と AdviceListResponse / DisplayLimitListResponse が lib/types に無い★ — YAML にインライン定義で名前が無いため |
DsLogScreen.tsx:54-57 / SystemScreen.tsx:47-53 |
★意図的★(CLAUDE.md §9.2「型は YAML のスキーマ名と揃える」の裏返し)。★YAML に名前を付けたら移すこと★ |
本文書に載せた具体値の出所は次の3種類だけ。★本番DBへの接続は一切行っていない★(D-148 項5)。
| 出所 | 件数の目安 | 内訳 |
|---|---|---|
| ★構造由来★ | 最多 | prisma/schema.prisma(列の型・既定値・値域)/ lib/config/index.ts(閾値・上限・対応表)/ lib/types/index.ts(union の値)/ lib/format/*(変換の入出力) |
| ★シード由来★ | 多数 | lib/fixtures/ds-devices.ts(12件)/ ds-alerts.ts(12件)/ facilities.ts(12件)/ schedules.ts(13件) ★すべて当方のサンプルデータ★ |
| ★記録済み実測★ | 9件 | docs/handoff/テストDBのデータ出所_20260917.md:84(clean_schedules 21行)/ :87(robots 14行)/ :89(control_reports 14行)/ :90(facilities 12行)/ :91(controls 11行)/ :92(control_error_histories 5行)/ :94-96(customers / groups / devices 各1行)。★いずれも2026-09-17 の記録で、出所は「当方のシード由来」★ |
| ★先方投入★ | ★0件★ | ★先方由来の行は1行も見つかっていない★(同 :98) |
| # | 何 | 理由 |
|---|---|---|
| C-1 | ★security_off 側の D-Secアラートの取得元★ |
★DBに対応する列が存在しない★(lib/db/index.ts:1519-1531。D-090 項3-9 / 3-10)。★第3歩で扱う★と決まっており、現時点で追える縦線が無い |
| C-2 | ★DsAlert.section_name の実DBでの妥当性★ |
Prisma 側は sections.section_name を読んでいる(lib/db/index.ts:1552)が、★fixtures 側は「暫定のダミー値」と明記されている★(lib/types/index.ts:865-868 / lib/fixtures/ds-alerts.ts:15-17)。★どちらが仕様として正しいかの記録が見つからなかった★ |
| C-3 | ★responded_at / resolved_at が画面に出る経路★ |
型(lib/types/index.ts:873-874)と Prisma(lib/db/index.ts:1556-1557)にはあるが、★F-07 の画面に描画箇所が無い★。★更新系(PATCH /ds-alerts/{id}/status)が未実装★(app/api/ds-alerts/route.ts:7-9)なので、表示だけ先に作る判断がされた形跡も見つからなかった |
| C-4 | ★d-watch-api.yaml の該当スキーマ定義の行番号★ |
本文書は TS の型(lib/types/index.ts)を正として追った。★YAML 側の行番号は本文書では検証していない★(YAML は読み取り専用で、行番号の実測に時間を要するため) |
| C-5 | ★本番DBでの実際の contract の値の分布★ |
★本番DBに接続しないため★(D-148 項5)。★記録上は「contract を含む全列が lib/fixtures/facilities.ts と一致」★(docs/handoff/テストDBのデータ出所_20260917.md:90)とあるので、★4パターンすべてが入っていると推測されるが、推測なので値としては載せていない★ |
| C-6 | ★json-store.ts の内部実装(アトミック差し替え・.bak 退避)の行番号★ |
3ストアの共通土台として言及したが、★本文書の主題(画面の縦線)から外れるため行単位では追っていない★ |