/ | / | と揃えること★ */
-const COLUMN_COUNT = 5;
+const COLUMN_COUNT = 6;
/** 1行を送る(PATCH /ds-devices/{serial_number}/note)。★返ってきた備考(トリム済み・空なら null)を返す★。失敗は例外のまま */
async function sendDeviceNote(device: DsDevice, note: string): Promise {
@@ -143,6 +144,13 @@
const [saving, setSaving] = useState(false);
const draftCount = drafts.size;
+ /**
+ * 「ログ」モーダル(指示書AY 段階9-1)。
+ * 閉じたら押したボタンへフォーカスを戻すため、開いたときのボタンを保持する。
+ */
+ const [logsTarget, setLogsTarget] = useState(null);
+ const logsButtonRef = useRef(null);
+
// ★下書きがあるあいだはページを離れる前に確認する★(D-165)
useEffect(() => {
if (draftCount === 0) return;
@@ -467,6 +475,7 @@
+
@@ -475,6 +484,7 @@
| {t("接続状態")} |
{t("最終通信")} |
{t("備考")} |
+ {t("ログ")} |
@@ -517,6 +527,21 @@
)}
+
+ {/* ★編集中は押せない(U6)★。表は fieldset の外なので disabled を直接付ける。
+ ★幅は固定★(JP/EN で位置が動かない。M1) */}
+
+ |
{/* ★保存に失敗した行は直後に文言を出す★(下書きは残っている)。サーバーの文言は辞書に無ければ原文のまま */}
{rowError !== undefined && (
@@ -547,6 +572,15 @@
)}
+
+ {logsTarget && (
+ setLogsTarget(null)}
+ />
+ )}
);
}
diff --git a/d-watch-api.yaml b/d-watch-api.yaml
index 434955e..23ce6e0 100644
--- a/d-watch-api.yaml
+++ b/d-watch-api.yaml
@@ -55,6 +55,10 @@
description: D-Secログ(D-S制御盤稼働状況)
- name: ds-alerts
description: D-Secアラート
+ - name: ds-agent
+ description: |
+ 制御盤(ラズパイ)の常駐プログラム向け。認証は制御盤ごとの Bearer トークン(agentBearer)だけで、cookie では通らない(D-179)。
+ ログのファイルは D-Watch に保存せず、メモリで受けて待っているブラウザの応答へ流すだけ。
- name: facilities
description: 施設ロボットマスタ(Viewer)
- name: schedules
@@ -1809,6 +1813,385 @@
application/json:
schema: { $ref: "#/components/schemas/Error" }
+ /ds-devices/{serial_number}/logs:
+ get:
+ tags: [ds-devices]
+ summary: 制御盤のログファイルの一覧(制御盤に問い合わせ、D-Watch が絞る)
+ description: |
+ ★サーバーには保存しない(D-179)★。一覧は毎回制御盤からもらい(最大15秒待つ)、
+ 種類・期間の絞り込み・並べ替え・ページ分けは D-Watch 側で行う(D8。`lib/ds-logs/query.ts`)。
+ 制御盤との中継はメモリ内で、★1プロセス前提・再起動で進行中の問い合わせは失われる★。
+
+ 期間は `from ≤ modified_at < to`(瞬間で比べる。オフセットの違いは同じ瞬間として扱う)。
+ `modified_at` は JST(+09:00)に揃えて返す。
+ 制御盤は更新日時の新しい順に2万件または JSON 4MB で打ち切り、そのとき `truncated: true`(D18)。
+ `types` に無いキーを指定したときは0件(200)。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ - name: types
+ in: query
+ schema: { type: string, example: "api_client,send_start_sig" }
+ description: |
+ 種類のキー(カンマ区切り)。省略・空(空白だけ)なら全種類。
+ 空でない並びの中の空の要素(`app,,sys`・`app,`)は422。
+ - name: from
+ in: query
+ schema: { type: string, format: date-time, example: "2026-10-19T11:00:00+09:00" }
+ description: |
+ ISO 8601。★オフセット必須★(`Z` や `+00:00` も可)。日付だけ・オフセットなし・存在しない日付(2/30 など)は422。
+ 空は省略と同じ。`from` > `to` は422(`from` = `to` は0件)。
+ - name: to
+ in: query
+ schema: { type: string, format: date-time, example: "2026-10-19T12:00:00+09:00" }
+ description: "`from` と同じ規則。この瞬間ちょうどのファイルは含まない。"
+ - name: sort
+ in: query
+ schema:
+ type: string
+ enum: [date_desc, type_date_desc]
+ default: date_desc
+ description: |
+ ★この API の `sort` は向きを持たない1引数★。清掃詳細ログ等の `sort` / `order` とは別(M9)。
+ date_desc = 新しい順(同時刻は name 昇順)。type_date_desc = 制御盤の `types` の順 → 新しい順 → name 昇順。
+ 空は省略と同じ。それ以外の値は422。
+ - name: page
+ in: query
+ schema: { type: integer, minimum: 1, default: 1 }
+ description: 不正値は422にせず1へ丸める(既存の一覧と同じ)
+ - name: per_page
+ in: query
+ schema: { type: integer, minimum: 1, maximum: 200, default: 200 }
+ description: |
+ 1〜200。不正値は422にせず既定値(200)・上限へ丸める(既存の一覧と同じ)。
+ 画面の選択肢は 50 / 100 / 200(M7)。
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsLogListResponse" }
+ "404": { $ref: "#/components/responses/DsDeviceNotFound" }
+ "409": { $ref: "#/components/responses/DsAgentOffline" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+ "429":
+ description: "`agent_busy`(制御盤への指示の待ち行列が満杯)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "502": { $ref: "#/components/responses/DsAgentError" }
+ "504": { $ref: "#/components/responses/DsAgentTimeout" }
+
+ /ds-devices/{serial_number}/logs/exports:
+ post:
+ tags: [ds-devices]
+ summary: ログのエクスポートの準備(制御盤が zip を作り終えるまで待つ)
+ description: |
+ ★エクスポートは「準備 → 送信」の2段(D14)★。このPOSTは制御盤に zip を作らせ、
+ 制御盤が作り終えて大きさ・SHA-256 を報告するまで ★最大50秒★ 待ってから201を返す。
+ 受け取りは `download_url`(GET。1回だけ・`expires_in_sec` 秒以内)。
+ ★サーバーには保存しない(D-179)★。★1プロセス前提・再起動で進行中のエクスポートは失われる★。
+ 上限の判定は zip のバイト数(100MB。D15)。同時に進められるのは制御盤ごとに1本・全体で3本。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsLogExportRequest" }
+ responses:
+ "201":
+ description: 準備完了
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsLogExportResponse" }
+ "404": { $ref: "#/components/responses/DsDeviceNotFound" }
+ "409": { $ref: "#/components/responses/DsAgentOffline" }
+ "422":
+ description: |
+ バリデーションエラー(file_ids が 1〜500 件・重複なし・各 1〜8192 文字でない)。
+ `file_ids` を JSON にした大きさ(UTF-8)が 49152 バイトを超える → message「選んだファイルが多すぎます。数を減らしてください」(制御盤が応答を 64KB までしか読まないため。指示書BK K7)。
+ 本文が JSON でない・JSON のオブジェクトでない(null・配列・数値・文字列)も422「JSONの形式が不正です」(note と同じ。M7 (d))
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "429":
+ description: "`export_busy`(この制御盤か全体の同時エクスポートの上限)/ `agent_busy`(指示の待ち行列が満杯)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "502":
+ description: |
+ `agent_error`。制御盤が準備に失敗した(`details.agent_code` に `too_large` / `file_not_allowed` / `file_not_found` / `io_error` 等)。
+ zip が100MBを超えたときも `too_large`(D15)
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "504": { $ref: "#/components/responses/DsAgentTimeout" }
+
+ /ds-devices/{serial_number}/logs/exports/{export_id}:
+ get:
+ tags: [ds-devices]
+ summary: 準備した zip を受け取る(1回だけ)
+ description: |
+ 制御盤に送信を指示し、最初のチャンクを最大30秒待ってから200で流し始める。
+ 制御盤は zip を 4MB 以下のチャンクに分けて送る(★proxy が本体を10MBまでしかバッファしないため。D1★)。
+ D-Watch はチャンクをメモリで受けてこの応答へ流すだけで、★サーバーには保存しない(D-179)★。
+ ★1回だけ使える★。2回目・失効後・パスのシリアルが違うときは404 `export_not_found`。
+ ★流し始めた後の失敗(制御盤の切断・ハッシュ不一致など)はダウンロードの失敗になる(ステータスは200のまま)★。
+ ★JSON のエラーはまれ(制御盤の切断など)で、ブラウザによってはファイルとして保存される(D21)★。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ - $ref: "#/components/parameters/DsExportId"
+ responses:
+ "200":
+ description: zip のバイト列
+ headers:
+ Content-Type: { schema: { type: string, example: application/zip } }
+ Content-Disposition:
+ schema: { type: string, example: "attachment; filename=\"DS-002_logs_20261019-123456.zip\"; filename*=UTF-8''DS-002_logs_20261019-123456.zip" }
+ description: "ファイル名は `{シリアル}_logs_{JSTのYYYYMMDD-HHmmss}.zip`。シリアルの `[A-Za-z0-9._-]` 以外は `_`"
+ Cache-Control: { schema: { type: string, example: no-store } }
+ X-Accel-Buffering: { schema: { type: string, example: "no" }, description: nginx に応答をバッファさせない(一時ファイルに書かせない) }
+ Content-Length: { schema: { type: integer }, description: 準備のときに制御盤が報告した zip の大きさ }
+ content:
+ application/zip:
+ schema: { type: string, format: binary }
+ "404":
+ description: "`export_not_found`(無い・失効・使用済み・パスのシリアルと違う)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+ "502": { $ref: "#/components/responses/DsAgentError" }
+ "504": { $ref: "#/components/responses/DsAgentTimeout" }
+
+ /ds-devices/{serial_number}/agent-token:
+ get:
+ tags: [ds-devices]
+ summary: 制御盤トークンの発行日時(P1)
+ description: |
+ ★トークンの値・ハッシュは返さない★。未発行なら `issued_at: null`。
+ 制御盤が未接続でも使える(初めて設定するときは必ず未接続のため)。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentTokenStatus" }
+ "404": { $ref: "#/components/responses/DsDeviceNotFound" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+ post:
+ tags: [ds-devices]
+ summary: 制御盤トークンを発行する(作り直し)
+ description: |
+ ★生の値はこの応答で1回だけ★。既にあれば作り直し、古いトークンは無効になる。
+ 保存は SHA-256 だけ(DFA 独自ストア。メインDBには書かない。D16)。本文なし。応答は `Cache-Control: no-store`。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ responses:
+ "201":
+ description: 発行した
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentTokenIssued" }
+ "404": { $ref: "#/components/responses/DsDeviceNotFound" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+ delete:
+ tags: [ds-devices]
+ summary: 制御盤トークンを失効させる
+ description: ★未発行でも204(冪等。P2)★。以後この制御盤はログを送れない。
+ parameters:
+ - $ref: "#/components/parameters/DsSerialNumber"
+ responses:
+ "204": { description: 失効した(未発行だったときも) }
+ "404": { $ref: "#/components/responses/DsDeviceNotFound" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ # --------------------------------------------------------------- ds-agent
+ /ds-agent/commands:
+ get:
+ tags: [ds-agent]
+ summary: 指示の取得(ロングポーリング)
+ description: |
+ 接続はすべて制御盤から開く(店舗のネットワークに入口を作らない。D2)。
+ 指示があればすぐ200、無ければ `wait` 秒で204。同じ制御盤の2本目のポーリングが来たら古い方を204で返す。
+ 認証の通ったすべての制御盤向けリクエストで最終接続日時を更新し、60秒を超えて途切れたら未接続とする(D19)。
+ 中継はメモリ内で、★1プロセス前提・再起動で待ち行列は失われる★。
+ security:
+ - agentBearer: []
+ parameters:
+ - name: wait
+ in: query
+ schema: { type: integer, minimum: 1, maximum: 25, default: 25 }
+ description: 待つ秒数。範囲外・数でない値は422(★丸めない★ — 制御盤のプログラムの誤りを早く見つけるため)
+ - $ref: "#/components/parameters/XAgentVersion"
+ responses:
+ "200":
+ description: 指示
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentCommand" }
+ "204": { description: 指示なし }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ /ds-agent/commands/{command_id}/result:
+ post:
+ tags: [ds-agent]
+ summary: 一覧の結果(list の指示への応答)
+ description: |
+ 本体は4MB以下(読んだ長さで判定)・2万件以下。1件でも形が違えば結果全体を不正とする(400)。
+ ★ブラウザの一覧は 502 `agent_error`(`agent_code` は `result_invalid`)で終わり、その指示は消える(その後の結果は 404)★。
+ 失敗は `{ error: { code, message } }` で報告する(ブラウザには502 `agent_error`)。
+ security:
+ - agentBearer: []
+ parameters:
+ - $ref: "#/components/parameters/DsCommandId"
+ - $ref: "#/components/parameters/XAgentVersion"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentListResult" }
+ responses:
+ "204": { description: 受け取った }
+ "400": { $ref: "#/components/responses/DsResultInvalid" }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "404": { $ref: "#/components/responses/DsCommandNotFound" }
+ "410":
+ description: "`command_expired`(ブラウザ側の待ちが時間切れになった後)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ /ds-agent/commands/{command_id}/prepared:
+ post:
+ tags: [ds-agent]
+ summary: エクスポートの準備完了(export の指示への応答)
+ description: |
+ zip を作り終えたら大きさ・SHA-256・ファイル数を報告する。D-Watch は `total_bytes` が100MB以下かを確かめ、
+ 超えていればエクスポートを失敗にする(この応答は204のまま。後の send は来ない。D15)。
+ `send` が来たら同じ zip を送る(作り直さない)。
+ security:
+ - agentBearer: []
+ parameters:
+ - $ref: "#/components/parameters/DsCommandId"
+ - $ref: "#/components/parameters/XAgentVersion"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentPrepared" }
+ responses:
+ "204": { description: 受け取った }
+ "400": { $ref: "#/components/responses/DsResultInvalid" }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "404": { $ref: "#/components/responses/DsCommandNotFound" }
+ "410":
+ description: "`command_expired`(ブラウザ側の待ちが時間切れになった後)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ /ds-agent/commands/{command_id}/chunks/{seq}:
+ post:
+ tags: [ds-agent]
+ summary: zip のチャンク(send の指示を受けて送る)
+ description: |
+ ★URL の command_id は export の指示の id(D25 ①)★。send の指示の id へ送ると404。
+ ★1チャンク 4MB 以下★(proxy が本体を10MBまでしかバッファせず、超えた分を黙って切り捨てるため。D1)。
+ `seq` は0から連番。D-Watch は長さと `X-Chunk-Sha256` を検証し、最後に `complete` で合計と全体の SHA-256 を照合する。
+ ★204 はブラウザ側に空きができてから返す(流量制御。最大45秒。超えたら410)★。
+ 409 / 410 が返ったら送信をやめる。チャンクはメモリで中継するだけで、★サーバーには保存しない(D-179)★。
+ security:
+ - agentBearer: []
+ parameters:
+ - $ref: "#/components/parameters/DsCommandId"
+ - $ref: "#/components/parameters/DsChunkSeq"
+ - $ref: "#/components/parameters/XAgentVersion"
+ - name: X-Chunk-Sha256
+ in: header
+ required: true
+ schema: { type: string, pattern: "^[0-9a-f]{64}$" }
+ description: このチャンクの SHA-256(小文字の16進64桁)
+ requestBody:
+ required: true
+ content:
+ application/octet-stream:
+ schema: { type: string, format: binary }
+ responses:
+ "204": { description: 受け取った(ブラウザ側に空きができた) }
+ "400":
+ description: "`chunk_invalid`(長さが0・4MB超・ハッシュ不一致・合計が準備のときの大きさを超えた)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "404": { $ref: "#/components/responses/DsCommandNotFound" }
+ "409":
+ description: "`chunk_out_of_order`(seq が連番でない・send の前・★前のチャンクの 204 を待たずに次を送った(J8)。このとき保留中だった前のチャンクの POST は 410 `export_cancelled` で返る★)。エクスポートは失敗になる"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ "410": { $ref: "#/components/responses/DsExportCancelled" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ /ds-agent/commands/{command_id}/complete:
+ post:
+ tags: [ds-agent]
+ summary: zip の送信完了
+ description: |
+ ★URL の command_id は export の指示の id(D25 ①)★。
+ 受け取った合計 = 準備のときの `total_bytes` = この `total_bytes`、かつ受け取ったバイト列の SHA-256 = 準備のときの `sha256` = この `sha256` を確かめる(D15)。
+ 一致しなければ400で、ブラウザ側のダウンロードは失敗になる。
+ security:
+ - agentBearer: []
+ parameters:
+ - $ref: "#/components/parameters/DsCommandId"
+ - $ref: "#/components/parameters/XAgentVersion"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentComplete" }
+ responses:
+ "204": { description: 完了した }
+ "400": { $ref: "#/components/responses/DsResultInvalid" }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "404": { $ref: "#/components/responses/DsCommandNotFound" }
+ "410": { $ref: "#/components/responses/DsExportCancelled" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
+ /ds-agent/commands/{command_id}/fail:
+ post:
+ tags: [ds-agent]
+ summary: 指示の失敗の報告
+ description: |
+ 準備中の失敗(`too_large` / `file_not_allowed` / `file_not_found` / `io_error` 等)は export の指示の id へ。
+ ★送信中の失敗も export の指示の id へ★。`send` が来たのに zip が無いときは `export_not_found`(D25 ②)。
+ `message` はログに載せない(画面にも出さない)。
+ security:
+ - agentBearer: []
+ parameters:
+ - $ref: "#/components/parameters/DsCommandId"
+ - $ref: "#/components/parameters/XAgentVersion"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/DsAgentFail" }
+ responses:
+ "204": { description: 受け取った }
+ "400": { $ref: "#/components/responses/DsResultInvalid" }
+ "401": { $ref: "#/components/responses/Unauthorized" }
+ "404": { $ref: "#/components/responses/DsCommandNotFound" }
+ "410": { $ref: "#/components/responses/DsExportCancelled" }
+ "422": { $ref: "#/components/responses/ValidationError" }
+
# -------------------------------------------------------------- ds-alerts
/ds-alerts:
get:
@@ -2111,6 +2494,14 @@
`dwatch_session` cookie が発行される(httpOnly / Secure / SameSite=Lax / path=/)。
cookie の値は設定値 `AUTH_TOKEN` と定数時間比較で突き合わせる(D-072)。
判定は `proxy.ts` が全ルートで行う。
+ agentBearer:
+ type: http
+ scheme: bearer
+ description: |
+ 制御盤ごとのトークン(32バイトの乱数を base64url にした43文字。D-179)。★`/ds-agent/` 配下だけ★で使い、cookie では通らない。
+ ★それ以外のパスで Bearer を出されても無視する(cookie が要る)★。
+ 発行・失効は画面から(`POST` / `DELETE /ds-devices/{serial_number}/agent-token`)。D-Watch は SHA-256 だけを保存する。
+ IP 制限は他のパスと同じくかかる(D6。リリース時に扱いを決める)。
parameters:
Page:
@@ -2127,6 +2518,37 @@
required: true
schema: { type: integer, minimum: 1 }
description: 1以上の整数。0・負値・非整数・安全整数超は422で弾く(D-123)
+ DsSerialNumber:
+ name: serial_number
+ in: path
+ required: true
+ schema: { type: string, maxLength: 100 }
+ description: |
+ D-S制御盤のシリアル番号。100文字を超えると422。文字種は絞らない(D-123)。
+ `/ds-devices/{serial_number}/note` と同じ規則。
+ DsExportId:
+ name: export_id
+ in: path
+ required: true
+ schema: { type: string, minLength: 1, maxLength: 64 }
+ description: 準備の201で返した値。空と64文字超だけ422(文字種は絞らない。M8)
+ DsCommandId:
+ name: command_id
+ in: path
+ required: true
+ schema: { type: string, minLength: 1, maxLength: 64 }
+ description: 指示の id。空と64文字超だけ422(文字種は絞らない。M8)
+ DsChunkSeq:
+ name: seq
+ in: path
+ required: true
+ schema: { type: integer, minimum: 0, maximum: 999999999 }
+ description: 0から連番。0以上の整数で、先頭の0・符号・小数・空白・10桁以上は422(M8)
+ XAgentVersion:
+ name: X-Agent-Version
+ in: header
+ schema: { type: string, pattern: "^[0-9A-Za-z.+-]{1,32}$" }
+ description: 接続の記録に使う。形の合わない値は無視する(拒否しない)
GroupId:
name: group_id
in: query
@@ -2264,6 +2686,41 @@
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
+ DsDeviceNotFound:
+ description: 一覧に出ない制御盤(シリアルが無い・論理削除・controls.status が 0/1 以外。D-182)
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsAgentOffline:
+ description: "`agent_offline`。制御盤が未接続(最後のリクエストから60秒を超えた)。`details.last_seen_at` に最終接続日時(無ければ空配列)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsAgentError:
+ description: "`agent_error`。制御盤がエラーを返した・制御盤の結果の形が不正。`details.agent_code` に制御盤の code(形の不正は `result_invalid`)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsAgentTimeout:
+ description: "`agent_timeout`。制御盤から時間内に応答が無い(一覧15秒・準備50秒・最初のチャンク30秒。どれも nginx の60秒より短い)"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsResultInvalid:
+ description: "`result_invalid`。本体の形の誤り(JSON でない・オブジェクトでない・必須の項目が無い・型が違う)と値の不一致。★形の誤りは、`/result` ではブラウザの一覧を 502 で終わらせ(指示書BJ)、prepared・complete・fail では状態を変えない(D25 ④)。complete の値の不一致(合計・SHA-256)はエクスポートを失敗にする★"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsCommandNotFound:
+ description: 指示が無い・別の制御盤の指示・種類の違う指示(list の id に prepared 等)。403 にはしない
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
+ DsExportCancelled:
+ description: "`export_cancelled`。エクスポートが取り消し・失敗・失効した後(ブラウザの切断・時間切れ等)。送信をやめる"
+ content:
+ application/json:
+ schema: { $ref: "#/components/schemas/Error" }
schemas:
@@ -2561,7 +3018,7 @@
precheck_minutes:
type: integer
example: 60
- description: 清掃開始予定時刻の何分前のチェックか(clean_timetables.precheck の生値)。null にならない(旧: launch_failed のとき null)
+ description: "清掃開始予定時刻の何分前のチェックか(clean_timetables.precheck の生値)。null にならない(旧: launch_failed のとき null)"
serial_number:
type: string
example: "8111R5628060011"
@@ -3190,6 +3647,161 @@
items: { $ref: "#/components/schemas/DsDevice" }
meta: { $ref: "#/components/schemas/Meta" }
+ DsLogType:
+ type: object
+ description: ログの種類。値は制御盤の設定ファイルが決め、D-Watch は持たない(U5)。ブラウザ向けの `DsLogListResponse` と制御盤の一覧結果で共有し、制御盤から来た値をそのまま返す
+ required: [key, label]
+ properties:
+ key:
+ type: string
+ minLength: 1
+ pattern: "^[^,\\s](?:[^,]*[^,\\s])?$"
+ example: "api_client"
+ description: 1文字以上・`,` を含まない・前後に空白が無い
+ label: { type: string, example: "API 通信" }
+
+ DsLogFile:
+ type: object
+ description: ログファイル1件。ブラウザ向けの `DsLogListResponse` と制御盤の一覧結果で共有し、制御盤から来た値をそのまま返す
+ required: [file_id, name, type, size_bytes, modified_at]
+ properties:
+ file_id: { type: string, minLength: 1, maxLength: 8192, description: 制御盤が付けた id(D-Watch は解析しない) }
+ name:
+ type: string
+ example: "api_client/api_client.log.2026-10-18"
+ description: "`{root の name}/{root からの相対パス}`(zip の中のパスと同じ。D24)。D-Watch は解析しない"
+ type: { type: string, example: "api_client", description: "`DsLogType.key` のどれか" }
+ size_bytes: { type: integer, minimum: 0, example: 1048576 }
+ modified_at: { type: string, format: date-time, example: "2026-10-19T11:00:00+09:00" }
+
+ DsAgentStatus:
+ type: object
+ required: [connected, last_seen_at, agent_version]
+ properties:
+ connected: { type: boolean }
+ last_seen_at: { type: string, format: date-time, nullable: true, description: 最終接続日時。一度も接続していなければ null }
+ agent_version: { type: string, nullable: true, example: "1.0.0" }
+
+ DsLogListResponse:
+ type: object
+ required: [agent, types, items, truncated, meta]
+ properties:
+ agent: { $ref: "#/components/schemas/DsAgentStatus" }
+ types:
+ type: array
+ description: 制御盤から来た順
+ items: { $ref: "#/components/schemas/DsLogType" }
+ items:
+ type: array
+ items: { $ref: "#/components/schemas/DsLogFile" }
+ truncated:
+ type: boolean
+ description: 制御盤が件数(2万件)か大きさ(4MB)で一覧を打ち切ったか。true なら新しいものから一部だけ(D18)
+ meta: { $ref: "#/components/schemas/Meta" }
+
+ DsLogExportRequest:
+ type: object
+ required: [file_ids]
+ properties:
+ file_ids:
+ type: array
+ minItems: 1
+ maxItems: 500
+ uniqueItems: true
+ description: |
+ 各要素は 1〜8192 文字。加えて、配列を JSON にした大きさ(UTF-8)が 49152 バイト以内であること
+ (制御盤が poll の応答を 64KB までしか読まないため。指示書BK K7。JSON Schema では表せない)
+ items: { type: string, minLength: 1, maxLength: 8192 }
+
+ DsLogExportResponse:
+ type: object
+ required: [export_id, download_url, total_bytes, file_count, expires_in_sec]
+ properties:
+ export_id: { type: string }
+ download_url: { type: string, example: "/api/ds-devices/DS-002/logs/exports/0b6f3c1e-8d2a-4f5e-9c7b-1a2b3c4d5e6f" }
+ total_bytes: { type: integer, minimum: 1, description: zip の大きさ }
+ file_count: { type: integer, minimum: 1 }
+ expires_in_sec: { type: integer, example: 60, description: この秒数以内に download_url を開かないと失効する }
+
+ DsAgentTokenStatus:
+ type: object
+ required: [issued_at]
+ properties:
+ issued_at: { type: string, format: date-time, nullable: true, description: 未発行なら null }
+
+ DsAgentTokenIssued:
+ type: object
+ required: [token, issued_at]
+ properties:
+ token: { type: string, pattern: "^[A-Za-z0-9_-]{43}$", description: ★この応答で1回だけ★。制御盤の設定ファイルに入れる }
+ issued_at: { type: string, format: date-time }
+
+ DsAgentCommand:
+ type: object
+ required: [command_id, kind, params]
+ description: |
+ list: params は `{}`。
+ export: params は `{ file_ids, max_bytes, chunk_bytes }`(zip を作って prepared を送る)。
+ send: params は `{ export_command_id }`(準備した zip をチャンクで送る。チャンク・complete・fail の URL は export_command_id。D25 ①)。
+ properties:
+ command_id: { type: string }
+ kind: { type: string, enum: [list, export, send] }
+ params:
+ type: object
+ properties:
+ file_ids: { type: array, items: { type: string } }
+ max_bytes: { type: integer, example: 104857600 }
+ chunk_bytes: { type: integer, example: 4194304 }
+ export_command_id: { type: string }
+
+ DsAgentListResult:
+ description: 一覧の結果、または失敗の報告
+ oneOf:
+ - type: object
+ required: [types, items, truncated]
+ properties:
+ types:
+ type: array
+ description: key は重複させない
+ items: { $ref: "#/components/schemas/DsLogType" }
+ items:
+ type: array
+ maxItems: 20000
+ description: 更新日時の新しい順。file_id は重複させない。type は types のどれか。modified_at は必ずオフセット付き
+ items: { $ref: "#/components/schemas/DsLogFile" }
+ truncated: { type: boolean }
+ - type: object
+ required: [error]
+ properties:
+ error:
+ type: object
+ required: [code, message]
+ properties:
+ code: { type: string, minLength: 1, maxLength: 64 }
+ message: { type: string, minLength: 1, maxLength: 500 }
+
+ DsAgentPrepared:
+ type: object
+ required: [total_bytes, sha256, file_count]
+ properties:
+ total_bytes: { type: integer, minimum: 1 }
+ sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
+ file_count: { type: integer, minimum: 1 }
+
+ DsAgentComplete:
+ type: object
+ required: [total_bytes, sha256]
+ properties:
+ total_bytes: { type: integer, minimum: 1 }
+ sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
+
+ DsAgentFail:
+ type: object
+ required: [code, message]
+ properties:
+ code: { type: string, minLength: 1, maxLength: 64, example: "export_not_found" }
+ message: { type: string, minLength: 1, maxLength: 500 }
+
DsAlert:
type: object
description: モックのDSA(type/dev/fac/tm/det/st/mm)に対応。トークン期限種別は除外
diff --git a/docs/code-map.md b/docs/code-map.md
index 480ff57..284e11e 100644
--- a/docs/code-map.md
+++ b/docs/code-map.md
@@ -25,15 +25,15 @@
★直した箇所・新しく書いた箇所の行番号は `a961829`〜`04f974a`(HEAD。`a961829` 以降はテスト1本と docs だけの変更)で `git show HEAD:` から確かめました★(D-171 までの節で `f2b91fc` と書いてある行番号は、D-173 で動かなかったファイルのもの)。それ以外は上のとおりです(★`lib/config/index.ts` は AU 以降で後ろにずれています。例: `MAX_CSV_ROWS` は `:469` → `:494`(`a961829`)★。D-161〜D-173 で触っていない行は直していません)。
- ★**第4版改訂5: 2026-10-19**(**D-180** / 指示書AX の実装 `d282e07` まで)★ — 指示書AX で古くなった D-Sec アラートの記述だけを直しました(取得元を `robot_error_reports` に・編集モード・`PATCH /ds-alerts/{id}/status` の削除・fixtures の `ds-alerts.ts` の削除・削除済みの施設・清掃エリアの行を出さない)。★直した箇所の行番号は `d282e07` で確かめました★。それ以外は上のとおりです
- ★**第4版改訂6: 2026-10-19**(**D-181** / 指示書BA の実装 `b09e352` まで)★ — 編集モードのある3画面の見た目と D-Sec の文言を揃えた分だけ直しました(解消日時の判定 `showsResolvedTime()`・対応状況 120px・D-Sec の備考の入力欄の位置・変えた行の目印・アラートの「編集」の位置・D-Sec の状態ボタン「未解消のみ / 全件 / 解消済み」)。★直した箇所の行番号は `b09e352` で確かめました★。それ以外は上のとおりです
-- ★**第4版改訂7: 2026-10-19**(**D-182** / 指示書BB)★ — データ層・API の止まる経路と削除済みの扱いを揃えた分だけ直しました(`lib/db/visibility.ts` を足した・ロボット0台/状態が 0/1/2 以外/心拍0件/直近モードのメタ欠けで例外にしない・`dsAlerts({ open_only })`・`ConsecutiveDbRange.facility_ids`・`parseDate()` の存在しない日付・`resolveTargetCustomer()` の AND・`weekStart()` / `cleanErrorLabel()` / `CLEAN_ERROR_LABELS` の削除・`/ds-devices` の `last_reported_at` が nullable)。★関数・型・設定の行番号と、本書で変わったファイルの行数は `86fea53`(コード)で引き直しました★(JSON ストアの書き込みの直列化 `c5fe519` を含む。各所の「行番号は `xxxx`」という注記よりこちらが新しい)。★関数名を伴わない位置の参照(`:N` だけのもの)は、その文を最後に書いた版から差分で写しただけ★で、それより前からずれていたものはずれたままです(関数名で探すこと)。D-182 の追記は他のセッションの未コミットの差分があるため監督待ち
+- ★**第4版改訂7: 2026-10-19**(**D-182** / 指示書BB)★ — データ層・API の止まる経路と削除済みの扱いを揃えた分だけ直しました(`lib/db/visibility.ts` を足した・ロボット0台/状態が 0/1/2 以外/心拍0件/直近モードのメタ欠けで例外にしない・`dsAlerts({ open_only })`・`ConsecutiveDbRange.facility_ids`・`parseDate()` の存在しない日付・`resolveTargetCustomer()` の AND・`weekStart()` / `cleanErrorLabel()` / `CLEAN_ERROR_LABELS` の削除・`/ds-devices` の `last_reported_at` が nullable)。★関数・型・設定の行番号と、本書で変わったファイルの行数は `86fea53`(コード)で引き直しました★(JSON ストアの書き込みの直列化 `c5fe519` を含む。各所の「行番号は `xxxx`」という注記よりこちらが新しい)。★関数名を伴わない位置の参照(`:N` だけのもの)は、その文を最後に書いた版から差分で写しただけ★で、それより前からずれていたものはずれたままです(関数名で探すこと)。D-182 は 2026-11-05 に `docs/decisions.md` へ追記済み
期待値表(D-171 の N1〜N5・M1〜M6、D-173 の表1・表2)の中身は `docs/decisions.md` とテストを見てください。★D-172(第12回の候補への回答)のうち、レポートの書き込みの遅れ(30分を超えうる)は D-173 で解決済み★。★D-174(アドバイス B2 の根拠をエラーコードが決まったら本清掃のエラー内容に切り替える)は 5.8 に書きました★
-- ★**第4版改訂8: 2026-10-19**(**D-183** / 指示書BC の実装 `50a3082` まで)★ — 画面の古い応答・失敗の表示・表示設定の保存・D-Sec ログの備考の編集モードの分だけ直しました。★直した箇所の行番号は `50a3082` で確かめました★(画面のファイルの `:N` は `77f2f1b` → `50a3082` の差分で写し、変わった行を指すものは関数名・文言で引き直した)。それ以外は上のとおりです。D-183 の追記は他のセッションの未コミットの差分があるため監督待ち
+- ★**第4版改訂8: 2026-10-19**(**D-183** / 指示書BC の実装 `50a3082` まで)★ — 画面の古い応答・失敗の表示・表示設定の保存・D-Sec ログの備考の編集モードの分だけ直しました。★直した箇所の行番号は `50a3082` で確かめました★(画面のファイルの `:N` は `77f2f1b` → `50a3082` の差分で写し、変わった行を指すものは関数名・文言で引き直した)。それ以外は上のとおりです。D-183 は 2026-11-05 に `docs/decisions.md` へ追記済み
- ★**第4版改訂9: 2026-10-27**(**D-184** / 指示書BD の実装。★未コミットの作業ツリー★(`bb79de9` の上))★ — エラー一覧の「エラー内容」(`error_lists` の id)を API で選べるようにした分だけ直しました
(新しい API ★`GET /api/error-lists`★(選択肢)・`PATCH /api/errors/{error_id}/status` が ★5列目の `error_list_id`★ も書く(★この経路だけ。事前チェックと D-Sec は4列のまま★)・
`ErrorReportItem` の3項目(`error_list_id` / `error_list` / `error_list_selectable`)・新しいファイル(`lib/db/error-list-source.ts`・`lib/errors/status-update.ts`・`lib/fixtures/error-lists.ts`)・
`DataSource.errorLists()`・設定2つ(`ERROR_LIST_NOTE_REQUIRED_IDS` / `ERROR_LIST_UNSELECTABLE_ERROR_TYPES`)・★`prisma/schema.prisma` の `ErrorList` と `error_list_id` は db pull できないための手書き(D-056 の例外)★)。
- ★直した箇所の行番号は 2026-10-27 の作業ツリーで `grep -n` して確かめました★。それ以外は上のとおりです。PDF は作り直していません。D-184 の追記は `docs/decisions.md` に他のセッションの未コミットの差分があるため監督待ち
-- ★**第4版改訂10: 2026-10-27**(指示書BE。エラー一覧の画面でエラー内容を選ぶ)★ — 「種類」の2行目と選択窓(`ErrorContentPicker`)・エラー一覧専用の下書き(`lib/errors/edit-drafts.ts`)・その他の備考必須。★D-185 は `docs/decisions.md` に他のセッションの未コミット差分があるため書いていない★(案は指示書BE §11)
+ ★直した箇所の行番号は 2026-10-27 の作業ツリーで `grep -n` して確かめました★。それ以外は上のとおりです。PDF は作り直していません。D-184 は 2026-11-05 に `docs/decisions.md` へ追記済み
+- ★**第4版改訂10: 2026-10-27**(指示書BE。エラー一覧の画面でエラー内容を選ぶ)★ — 「種類」の2行目と選択窓(`ErrorContentPicker`)・エラー一覧専用の下書き(`lib/errors/edit-drafts.ts`)・その他の備考必須。D-185 は 2026-11-05 に `docs/decisions.md` へ追記済み(案は指示書BE §11)
- **この文書は「正」ではありません。** ルールの正は [CLAUDE.md](../CLAUDE.md)、構成と依存の向きの正は
[docs/architecture.md](architecture.md)、決定の理由の正は [docs/decisions.md](decisions.md) です。
食い違ったら向こうが正しいと考え、この文書を直してください
@@ -66,7 +66,7 @@
| ★`customers` の見出し語★ | — | 屋号 | ★**顧客**(EN "Customer" / "All Customers")★ | **D-158**(第11回 P-15=B) |
| ★ダッシュボードの数値カード・表の母集団★ | 当日 | 当日 | ★**各清掃エリアの直近の清掃1件**(7日以内・★予定の終了 ≤ 現在★の予定のみ・出発失敗と結果待ちを含む)★。★旧(10/06): 終了+30分(`settleMinutes`)たった予定のみ。D-173 で廃止★。`?date=` 指定時は従来どおりその日。★スケジュール進捗・要レスキューは当日のまま★ | **D-159**(指示書AV)→ **D-173** |
| ★清掃詳細ログの既定の期間★ | 今日1日 | 今日1日 | ★**期間を指定しない(全期間)**★ | **D-159** 追記(ユーザー指示「C」) |
-| 予定外のタスク(`clean_timetable_id` が NULL) | — | (扱いなし) | ★完了率・実施回数には**入れない**。総清掃面積には**入れる**★ | D-155 項3 |
+| 予定外のタスク(`clean_timetable_id` が NULL か 0) | — | (扱いなし) | ★**1件として数える**(予定 +1・開始 +1・`status = 4` なら正常終了 +1・1回ごとの完了率を平均に入れる。★今の期間だけ。確定期間は TeamWorks のサマリのまま★)。総清掃面積にも入れる★。旧(10/02〜10/29): 完了率・実施回数には入れない | D-155 項3 → **D-187**(指示書BG・A案) |
| 清掃実績の取得元 | `clean_status_histories` | `clean_task_reports` | `clean_task_reports` | D-151 |
| 清掃結果(`clean_task_reports.status`) | — | 数字のまま | ★表示名(0 清掃開始前〜5 清掃異常終了)★。**判定には使わない** | D-155 項2 |
| ★接続先DBの呼び名★ | (外部DBは未接続。ローカルの MySQL のみ) | 「本番DBそのもの」 | ★**ステージング**(本番DBは別にある)★ | D-148 → **D-152** |
@@ -436,7 +436,7 @@
| 出ないもの | 理由 | 根拠 |
|---|---|---|
-| ★予定外のタスク★(`clean_task_reports.clean_timetable_id` が NULL の行) | ①の予定から引くので、予定を持たない清掃は③の `in` の条件に一致しません。**欠損ではなく実在する清掃**ですが、この画面は予定が起点です | ユーザー回答 2026-10-01(D-152 3-3)。予定起点・実績起点・両方のどれにするかは、データが入ってから検討(D-151 項7) |
+| ~~★予定外のタスク★~~ ★(2026-10-29 から出る。D-187)★ | 旧: ①の予定から引くので、予定を持たない清掃は③の `in` の条件に一致せず出なかった。★今は `loadUnscheduledCleanLogRows()`(`lib/db/unscheduled-clean-log-source.ts`)で `clean_timetable_id` が 0 か NULL の行を別に読み、1件1行で足す(期間は `started_at`)★ | D-152 3-3・D-151 項7 → **D-187**(指示書BG) |
| まだ予定時刻が来ていない清掃 | `isFutureSchedule()`(`clean-log-mapping.ts:308`)で外します。外さないと、まだレポートが無い未来の清掃が全部「出発失敗」になります | D-107 設計3-C(`lib/db/index.ts:1110-1127` のコメント) |
#### いまステージングで画面を開くとどう見えるか
@@ -579,7 +579,7 @@
2. `/detail-log` / `app/(dashboard)/detail-log/page.tsx`
3. `components/features/detail-log/DetailLogScreen.tsx`(★587行。`50a3082`★)。子部品: `CleanLogTable.tsx`(★394行。`50a3082`★。表)/ `AreaModal.tsx`(185行。エリア別完了率のモーダル)
4. `GET /api/clean-logs`(`:205`)、`GET /api/clean-logs/export`(CSV。`:516` のリンク)、`GET /api/clean-logs/{id}/advice`(`:253`)、`GET /api/clean-logs/{id}/areas`(`:297`)(★行番号は `f2b91fc`★)
-5. **この画面の数字はどこから**: 1行=`clean_timetables`(★と `clean_timetable_histories`。2026-10-05 から★)の本清掃1件。結果は `clean_task_reports`(`clean_timetable_id` で突き合わせ)、事前チェックの状態は `robot_error_reports.error_type`。★完了率は `clean_task_reports.clean_area ÷ task_area`(上限100・小数第1位切り捨て)★で、`clean_ratio` 列は使いません(D-155。★第11回 P-2=A で確定。D-158★)。列ごとの対応表は[章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで)の 2.4。エリア別完了率のモーダルも同じ `clean_task_reports` の面積と `sections.section_name` から作ります(`lib/db/index.ts:1358`)
+5. **この画面の数字はどこから**: 1行=`clean_timetables`(★と `clean_timetable_histories`。2026-10-05 から★)の本清掃1件、★または予定外のタスク(`clean_task_reports` の `clean_timetable_id` が NULL か 0)1件(2026-10-29 から。D-187)★。結果は `clean_task_reports`(`clean_timetable_id` で突き合わせ)、事前チェックの状態は `robot_error_reports.error_type`。★完了率は `clean_task_reports.clean_area ÷ task_area`(上限100・小数第1位切り捨て)★で、`clean_ratio` 列は使いません(D-155。★第11回 P-2=A で確定。D-158★)。列ごとの対応表は[章2](#2-まず1本だけ追う--清掃詳細ログの数字が画面に出るまで)の 2.4。エリア別完了率のモーダルも同じ `clean_task_reports` の面積と `sections.section_name` から作ります(`lib/db/index.ts:1358`)
6. 固有の注意点:
- ★ページ送りあり★
- ★日付範囲の上限は **731日(2年)**★(`MAX_DATE_RANGE_DAYS`)。超えると画面側で止めます(`dateRangeError()` `:165`。★両方入っているときだけ★)。★初期表示は期間を指定しない(全期間)★(D-159 追記。旧: 今日1日)。★片方だけ入れたら入っている側だけを送ります★(5b 以前と同じ挙動。`lib/clean-logs/detail-log-date-range.ts`)。★期間なしの取得は Prisma 版で保持期間の予定を全部読むので、リリース前に見直す★
@@ -592,7 +592,7 @@
- ★「清掃結果」列は `clean_task_reports.status` の**表示名**です★(D-155 項2)。0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了。★表に無い値(6 など)は数字のまま★、レポート行が無ければ「出発失敗」(★ただし結果待ちの行は「結果待ち」。D-173★)。組み立ては `formatCleanTaskStatus()`(`lib/format/clean-result.ts:61`)を `cleanLogRowView()`(`:98`)経由で使い、画面では `t()` に通して英語にも切り替わります(`CleanLogTable.tsx:287`。`a961829`)。★判定には使いません★(中断・異常終了でも完了率で ◎○△× を出す)
- ★**結果待ち**(D-173)★: 予定の開始を過ぎてレポートがまだ無い清掃のうち、★予定の終了から `RESULT_WAIT_MINUTES`(120分)以内のもの★(清掃中も含む)は、出発「…」・清掃結果「結果待ち」・完了率「-」・判定なしで出し、★行を開けません★(`result_pending`。見え方は `cleanLogRowView()` `lib/format/clean-result.ts:98` の1か所。`CleanLogTable.tsx:212-293`)。★「問題あり(△×)」「×のみ」の絞り込みからは外れ、「全件」にだけ出ます★(`lib/clean-logs/query.ts:186`)。CSV も出発・清掃結果が「結果待ち」、完了率・判定は空のセルです。120分を過ぎたら今までどおり「出発失敗」。詳しくは [5.9b](#59b-結果待ち--レポートがまだ届いていない清掃d-173)
- ★`task_area` が 0 の清掃は完了率 0%(判定 ×)です★(`runCompletionRate()`。第11回 P-5=B。D-158)。旧(10/02〜10/05): 出発していても「-」
- - ★予定外のタスク(`clean_timetable_id` が NULL)はこの画面に出ません★(予定が起点のため。D-151 項7 で起点を検討中)
+ - ★予定外のタスク(`clean_timetable_id` が NULL か 0)も1行として出ます★(2026-10-29 から。指示書BG・D-187。旧: 予定が起点のため出なかった)。行の `id` は null・`row_key` は `rp-`(`report_id` は19桁の文字列)。予定の列は「予定なし」(D-188)。△× の行の ▼ は `GET /clean-logs/unscheduled/{report_id}/advice`(D-189)。エリア別詳細のボタンは予定のある行だけ
- ★取得中に絞り込みを変えたら、遅れて届いた古い応答は捨てます★(取得の effect の片付けで `cancelled` にする。エラー一覧と同じ。D-183)。★アドバイスの取得に失敗したら「アドバイスの取得に失敗しました」★(「該当なし」と区別。`adviceFailedIds`)、★予定ありの行では、「エリア別詳細を見る →」はアドバイスの有無・成否によらず出します。予定なしの行には出しません★(指示書BI / D-183)。★CSV は期間の誤り・読み込み中は押せません★(`csvUnavailable`。D-183)。エリア別モーダルの★結果待ちの行は完了率「-」・札の代わりに「結果待ち」・赤字にしない★(`result_pending`。D-182 / D-183)
---
@@ -653,7 +653,7 @@
- ★対応状況・備考・エラー内容は**編集モード**でだけ変えられます★(D-165 / 指示書BE。★対応状況と備考の動きは `/alerts`(3.5)と同じ★。エラー内容はエラー一覧だけ)。通常は表示だけ → 見出しの右(件数の右)の「編集」(読み込み中・0件・エラー表示中は押せない。`canStartEdit`)で入る → 対応状況はプルダウン(D-161。解消のときは下に解消日時(下書きで未解消・対応中に戻すと隠れる。D-181))、備考は入力欄、詳細の列はエラー内容(10/27 に種類の2行目から独立した列へ。長い文字は折り返す。指示書BE §12。選べる行はボタン → `ErrorContentPicker`)→ ★「保存(N件)」で変えた行だけを1行ずつ順に PATCH★(`saveEdit()` → `saveErrorDrafts()`)→ ★全部成功で通常モードに戻る・失敗した行は下書きを残して行の下に理由★。★その他(`note_required`)で備考が空の行は送らず、同じ件数に数える★。★編集中は絞り込みとページ送りを使えません★(`filtersDisabled`)。★下書きがあればページを離れる前に確認★(`beforeunload`)、「キャンセル」も確認してから捨てます(`cancelEdit()`)。★日時の規則は `/alerts` と同じ★。書けるのは4列に `error_list_id` を足した5列([章8](#8-データの出どころ) 8.12。D-184)。★保存に成功した行は返ってきた1行で差し替え、解消にした行も取り直すまで消えません★
- ★備考は1行で表示し、はみ出す分は「…」。全文は `title`(マウスを乗せる)★(`:635`。D-162)。★枠は列幅いっぱいの固定の大きさ★(D-164。旧: 入力した文字の長さの幅しかなかった)。編集欄も `resize: none`。★解消日時は 12px★(`.statusTime`。D-162)
- ★書けるのは `robot_error_reports` に行があるものすべて★(事前チェック以外・`robot_id` が NULL の行も)。サーバー側は行が無い id・大小文字だけ違う id・★削除済みの施設・清掃エリアの行(U8。D-180 項11)★に404
- - ★表は `table-layout: fixed`+``(`:611`)で JP/EN の切り替えで列幅が動きません★。★列幅は D-166 → 2026-10-27 に「詳細」の列を足して改めた(指示書BE §12)★: 検知時刻 120 / シリアル 140 / レベル 90 / 紐づく予定 110 / 対応状況 150(中のバッジ・プルダウンは 120px 固定。D-181) / 備考 120px を固定し、★施設名と種類は表の幅の 9%、詳細は残りの幅すべて(最小 196px)★。★表の最小幅 ≒1129px★((730 + 196) ÷ 0.82。旧 1130px・1156px。狭い画面では表だけ横スクロール)。値は `ErrorsScreen.module.css` の `.table`。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)。ただし★ツールバーは JP で「解消済みも表示」が2行目に回ります★(EN は1行)
+ - ★表は `table-layout: fixed`+``(`:611`)で JP/EN の切り替えで列幅が動きません★。★列幅は D-166 → 2026-10-27 に「詳細」の列を足して改めた(指示書BE §12)★: 検知時刻 120 / シリアル 140 / レベル 90 / 紐づく予定 110 / 対応状況 150(中のバッジ・プルダウンは 120px 固定。D-181) / 備考 223px を固定し、★施設名は表の幅の 9%・種類は 12%、詳細は残りの幅すべて(最小 60px)★。★表の最小幅 ≒1130px★((833 + 60) ÷ 0.79。旧 1132・1129・1156px。狭い画面では表だけ横スクロール。★割合を変えたら 0.79 も直す★。2026-10-27 の4回の手直しの経緯は CSS のコメント)。値は `ErrorsScreen.module.css` の `.table`。[3.15](#315-一覧画面に共通の見た目--分類の幅期間の入力表の列幅)。ただし★ツールバーは JP で「解消済みも表示」が2行目に回ります★(EN は1行)
- ★期間の入力欄は枠のどこを押してもカレンダーが開き、ホールディングス・顧客のプルダウンは 180px 固定★(D-162)
- ★i18n に「備考」→ "Note" を足したので、D-Secログの備考列の見出しも EN で "NOTE" になりました★(辞書は全画面で共有。7-8。★`f2b91fc` 時点でもそのまま★)
- ★契約によるメニューの出し分けはありません★(全メニューと同じ。3.0)
@@ -1166,7 +1166,7 @@
|---|---|---|
| 〜2026-09-29 | Σ清掃面積 ÷ Σ清掃対象面積(期間の合計どうしの面積比) | 旧 `CLAUDE.md` §4.2 |
| 2026-09-30〜10-01 | `clean_task_reports.clean_ratio` 列の単純平均 | D-151(指示書AQ 決定4) |
-| ★2026-10-02〜(現在)★ | ★**1回ごとの完了率**(`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て)の単純平均。予定外のタスクは除く★。★2026-10-05 から `task_area = 0` は 0%★ | ★D-155(ユーザー判断「一旦B」2026-10-02)/ 指示書AS → ★D-158 で確定(第11回 P-2=A・P-5=B)/ 指示書AU★ |
+| ★2026-10-02〜(現在)★ | ★**1回ごとの完了率**(`min(clean_area ÷ task_area × 100, 100)` を小数第1位まで切り捨て)の単純平均。予定外のタスク(0・NULL)も1件として入れる(2026-10-29 から。D-187。旧: 除く)★。★2026-10-05 から `task_area = 0` は 0%★ | ★D-155(ユーザー判断「一旦B」2026-10-02)/ 指示書AS → ★D-158 で確定(第11回 P-2=A・P-5=B)/ 指示書AU★ |
あわせて ◎○△× の境界も 2026-09-16 に変わっています(「超」→「以上」。D-139。章5-2)。
@@ -1325,15 +1325,18 @@
| 条件 | 扱い | 根拠 |
|---|---|---|
| ★本清掃(`clean_timetables.precheck = 0`)に対応する行だけ★ | 事前チェック(`precheck` が 60 / 30 など)の予定に紐づく行は数えない | TeamWorks 第10回 N-3「事前確認の結果は含めない。`precheck` が0のデータのみ集計対象」(`TeamWorks回答_第10回_20261002.md:75`)。`CLEAN_TIMETABLE_PRECHECK.main = 0`(`lib/config/index.ts:272-275`) |
-| ★`clean_timetable_id` が NULL の行(予定外のタスク)は完了率にも実施回数にも入れない★ | ただし★総清掃面積の面積の合計には入れる★ | D-155 項3。サマリが NULL を除く(N-3「当然 clean_timetable_id が NULL のデータも集計対象外」)のにそろえた。面積は実際に清掃した広さなので含める(ユーザー判断 2026-10-02) |
+| ★予定外(`clean_timetable_id` が NULL か 0)も1件として入れる(今の期間だけ)★ | 予定 +1・開始 +1・`status = 4` なら正常終了 +1・1回ごとの完了率を平均に入れる。総清掃面積の面積の合計にも入る。★確定期間は TeamWorks のサマリのまま(サマリは予定外を除く)★ | D-155 項3 → **D-187**(指示書BG・A案。2026-10-29)。旧: 完了率・実施回数には入れない |
| ★`task_area = 0` の行は1回ごとの完了率を 0%★ | 分子に 0 を足し、分母(件数)にも入れる。「実施」にも数える | ★第11回 P-5=B(D-158)/ 指示書AU 段階2★。旧: `null`(分子にも分母にも入れない。D-155 項1 / 指示書AS D4) |
-★予定外を外している仕組みは画面によって違います★:
+★予定外を数える仕組みは画面によって違います★(2026-10-29 から。D-187。旧: 予定から引くので「もともと入らない」):
-| 画面 | 予定外を外している仕組み |
+| 画面 | 予定外を数える仕組み |
|---|---|
-| 清掃詳細ログ・ダッシュボード・アラート・エリア別完了率・レポート(当期)・連続日数・要レスキュー | ★もともと入らない★ — どれも本清掃の予定(`clean_timetables` の `precheck = 0`)の id で `clean_task_reports` を引くので、NULL の行は `in` に一致しない。`latestReportByTimetableId()`(`lib/db/clean-log-mapping.ts:241-255`)も NULL の行を Map に入れない(`:247`) |
-| ★総清掃面積(`/clean-area`)★ | ★面積は全行の `groupBy`、完了率・実施回数は別に取り寄せた行のうち本清掃に紐づくものだけ★(`cleanAreaStatsFromRows()` `lib/db/clean-area-source.ts:287-327`。`:308-309` の2行が判定) |
+| 清掃詳細ログ・ダッシュボード | 予定から引いた行に、`loadUnscheduledCleanLogRows()`(`lib/db/unscheduled-clean-log-source.ts`)で読んだ予定外の行を足す(期間は `started_at`)。ダッシュボードの直近の候補は、同じ清掃エリアで開始が同時刻なら予定のある行を採る(`preferOnTie()` `lib/dashboard/latest-run.ts:59`) |
+| 連続日数・要レスキュー・連続問題施設 | `lib/db/consecutive-source.ts` が `UNSCHEDULED_TIMETABLE_ID_WHERE` で予定外を読み、施設×日付の結果に入れる(予定外だけの日もスケジュール存在日。`CLAUDE.md` §4.5) |
+| レポート(今の期間)・エラー内訳 | `findUnscheduledReports()` / `findUnscheduledErrors()`(`lib/db/unscheduled-report-source.ts`)で足す。★確定期間はサマリのまま★ |
+| ★総清掃面積(`/clean-area`)★ | 面積は全行の `groupBy`。完了率・実施回数は `scheduledTimetableId()` が null(0・NULL)の行を予定外1件=実施1回として数える(`cleanAreaStatsFromRows()` `lib/db/clean-area-source.ts`) |
+| アラート | ★出ない★ — 事前チェックの異常だけを出す(D-191) |
★予定は `clean_timetables` と `clean_timetable_histories` の両方から読みます★(TeamWorks 第10回 N-3「`clean_timetables` もしくは `clean_timetable_histories` の precheck が 0」/ 第11回 P-7=C。D-158 項4。指示書AU 段階3b。2026-10-05 から)。同じ id は `clean_timetables` 側を採ります(読み口は `lib/db/timetable-source.ts` の1か所。章2 の 2.4 の注)。
@@ -1634,7 +1637,7 @@
- 本清掃だけに絞り、★未来の予定を外します★(`consecutiveInputsFromRows()` `:235-265`)
- ★清掃詳細ログ・施設別パフォーマンス・連続問題施設・要レスキューの4経路が同じ1本を使います★。以前は2箇所に似たコードがあり、同じ「連続3日」が画面によって違う数になりました(D-103 / D-109)
-★**知っておくべき挙動(直すものではない)**★: `clean_task_reports.clean_timetable_id` が NULL の行(予定外のタスク)は、予定の id の `in` に一致しないので、ここには返って来ません(`:211-226` / `:373-377`)。予定外なので、どの予定の結果でもなく、完了率・実施回数に入らないのが正しい扱いです(D-155 項3)。ただし、予定された清掃の結果が何かの理由で NULL のまま届くと、その予定は「レポート行が無い」=★未実施に化けます★。例外もテスト失敗も出ないまま稼働率だけが静かに下がるので、未実施が多すぎるときは NULL の行数を疑ってください。
+★**知っておくべき挙動(直すものではない)**★: `clean_task_reports.clean_timetable_id` が NULL か 0 の行(予定外のタスク)は、予定の id の `in` に一致しないので、ここには返って来ません(`:211-226` / `:373-377`)。★予定外は別の読み口(`lib/db/unscheduled-clean-log-source.ts`・`consecutive-source.ts` の `UNSCHEDULED_TIMETABLE_ID_WHERE`)で足します★(2026-10-29 から。D-187。旧: 完了率・実施回数に入らないのが正しい扱いとしていた。D-155 項3)。ただし、予定された清掃の結果が何かの理由で NULL のまま届くと、その予定は「レポート行が無い」=★未実施に化けます★。例外もテスト失敗も出ないまま稼働率だけが静かに下がるので、未実施が多すぎるときは NULL の行数を疑ってください。
**変えると何が起きるか**: `isFutureSchedule()` の除外を外すと、まだ来ていない1週間ぶんの予定(`clean_timetables` には1週間分がまとめて登録される。D-106)が全部「出発失敗」になり、★全施設が常時 L3★ になります(D-106 / D-107 / D-110)。
diff --git a/docs/decisions.md b/docs/decisions.md
index ca85fcf..73f4253 100644
--- a/docs/decisions.md
+++ b/docs/decisions.md
@@ -13035,6 +13035,56 @@
6. ★M8 トークンは D-Watch の API と画面で発行・失効する★(既存の cookie 認証。生の値は発行の応答で1回だけ)。
★本番のサーバーは TeamWorks が構築保守する(D-143)ので、サーバーでスクリプトを動かす前提を置かない★(`scripts/ds-agent-token.ts` は作らない)
7. ★指示書AY の Q14・Q15 を採用★: 発行日時の読み取り `GET /api/ds-devices/{serial_number}/agent-token` → 200 `{ issued_at: string | null }`(★トークンの値は返さない★)/未発行の `DELETE` も 204(冪等)。★ブラウザ向け API は6本★(一覧・エクスポートの準備・ダウンロード・トークンの読み取り/発行/失効)
+- ★追記(2026-10-19 同日・4回目。制御盤の実測・更新の仕組み・第13回の提出・指示書AY の見直し)★:
+ 1. ★第13回を TeamWorks に提出した★(2026-10-19。ユーザー申告)。送ったのは `docs/handoff/pdf/DWatch_確認事項_第13回_20261019.pdf`(原稿 `docs/handoff/TeamWorks確認事項_第13回_20261019_送付版.md`・S-1〜S-6。生成・点検のスクリプトは `cb0241a`)。★回答が来たら受領日をここに書く★
+ 2. ★制御盤の開発機(T000000002)と本番機(例 DFAS010003_アピタ金沢文庫)を読み取りだけで確かめた★(設計書 §1.6・§1.6.1。本番機は Claude のツールからは安全確認で止まるため、ユーザーが打った結果)。
+ ログは `/home/dfa/log/<プログラム名>/` に4種類(約1.2MB)。本番機の `~/log` 直下の6ファイルは古い版の名残で対象外
+ 3. ★本番機の更新は毎日12:00 に root で `git fetch origin master` → `git reset --hard origin/master`(/home/dfa)・再起動なし★。届くのは master に push したもの
+ 4. ★更新スクリプトの改訂案★(設計書 §5.13.1。ユーザー要望「差分があって更新したら再起動、差分がなければそのまま」・再起動はラズパイごと)。
+ ★更新日の 12:00 過ぎに出うる生死報告なしのアラートは許容する★(ユーザー判断「この時間帯のアラートであればなんの問題もないのでスルーでいい」)。本番機への導入はラズパイの担当(未実施)
+ 5. ★指示書AY を `99e5818`(指示書AX・AZ・BA の実装の後)に合わせて見直した★(指示書AY §11-3)。D-Sec ログ画面(`DsLogScreen.tsx`)は AY の作成時から変わっていない。行番号のずれ・`request.signal` の参照先・モーダルの Esc/フォーカス/スクロールの扱い・「ログ」列の幅・fixtures の種類を実機に合わせることを直した。★AY は新しい D 番号を取らず、実装の記録は本項の後ろに足す★(★2026-11-05 訂正: 10-19 の BC 段階4(`bce6a93`)で編集モードが入った★)
+ - ★記録の事故★: 本項は一度書いたが、別のセッションの `cc09649`(D-181)のコミットで作業ツリーの未コミットの追記ごと上書きされて消えたので、書き直した(2026-10-19)
+- ★追記(2026-11-05。指示書AY を `7492cc5` に合わせて見直した)★: 詳細は指示書AY §11-4。★AY は新しい D 番号を取らない★(実装の記録は本項の後ろに足す)。
+ 1. ★前提が崩れていた4件★: ① D-Sec ログ画面に備考の編集モードが入った(BC 段階4 `bce6a93`。編集/保存(N件)/キャンセル・` |