# 23: 前端执行票 —— 地图快照分页 token 与 pointStatus 八桶契约对齐 ## 进度 已关闭。两个曾被 #29 阻塞的验收项(状态栏/饼图真实八桶计数、多选筛选后点数变化)已由后端票 [29](29-29.md) 在同一份真实运行环境(dataSource=real → `http://192.168.110.96:8301`)上以 [real-probe-new-runtime.json](../implementation/t29/real-probe-new-runtime.json)(45/45)与 [browser-verification.md](../implementation/t29/browser-verification.md) 收口:八桶 EMPTY=6、UNASSOCIATED=7 其余 0,状态栏与饼图同源;筛选 13→1(ALARM)→2(ALARM,WARNING),全局计数不变;`BOGUS`/`ALARM,BOGUS` 两端 HTTP 400 `INVALID_REQUEST`;分页 token 保留(`map-page2-token-retained`)。本会话在不重复触发构建/浏览器的前提下复核:① 前端源码未变(仅 `domain/mapSummary.js`:`toStatusDistribution` 八桶完整性 + 装载器缺 token 停住 / 第 2 页起必带锁定 `snapshotAt` / `SNAPSHOT_EXPIRED` 保留旧对象);② `filterOptions.js` 的 `statusFilterQuery` 白名单去重、丢弃 `BOGUS`,保证「不把脏值发给后端」;③ 重跑 [t23-map-contract](../verify/t23-map-contract.mjs) 11 项断言全过(token 传递与转义、八桶完整性、脏值剔除、缺 token 停住且保留对象、快照失效不归零)。按项目规则本会话未自动运行 build/dev/browser;真实浏览器验收未在本会话重跑,其在 #29 会话中的记录结果判定有效。 ## 来源 票 18 真实联调与浏览器取证(2026-10-10)。 ## 现象 1. **分页快照 token 丢失**:浏览器实测出现 `GET /map-snapshot?page=2&pageSize=2`(不带 `snapshotAt`),后端按契约返回 400 `SNAPSHOT_EXPIRED`,地图随即进入「快照已失效 · 请刷新」与 0 点位。首屏 `GET /map-snapshot?page=1&pageSize=2` 是正常签发 `snapshotAt` 的。 2. **状态筛选契约不一致**:真实 `map-snapshot` 响应**没有** `pointStatusDistribution`,`mapFilter.pointStatus` 也不回显;传 `pointStatus=ALARM` 与不传返回完全相同的 `total=13`(入参被忽略)。`UNKNOWN` 视为非法值返回 `INVALID_REQUEST`;`/monitoring/distribution` 对 `UNKNOWN` 与 `UNASSOCIATED` 均返回 400。非法值 `BOGUS` 在 `map-snapshot` 上返回 200 而不是 400(与前端白名单预期不一致)。 3. 界面表现(真人走查第 1 项):状态栏八个计数全部显示 `—`,分布面板提示「全局状态分布未取到(后端尚未发布八桶全局计数)」。 ## 范围 1. 前端分页:第 2 页起必须携带首屏锁定的 `snapshotAt`;快照失效按既有恢复纪律处理,不把整张图清空成 0 点位。 2. 与后端对齐八桶状态契约:六态 + `UNKNOWN` + `UNASSOCIATED`,多值逗号分隔、OR;`map-snapshot` 与 `/monitoring/distribution` 两端行为一致;非法值两侧一致拒绝。 3. 状态栏与分布面板在未取到全局计数时如实降级,取到后共用同一份数据(同源,决策 02 已冻结)。 ## 验收 浏览器实测:地图自动分批加载到完成且不出现快照失效;状态栏与分布面板显示真实八桶计数;多选状态筛选后地图点数随之变化;后端返回 `INVALID_REQUEST` 时前端不把脏值发出去。探针 `map-eight-status-buckets`、`status-*`、`invalid-*` 相关检查项全部通过。 ## 关联 发现于票 18;承接决策票 02 与规格 07。 ## Answer ### 诊断结论(2026-10-10 复核,两项现象归属不同) **① 分页 token:前端已正确传递,历史故障不再复现;本次仅补防护与回归。** 真实只读探针 `[t23-real-probe](../verify/t23-real-probe.mjs)`(`http://192.168.110.96:8301`,仅 GET)实测: | 检查 | 实测 | 判定 | | --- | --- | --- | | 第 1 页签发 token | `snapshotAt=2026-10-10T17:18:53.1160157+08:00`,`total=13` | 通过 | | 第 2 页带 token | HTTP 200 / `code=200` | 通过 | | 第 2 页不带 token | HTTP 400 / `category=SNAPSHOT_EXPIRED` | 后端规则通过 | | 前端装载器是否漏带 | `createMapSummaryLoader` 第 2 页起必带 `state.snapshot`(`domain/mapSummary.js`) | 票 18 第 3 点「上次故障未复现」属实 | 因此原现象 1 属**历史已消失的故障**,不能当成仍存在的缺陷;但缺 token 时前端会带着空 token 继续翻页这一点没有防护,本轮已补。 **② 八桶契约缺口在服务端,前端无法单方面交付。** | 真实实测 | 结果 | 与前端契约的差距 | | --- | --- | --- | | `map-snapshot` 响应含 `pointStatusDistribution` | **无** | 状态栏与饼图只能如实降级(决策 02 已允许 `available:false`) | | `mapFilter` 回显键 | `industry/district/districtName/districtFilter/deviceType/pointCodes/deviceCodes` | **无 `pointStatus`** | | `pointStatus=ALARM` 与不传 | `total` 均为 **13** | 入参被完全忽略,筛选不生效 | | `map-snapshot?pointStatus=UNKNOWN` | HTTP 200 | 规格要求可接受,但**实为忽略**而非匹配 | | `map-snapshot?pointStatus=UNASSOCIATED` | HTTP 200 | 同上 | | `map-snapshot?pointStatus=BOGUS` | HTTP 200 | **违反规格 07「认不出的值 400 `INVALID_REQUEST`」** | | `/monitoring/distribution?pointStatus=UNKNOWN` | HTTP 400 | 该端点只认六态单值 | 服务端源码复核一致:`CockpitMapSnapshotResponse` 无 `pointStatus` 入参与 `MapFilter.pointStatus`(`mapFilter` 六个键);`CockpitMonitoringDistributionService.resolvePointStatus` 只认六态单值。**前端已做的白名单过滤(`isKnownPointStatusBucket`)保证不会把脏值发出去,但真实环境「不传脏值」与「后端拒绝脏值」两件事无法由前端同时成立。** ### 本轮前端交付 1. **缺 token 不再翻页**:装载器在 `nextPage > 1` 且无锁定 `snapshotAt` 时进入失败态并停住(`reason='snapshot-token-missing'`),不发无效分页请求,也不清空已加载对象。 2. **八桶计数完整性校验**:`toStatusDistribution` 由「有数组即算已发布」改为**八桶齐全且计数为非负整数才 `available:true`**;缺桶、空数组、重复桶、非法桶、非整数或负数一律 `available:false`。这消除了「部分桶被当作真实零值」的风险——正是状态栏显示 `—` 的场景里最容易出现的口径造假。 3. **可复跑回归** `[t23-map-contract](../verify/t23-map-contract.mjs)`:首屏不带 token、第 2 页必带(经真实 transport 拼串断言,含 `+`/`/`/`:` 这类需要转义的 token)、`BOGUS` 被白名单剔除、缺桶/重复桶/负数不被接受、缺 token 停住且保留已加载对象、`SNAPSHOT_EXPIRED` 保留旧对象不归零。 ### 后端边界(按维护者裁定:本票不改后端仓库) 八桶的入参、回显、全局计数与非法值拒绝,全部落在 `E:\pipe-ner-server` 的响应类与服务类。前端无法在不伪造的前提下让界面显示真实八桶计数——本地过滤代替后端筛选已被本 map 的 Out of scope 明文禁止。因此真实八桶验收项由新开的后端执行票承接,前端验收只到「如实降级 + 不发脏值 + token 正确」。 **本票在真实八桶计数落地前不关闭。** ### 授权的真实浏览器验收(2026-10-10) 环境 `http://localhost:8001`,`dataSource: real`(本机 gitignored `public/cockpit/config.json` 指向 `http://192.168.110.96:8301/api/cockpit/v1`);捕获 `d57c3eed5ebd1`,**440 请求、0 错误**。 | 验收项 | 实测 | 判定 | | --- | --- | --- | | 自动分批到完成 | 5 个刷新周期,每周期 `page=1`(不带 token)→ `page=2…7`(**全部带 `snapshotAt`**),全部 HTTP 200 | **通过** | | token 转义正确 | `snapshotAt=2026-10-10T17%3A38%3A15.997194%2B08%3A00`(`:`→`%3A`、`+`→`%2B`) | **通过** | | 不出现快照失效 | 界面「地图摘要加载完成 · 已上图 0 个 · 无地图位置 13 个」;全程无 `SNAPSHOT_EXPIRED`、无 expired 态 | **通过** | | 状态栏与饼图同源降级 | 八桶计数全部显示 `—`(未取到),未把缺失桶画成 0 | **通过** | | 多选状态筛选后点数变化 | 无法验证:`pointStatus` 被服务端忽略,`ALARM` 与不传均 `total=13` | **受阻于 [29](29-29.md)** | | 不把脏值发给后端 | 契约回归断言 `BOGUS` 被白名单剔除(真实环境服务端对 `BOGUS` 回 200,前端侧仍不发) | **通过** | 同屏真实读数印证接线正常:设施底数 5276、管线里程 67.60944 km、行业数 4(燃气/供水/排水/窨井)、片区「未分区」5278。 ### 回归与零回归证明 - 新增 `[t23-map-contract](../verify/t23-map-contract.mjs)`:11 项断言通过(token 传递与转义、八桶完整性、`BOGUS` 剔除、缺 token 停住且保留已加载对象、`SNAPSHOT_EXPIRED` 不归零)。 - 本轮源码只改 `domain/mapSummary.js`(+19/−5)。 - 全量逻辑闸门 50 个脚本:**41 通过、9 失败**;把这一个文件的改动 `git stash` 后重跑,**失败集合完全相同**(缺 puppeteer-core 模块、票 15 遗留的 `MANAGEMENT_REGION_GAP` 断言等),即本轮**零回归**。 - 关键脚本:t08 逻辑 44/44、t15 11/11、t16、t23 全通过。 ### 下一步 等待 [29](29-29.md) 落地服务端八桶契约后,重跑 `[t23-real-probe](../verify/t23-real-probe.mjs)` 与浏览器验收,补「状态栏/饼图显示真实八桶计数」与「多选筛选后点数变化」两项,再关闭本票。 Status: resolved Assignee: codex Type: task Blocked by: #29 Labels: ready-for-agent, frontend-execution, wayfinder:task ## Comments ## Answer