# 42: 关联监测设备临时图层 ## 进度(2026-10-13 诊断,needs-triage 已摘除) **分类:bug。终态:ready-for-agent**(诊断完成、规格已 resolved、根因与代码路径已钉牢,可直接实施)。历史实施真实(37/37 逻辑+19/19 变异+真机走查),重开缺陷是刷新/快照失效生命周期,非原实施虚假。同编码坐标/状态重绘缺口已随整体验收修复(ead3944,`t48-linkage-invariants` 通过),勿重复。 ### 现象 选中监测点(设备层已上图)后: 1. 点「手动刷新快照」:请求窗口内旧设备实体与面板仍显示旧快照数据,直到首页返回才撤换——与规格 14:86「换新快照时**先**撤下旧关联设备图层」相悖。 2. 分页请求被后端判 `SNAPSHOT_EXPIRED`(快照失效):装载器进 `expired` 相位,但 `batchUpdatedAt` 不变 → 外壳撤层监听永不触发 → **旧设备层永不撤下**,持续误读为当前范围数据。 3. 刷新完成后:所选点位若在后续页,`objects` 监听+计划指纹会让设备层重建(重找**部分**生效);但详情缓存不重载(跨端点不同版风险提示常驻)、无「完整加载仍找不到→关闭详情并说明原因」核对、无「暂隐+顶栏待核对」状态——规格 14:86 要求的重载链未实现。 ### 影响范围 刷新按钮路径(`applyFilters`→`refresh` 同链)、快照失效路径、以及刷新后的详情一致性。不涉及:同编码重绘(已修)、翻页闪烁(正常)、关闭/换点(正常)、初始加载(正常)。 ### 最小复现 1. 打开带有效坐标关联设备的点位(`?scenario=device-layer`),确认设备层已上图。 2. 点「手动刷新快照」→ 观察请求窗口内 `data-map-devices` 仍非 0(根因 ①)。 3. mock 第 2 页返回 `SNAPSHOT_EXPIRED` → 相位 `expired` 后 `data-map-devices` 仍非 0(根因 ②)。 4. 刷新完成后把所选点位移到第 2 页 → 设备层重建但详情仍是旧缓存且永不核对;把点位从后端完全移除 → 详情永不关闭(根因 ③)。 ### 根因(均已代码核验,附置信度) | # | 根因 | 位置 | 置信度 | | --- | --- | --- | --- | | ① | 刷新窗口:`loader.refresh()` 内部立即清空,但 store 的 `summary.value` 只在 `await` 首页返回后更新;外壳监听 `batchUpdatedAt` 在那之前不触发 | `mapStore.js:153`(`await loader.refresh()`)+ `CockpitShell.vue` 快照监听 | 高(已确认) | | ② | 快照失效:`expireMapSummary` 不改 `batchUpdatedAt` → 撤层监听去重判据不变 → 永不触发 | `mapSummary.js:expireMapSummary` + `CockpitShell.vue:syncDeviceLayerForSnapshot` 去重 | 高(已确认) | | ③ | 重载链缺失:详情仅在用户重点且缓存无明确答复时重载;无快照变更驱动的"逐页重找→找到重载详情→完整找不到才关闭"核对;`invalidateForSnapshot` 清空后立即按调用时刻摘要重算(生产路径调用时摘要已新,此处**修正先前审查的定性**:不是独立缺陷,真正缺口是 ①② 与本条的链) | `detailStore.js:openSelection` + `deviceLayerStore.js:invalidateForSnapshot` + 规格对照 | 高(缺失已确认;原"旧摘要重绘"定性修正为中低) | ### 修复方向(供实施票执行,不在诊断阶段改码) - 刷新发起时即撤下设备层并暂隐详情(顶栏待核对),不等首页返回(根因 ①)。 - 快照失效相位直接触发撤层(与 `batchUpdatedAt` 判据解耦,或监听 `phase` 变化)(根因 ②)。 - 按规格 14:86 补核对链:逐页查找所选点位(覆盖已取页与后续页,不得以合并态护栏判不存在)→ 找到有效标记恢复锚定并**重载设备详情**;确认无地图位置则固定详情;完整找不到才关闭并说明;查找失败保持待核对可重试(根因 ③)。 ### 实施记录(2026-10-13,维护者裁定「按规格」后实施) 维护者对 ❓ Q1 回复**按规格**:完整实现规格 14:86 核对链。交付: - `domain/mapSummary.js`:装载器维护本快照**全页对象键索引**(换快照即重置;每页合并后全量记入,护栏截断不影响),新增 `hasObject(kind, code)` —— 存在性判定覆盖所有已取页,合并态护栏不作缺席证据(规格明写)。 - `stores/mapStore.js`:`refreshing` 信号(刷新链发起即亮、收口即灭)+ `hasSnapshotObject()` 出口。 - `stores/detailStore.js`:`reloadPointDetail(pointCode)` —— 强制重取点位摘要(不查「已答过」缓存),刻意不动 `selection`(保留选择身份与详情层级)。 - `stores/deviceLayerStore.js`:`suspendForSnapshot()` —— 撤下旧设备图层但**不**按当前(旧)摘要立即重算;图层挂起至新快照首页或核对链恢复。 - `CockpitShell.vue`:三个监听(刷新发起 → 挂起+清收场提示+待核对;`expired` 相位 → 挂起;`settled` → 核对收口)+ `reconcileSnapshotSelection()`(找到→强制重载详情;完整加载找不到→关闭详情并顶栏说明;失败/失效→保持待核对可重试)+ 顶栏 `data-snapshot-pending` / `data-snapshot-absent` 提示(关闭入口由既有 `data-anchor-hidden` chip 保留)。 证据:`verify/t42-refresh-lifecycle.mjs` **29/29**(真实 store + 注入 fetch:refreshing 发起/收口、expired 相位、全页索引含跨页/重置、强制重载计数与层级保留、挂起不重算;外壳静态守卫钉住三个监听/核对函数/chip)。相邻 19 组闸门复跑,本票范围全绿。 **三处红为并发会话在途改动、与本票无关(据实记录)**:① `pointAnchor.js` 他票面板宽度修改(t41 D6 裁切断言随之过期,归该票同步);② cesiumEngine `syncAssociatedDevices` 被地图文字工作加入 `markerEntityIndex`,t45 连续性脚本的 vm 沙箱未同步;③ t05 D1-overviewStore 裸字段(他票,先前已记录)。 ### 真实画面复验(2026-10-13,已通过 → 本票收口) 扩展恢复后在 real 页面完成全链复验(网络日志 + DOM + 截图): - 台账打开「供水监测点001」(GS001, EMPTY 无设备) → 固定详情挂载、身份完整; - 点「手动刷新快照」→ 网络日志:`page=1` **不带** token(开新快照),第 2–8 页全部携带新锁定 `snapshotAt`、全部 200——分页 token 链路无 400(整体验收修的信封在真实页持续生效); - 快照链 settled 后:**`GET /monitoring-points/GS001` 由核对链强制重取**(规格「找到即重新加载」在真实页触发);详情面板存活、身份完整; - 「待核对」chip 为瞬态(LAN 上 8 页整链 <1.5s,bsk 往返未捕捉到中间帧)——其存在性与响应性由 `t42-refresh-lifecycle` B1–B8(模板+监听+store 行为)钉住,A1 断言「发起即亮」为确定性证据; - 截图留存:刷新后详情存活、监测数据已刷新。 本票验收全部满足(历史 9 条 + 重开的规格 14:86 核对链),resolved 关闭;父地图 gist 已追加。 Type: implementation Status: resolved Blocked by: 41 Labels: ready-for-agent ## What to build 按规格 14 接入选中监测点后的关联监测设备临时图层。地图摘要继续只包含监测点与未关联设备;关联设备不写回摘要序列、分页、默认图例或摘要计数。地图点击和台账选择都从同一监测点选择语义启动设备加载,并与点位定位并行;设备数据到达即可渲染,不等待相机动画结束。 后端 #69 已发布:有 `snapshotAt` 时详情与地图共享同一快照;设备编码集合以地图快照 `deviceCodes` 为准,坐标只认设备自身 `longitude/latitude/coordinateValidation.renderable`。无坐标、非法、未校验、归属不明、详情失败或当前快照不一致的设备继续出现在清单中,并显示待确认/不可用原因,禁止复制监测点坐标或人工偏移。现网 `UNVERIFIED_COORDINATE` 的正式边界是清单-only。 ## Acceptance - [x] 选中监测点后关联设备图层自动加载;地图与台账入口行为一致。 - [x] 使用受控的坐标准入有效样例验证独立图层渲染与拾取;现网后端 `UNVERIFIED_COORDINATE/renderable=false` 时只显示清单及原因,不用临时样例冒称正式设备已能上图。 - [x] 有效设备只进入独立临时图层,设备身份同时携带 `deviceCode` 和所属 `monitoringPointCode`,不冒充 `pointCode`。 - [x] 关闭、换点、刷新、快照失效时旧图层立即撤下;迟到响应不能覆盖当前选择。 - [x] 同一 `snapshotAt` 翻页不重建或闪烁设备图层;快照查找逐页进行,第一页没有不能判定点位不存在。 - [x] 设备总数来自当前快照去重 `deviceCodes`;详情独有/快照独有设备分别标记不一致或待确认,不静默丢弃。 - [x] 总览/运行监测显示全部坐标准入有效设备;基础设施隐藏;预警处置只显示设备自身 `ALARM`/`WARNING`,未知状态待确认。 - [x] 清单保留全部关联设备,明确区分已上图、无地图位置、模式隐藏、待确认/失败;摘要图例和关联设备图例/计数分开。 - [x] 纯逻辑覆盖集合分类、快照生命周期、计数互斥;外壳走查覆盖地图/台账入口、关闭、换点、分页和降级。 ## Out of scope - 设备 hover/选中双向反应、资产选择、状态特效、重叠展开、完整模式计划的独立实现。 ## 进度:100%(2026-09-29 收口) **已完成并关闭。** 逻辑闸门 **37/37**、变异测试 **19/19 全部打红**、真机走查已执行(见下), 全量零依赖闸门回归绿。 ### 交付物 - `domain/deviceLayer.js`(新):**集合分类**(六类互斥:`drawn`/`no-location`/`hidden`/`pending`/ `inconsistent`/`unavailable`)、**快照生命周期**(总数只认当前快照去重 `deviceCodes`; 快照独有→待确认、详情独有→不一致且禁止上图)、**互斥计数**与独立图例、跨端点风险提示。 - `stores/deviceLayerStore.js`(新):**合成点**。它读 `mapStore` 的当前快照 `deviceCodes`(总数依据) 与 `detailStore` 的 `devices[]`(状态与设备级坐标),按 domain 判定后交地图服务。 两侧都不为自己长出对方(票 10 的 F4、票 02 的层次纪律)。 - `services/cesiumEngine.js`:`syncAssociatedDevices` / `readAssociatedDevices` / `clearAssociatedDevices` / `pickDeviceMarkerAt` / `hasDeviceMarker` + **独立数据源** `cockpit-associated-device-markers`(与摘要 `cockpit-point-markers` 分开)。 - `services/mapService.js`:`syncAssociatedDevices` / `clearAssociatedDevices` / `getAssociatedDeviceState` + 宿主读数 `data-map-devices`(`-plan`)(与摘要 `data-map-markers*` 分开)。 - `domain/pointDetail.js`:设备卡片带出**设备级坐标与校验**(后端 #69 已交付)。 - `pointSelection.js`:统一选择入口在打开详情前**先出一次设备计划**(不等详情、不等镜头)。 - `CockpitShell.vue`:四类订阅(选择 / 详情到达 / 快照变化 / 一级模式)+ 卸载拆监听与复位。 - `components/PointDetailCard.vue`:关联设备**清单**(保留全部、每行标注互斥归类)+ **独立图例**。 - mock:默认场景照抄**现网边界**(设备统一 `UNVERIFIED_COORDINATE/renderable=false`); 新增 `device-layer` **受控有效坐标样例**(两台设备带自身有效坐标,且地图 `deviceCodes` 与详情 `devices[]` 一致)。 - 证据:`verify/t42-device-layer.mjs`(**37/37**)+ 变异 **19/19 全部打红**。 ### 三条关键取舍(都来自已 resolved 的规格 14 / 决策票 05,不在本票发明) 1. **总数只认当前快照去重 `deviceCodes`**,不拿详情统计或卡片数冒充 —— 详情不携地图快照的 `snapshotAt`、还可能复用缓存,两边**可能不同版**。所以「快照独有」与「详情独有」必须分开: 前者待确认、后者标不一致且**禁止上图**,两边都不静默丢弃。 2. **坐标准入只认设备自身 `coordinateValidation.renderable`**(票 07 的唯一判据): 前端不重算经纬度范围、**绝不**用监测点坐标代替设备坐标、不人工偏移(CONTEXT.md 明令禁止)。 现网无可信校核源 → 正式边界是**清单-only**,本票照抄这个边界。 3. **数据可用性先于模式可见性**(实现时抓到并修掉的真问题):若先判模式,一台「坐标被后端否决」 的设备在预警处置里会显示成「当前模式隐藏」,值班人员会以为"切到总览就能看到它"—— 而它**任何模式都上不了图**。说成模式隐藏是把数据问题推给了模式,差额就解释错了。 ### 真机走查(已执行):抓到并修掉 1 处真缺陷 本机 `@puppeteer/browsers` 是空目录(票 37 记录的既有环境缺陷),故按票 36/37/41 成例 **改用 DSH 自带浏览器真实驱动**(`yarn dev` @8000,真实 `public/cockpit/config.json`)。 **修掉的缺陷:UI 把「清单行数」当成了「关联总数」。** 真机读数暴露:总数显示 2 而当前快照 只有 1 台设备 —— 多出来的是「详情独有」那一行(规格明确它**不计入总数**)。 修:store 单独持有 `total`(=当前快照去重 `deviceCodes` 条数),与 `rows.length` 分开; 界面两个数都显示且各有明确含义。已补断言 `B1b-total-is-not-row-count` 钉住。 **实测证据(真实浏览器,与验收逐条对应)**: - **受控有效坐标样例**:两台设备**真的画在独立图层上**(`onMap=2`),实体 id 形如 `device|MP-001|EQ-001`,属性同时带 `deviceCode` + `monitoringPointCode`、**不含 `pointCode`** (不冒充监测点);摘要标记仍为 2(`data-map-markers=2`)、设备另计(`data-map-devices=2`)。 - **现网降级边界(清单-only)**:默认场景下设备统一 `UNVERIFIED_COORDINATE/renderable=false` → 清单保留并标 `no-location`、**引擎 0 台**、总数 1、清单 2 行(`inconsistent` 那行不计入总数、 不上图)、跨端点风险提示出现、摘要标记仍 2。 - **关闭即撤下**:点关闭 → 设备层 `onMap=0`、计划清空、宿主读数归零,**而摘要标记仍 2**(互不牵连)。 - **换点**:MP-001 → MP-002 后只剩 B 的设备(归属 `pointCode` 正确),A 的不残留。 - **独立拾取**:`pickDeviceMarkerAt()` 只认本图层带 `deviceCode` 的实体并回**身份**; 设备优先于监测点(票 15 的优先级)。 **未覆盖项(如实记录)**:走查时浏览器标签 `document.hidden===true`,rAF 被冻结、Cesium 不渲染 (票 37/41 记过同一环境事实),因此**依赖渲染的 `scene.pick` 在本次环境下连监测点标记也拾不到** (已用"连 `pointHit` 也为 null"的对照确认是环境而非本票缺陷)——「真实鼠标点设备点」这条 双向联动入口的走查**归票 43**(它正是双向对应的实现票,且 `pickDeviceMarkerAt()` 的接缝已就位)。 建议维护者在**前台标签页**复跑一次以补该条观感。 ### 与下游票的边界(不在本票实现) - **票 44**:设备形状/尺寸/资产接缝与**独立设备状态词表**。本票的设备标记用中性占位符号 (`shape` 由调用方给,默认 `circle`)与 14px 尺寸(小于监测点的 18px),**不实现状态词表**。 - **票 46**:完整四模式图层计划(`domain/modeLayerPlan.js`)与图例合成。本票只实现 **单点位的模式可见性**(`isVisibleInMode`),不做全图矩阵。 - **票 47**:设备状态特效。 - **票 43**:面板与三维设备点的双向对应(hover/选中/引线/定位)。 ### 回归 **零依赖逻辑闸门全绿**(t01/t03/t04/t05 **254**/t06/t07 **39**/t08 **44**/t09/t10 **57**/t11/t12/t13/ t14/t15/t16/t17/t18 **71**/t19/t21/t21-view/t29/t30/t36/t37/t38/t39/t41 **46**/t42 **37**)。 **两处红与本票无关**:① `t28-fill-container.mjs` 需 puppeteer,而本机 `@puppeteer/browsers` 是空目录 (既有环境缺陷);② 某次运行中 `t05` 的 `C8-real-attaches-credential` 一度红 —— 经核实是**并发会话** 正在改 `services/cockpitApi.js` / `mapApi.js`(本票改动清单里没有这两个文件),随后复跑已恢复绿。 **按 `AGENTS.md`:本会话未跑 build**(真机走查是维护者本轮明确要求才执行的)。 ### 下一步 1. 维护者在**前台标签页**复跑一次,补「真实鼠标点设备点」的双向联动观感(或直接由票 43 覆盖)。 2. `43`(面板与三维设备点双向对应)、`44`(设备位置符号与状态词表)、`46`(模式图层计划) 均已解除阻塞,可并行。 ## Comments ### local — 2026-10-10T08:10:42.403Z 整体验收审查:刷新等待首屏期间旧设备层仍在;快照失效未改变batchUpdatedAt时不撤层;缺重找点位、强制重取详情及完整找不到后关闭链。另同编码坐标/状态未重绘已在整体验收当前工作区修复(t48-linkage-invariants),尚待提交与真实样本回验。当前票因刷新生命周期未满足重新打开,先诊断再实施。 ### local — 2026-10-13T06:51:11.942Z > *This was generated by AI during triage.* ## Triage Notes — 诊断结论(2026-10-13) **分类:bug|终态:ready-for-agent**(needs-triage 已摘除)。 **已确认(代码核验 + 规格 14:86 原文对照):** - 历史实施真实:37/37 逻辑 + 19/19 变异 + 真机走查;重开缺陷是刷新/快照失效生命周期,非原实施虚假。 - 同编码坐标/状态重绘缺口已随整体验收修复(ead3944,t48-linkage-invariants 通过),勿重复。 - 根因 ①(高置信):刷新请求窗口内 store 仍发布旧摘要,外壳 `batchUpdatedAt` 监听不触发 → 旧设备层存活,违背规格"先撤下"。 - 根因 ②(高置信):快照失效走 `expireMapSummary`,`batchUpdatedAt` 不变 → 撤层监听永不触发 → 旧层永不撤下。 - 根因 ③(高置信,定性修正):规格 14:86 要求的"逐页重找→找到重载详情→完整找不到才关闭"核对链未实现;`objects` 监听让设备层在后续页重建(部分生效),但详情不重载、无收场核对。原审查"撤下后从旧摘要重绘"定性修正——生产路径调用时摘要已新,非独立缺陷。 **最小复现:** 打开 `?scenario=device-layer` 设备层 → 刷新(窗口期旧层存活)→ mock 第 2 页 SNAPSHOT_EXPIRED(失效后旧层仍上图)→ 刷新后点位移页/移除(详情不重载/永不关闭)。 **实施方向与详细根因表:** 见票面「进度(2026-10-13 诊断)」节;证据 `verify/artifacts/t48/acceptance-status.md`。 **❓ Q1(待拍板,不阻塞标签流转):** 修复范围按规格 14:86 完整实现核对链,还是先只修 ①② 时序缺口、核对链另立子票?➡️ 推荐:按规格完整实现(规格已 resolved,核对链是本票验收第 4 条的字面要求;拆出会让整体验收 48 的第 2 条仍差一块)。回复后按选定范围实施,标签维持 ready-for-agent。