Selaa lähdekoodia

docs(cockpit): 票46 三轮收口关闭 —— 真实后端浏览器走查 27/27 与地图索引同步

- 票 46 票据:三轮进度(实施/维护者返工/走查关闭)+ 走查收口评论;状态 resolved
- 走查证据 verify/artifacts/t46/:CDP 探针(可重入进入路径 + 单 evaluate 原子块,绕开并行会话 vite 重载打断)、5 张截图、walkthrough-result.json
- 真实后端 CBWS202610150001 / 设备 21119740(5 预警/66 报警/7 工单):KPI 计数条、五页签默认档案+图片并入、第 1/7 页·末页余 6·到边禁用、先筛后翻页码回 1、再点取消、展开四件事、图例图标与顶部状态栏同源、返回零重复取数、480px 无溢出、零报错
- map.md:票 46 索引行勾选 + 票 48 新行 + Decisions so far 追加 gist(HEAD 基线 plumbing 暂存,未触碰工作树里并行票 43/47 的在制编辑)
ChenJZ 5 päivää sitten
vanhempi
commit
cdeb1409e6
17 muutettua tiedostoa jossa 885 lisäystä ja 5 poistoa
  1. 8 4
      .scratch/cockpit-real-data-wayfinding/issues/46-untitled.md
  2. 4 1
      .scratch/cockpit-real-data-wayfinding/map.md
  3. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/01-point-level.png
  4. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/02-device-archive-tab.png
  5. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/03-alarms-page1.png
  6. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/04-alarms-last-page.png
  7. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/06-alarms-expanded.png
  8. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/07-back-and-reenter.png
  9. BIN
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/08-narrow-480.png
  10. 28 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/build-map-next.mjs
  11. 44 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose.mjs
  12. 46 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose2.mjs
  13. 39 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose3.mjs
  14. 15 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-find-point.mjs
  15. 42 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-real-counts.mjs
  16. 367 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs
  17. 292 0
      .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/walkthrough-result.json

+ 8 - 4
.scratch/cockpit-real-data-wayfinding/issues/46-untitled.md

@@ -24,12 +24,13 @@ Category: frontend-execution
 
 ## 进度
 
-- 2026-10-15 第一轮:已认领 → 已实施(消费侧 4 文件)→ 离线闸门 t46 32/32 + 变异 4/4 → 既有闸门回归全绿。改动未提交。
-- 2026-10-15 第二轮(维护者反馈返工):① 设备详情弃六块长滚动 → 标签页(设备档案默认,最新图片并入该页下方|最新指标|预警信息|报警信息|工单状态),预警/报警每页最多 10 条(上一页/下一页 + 页码);② 状态图例与一张图顶部视觉+词表双对齐(图标登记提为 legendIconFor 单源,详情内 5 处 StatusTag 全部绑定 icon)。t46 扩到 53/53、变异 11/11;18 组离线闸门回归全绿。改动仍未提交。
+- 2026-10-15 第一轮:已认领 → 已实施(消费侧 4 文件)→ 离线闸门 t46 32/32 + 变异 4/4 → 既有闸门回归全绿。
+- 2026-10-15 第二轮(维护者反馈返工):① 设备详情弃六块长滚动 → 标签页(设备档案默认,最新图片并入该页下方|最新指标|预警信息|报警信息|工单状态),预警/报警每页最多 10 条(上一页/下一页 + 页码);② 状态图例与一张图顶部视觉+词表双对齐(图标登记提为 legendIconFor 单源,详情内 5 处 StatusTag 全部绑定 icon)。t46 扩到 53/53、变异 11/11;18 组离线闸门回归全绿。
 - 2026-10-15 维护者反馈第 3 条(一张图底部管线颜色图例)→ 另建票 48,不在本票范围。
-- 下一步:浏览器走查(验收第 5 条,需维护者明确授权后执行);入库按票 33/36/37 裁剪纪律单独摘取本票 hunks。
+- 2026-10-15 第三轮(授权后浏览器走查,真实后端 real):CDP 探针 27/27 全过(目标 CBWS202610150001 / 设备 21119740:真实 5 预警/66 报警/7 工单)。KPI 计数条、五页签默认档案+图片并入、第 1/7 页·共 66 条、末页余 6 行到边禁用、66 条默认全收起、分类先筛后翻页码回 1、再点取消、展开四件事、详情 3 标签带图标且色键与顶部报警行 lv1 同源、返回再进零重复取数、480px 无溢出、零 pageerror/console error。证据 verify/artifacts/t46/。
+- 2026-10-15 本票关闭。源码与票据已随提交 2e27b29 推送 origin/master(ChenJZ)。
 
-Status: claimed
+Status: resolved
 Type: task
 Blocked by:
 Labels: ready-for-agent, wayfinder:task
@@ -113,5 +114,8 @@ Labels: ready-for-agent, wayfinder:task
 ### local — 2026-10-15T07:13:57.909Z
 返工完成(2026-10-15 第二轮,维护者反馈第 1/2 条):① 设备详情弃六块竖排长滚动,改标签页(设备档案=默认页且最新图片并入其键值行下方|最新指标|预警信息|报警信息|工单状态),页签带条数;预警/报警每页最多 10 条(上一页/下一页+「第 N/M 页 · 共 X 条」,越界页码夹回、单页不出现控件、换分类回第 1 页;工单不分页——原话只点名预警报警且工单条目自带时间线);② 状态图例与一张图顶部对齐(口径=视觉+词表双统一):图标登记从 legendVisuals 内联表提为 pointStatus 的 legendIconFor 单源,statusView/deviceStatusView 带 icon,StatusTag 加可选 icon prop,详情卡内 5 处状态标签全绑定;设备侧仍走票 44 的独立平行表(只借图标登记不借状态语义)。渲染计划仍六块独立分区(t11 D1/D9 不破)。过程:E10 断言逮到表头漏绑 icon;具名 import 拼错(EVENT→EQUIPMENT_CATEGORY_ORDER)文本守卫全绿、t41 SSR 才炸 → 新增 E11/E11b/E11c 按真实导出+双向清单判,M8/M8b 验穿;legendIconFor 第一版吃继承键(toString)改 hasOwnProperty。t46 53/53、变异 11/11;18 组离线闸门回归全绿。第 3 条(一张图底部管线颜色图例)另建票 48。改动仍未提交;浏览器走查按 AGENTS.md 未自动执行,票面留判据——票保持 open。
 
+### local — 2026-10-15T09:49:14.842Z
+浏览器走查完成并关闭(2026-10-15 第三轮,维护者授权,真实后端 real)。方式:一次性 CDP 探针(本仓票 26/33/36 手法;地图标记是 Cesium 实体,交互式会话点不到,且并行会话写 src 会触发 vite 整页重载——进入路径做成可重入、交互+读数合并进单个页内 evaluate 原子块)。目标 CBWS202610150001(唯一报警点,顶部状态栏「报警」筛选孤立出来 → setCameraFocus 飞近 → 真鼠标点票 47 DOM overlay 标记内部 SVG)/ 设备 21119740(真实 5 预警/66 报警/7 工单)。**27/27 全过**:① 点位摘要=状态卡+KPI 计数条(ALARM=1:lv1…真 0 照画),t41 钉住的 dd/b 形状原样;② 五页签默认档案、图片并入档案页(sections=archive,latestImage)、页签条数 5/66/7;③ 报警第 1/7 页·共 66 条、每页 10 行、末页余 6 行、到边按钮禁用、66 条默认全收起;④ chips all=66/4级=66、点分类后页码回第 1 页、回显「当前分类:4级(再点同一项取消)」、再点取消恢复全量;⑤ 展开一条报警见 数据时间/状态/来源/关联设备/编码/等级/量值/位置,值全非空白;⑥ 顶部状态栏 8 行全带图标、详情内 3 个状态标签全带图标且色键 lv1 与顶部报警行同源;⑦ 返回再进同一设备零重复取数(detail-aggregate 0→0)、视图回默认页签;⑧ 480px 窄面板 tabs/aside/body/chips 溢出量全 0;Z 应用侧零 pageerror、零 console error。证据 verify/artifacts/t46/(探针脚本 + 5 张截图 + walkthrough-result.json)。走查中修掉探针自身两处误判(顶部图例图标类名选择器错、C3 空集合 every 恒真需连行数一起判),非应用缺陷。源码提交 2e27b29 已推送 origin/master(ChenJZ)。本票关闭。
+
 
 ## Answer

+ 4 - 1
.scratch/cockpit-real-data-wayfinding/map.md

@@ -59,7 +59,8 @@ Labels: wayfinder:map
 - [ ] [43: 前端执行票 —— 工单堆叠图语义间隔与饼图交互](issues/43-untitled.md) — 调查工单堆叠图类别间小间隔是像素舍入/绘制缺陷还是设计语义(设计则加说明);补齐饼图/环图悬停放大、高亮、共享详情浮层与键盘焦点
 - [x] [44: 前端执行票 —— 类型分布降序与统计条视觉密度优化](issues/44-untitled.md) — **已收口(2026-10-15,维护者确认,closed)**:普查结论**无遗漏块**,新增 t44 完备性闸门(乱序载荷灌真实适配层 ×14 面板,RANK/FIXED 期望表双向锁定,103/103 + 变异 6/6);统计条密度落地(行内 8u→4u、删 8px 越界辉光、轨道发丝边、填充端帽+高光),EllipsisText/panel-action/data-bar-* 三接缝零改动;改动随 `bd45f27` 入库(本地未推送)
 - [ ] [45: 前端执行票 —— 场景监测点状态优先级、并行状态与告警特效](issues/45-map-marker-state-priority-and-effects.md) — 业务状态(工单处置>报警>预警>离线>正常)与附加状态(无关联/无设备/未发布)拆两条并行轴;无关联设备改黄绿色且不抢业务异常;强化预警/报警特效并让监测点产生特效
-- [ ] [46: 前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-point-device-detail-card-and-history-filters.md) — 详情面板优先用图表/卡片;设备卡片预警/报警/工单增加分类查询;历史条目可折叠且每条可展开详情
+- [x] [46: 前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-untitled.md) — **已实施并浏览器走查关闭(2026-10-15,提交 `2e27b29` 已推送)**。三轮:① 点位摘要卡片化(状态卡+设备计数 KPI,统计缺失不画假 0)+ 预警/报警按等级、工单按状态字典 0–7 分类 chips(词表全单源)+ 历史条目默认折叠、展开保留数据时间/状态/来源/关联对象;② 维护者返工:设备详情弃六块长滚动改**标签页**(档案默认页、最新图片并入该页|指标|预警信息|报警信息|工单状态),预警/报警**每页 10 条**分页;状态图例与一张图顶部**视觉+词表双对齐**(图标登记提为 `legendIconFor` 单源);③ 真实后端 CDP 走查 27/27(设备 21119740:66 报警 → 第 1/7 页·末页余 6·到边禁用·先筛后翻回第 1 页·再点取消·返回零重复取数·480px 无溢出·零报错)。t46 闸门 53/53 + 变异 11/11;证据 [verify/artifacts/t46/](../verify/artifacts/t46/)。
+- [ ] [48: 前端执行票 —— 一张图正下方居中持久显示管线颜色图例](issues/48-untitled.md) — 维护者 2026-10-15 新增(票 46 收口反馈第 3 条):● 燃气 ● 供水 ● 污水 ● 雨水 ● 窨井 持久居中图例;色值必须与 `PIPE_PALETTE` 同源不得另抄;`DRAINAGE_UNKNOWN`/`UNKNOWN` 两桶地图真会画出,是否列入须在票面给结论
 - [ ] [38: 前端执行票 —— 基础设施面板调整(基础档案同行 / 删除规模 / 运行信息定位修复 / 管理信息片区×行业图表)](issues/38-infrastructure-panel-adjustments.md)
 - [ ] [39: 前端执行票 —— 运行监测面板图表化(监测概览读数 / 监测分布分节)](issues/39-monitoring-charts.md)
 - [ ] [40: 前端执行票 —— 运行概况与监测点台账卡片化 + 定位修复](issues/40-operation-ledger-cards.md)
@@ -72,6 +73,8 @@ Labels: wayfinder:map
 
 ## Decisions so far
 
+- [前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-untitled.md) — **三轮收口(2026-10-15,真实后端走查 27/27 后关闭,提交 `2e27b29` 已推送)**。监测点摘要改状态卡+KPI 计数条(`pointDeviceCountsView`:统计缺失→不画,真 0→照画);预警/报警/工单加分类 chips(等级词表复用票 13 `eventLevelTag`、工单复用字典 0–7,认不出落 unknown 不猜);历史条目 `<details>` 默认折叠、展开必留数据时间/状态/来源/关联对象四件事;设备详情按维护者返工改**标签页**(档案默认页+图片并入、指标、预警信息、报警信息、工单状态;渲染计划仍六块独立分区不破 t11 D1/D9),预警/报警每页 10 条分页(`pagedRows` 越界夹回、单页不出现控件);状态图例与一张图顶部对齐=图标登记提为 `legendIconFor` 单源 + 详情 5 处 StatusTag 全绑定(设备侧仍走票 44 独立平行表,只借图标登记不借状态语义)。分类/页签/页码全为组件本地视图状态:不发请求、不动 selection、换设备复位。闸门 t46 53/53 + 变异 11/11(含 Windows 沙箱管道空输出的假信号排除记录);**教训**:具名 import 拼错文本守卫全绿、只有加载 .vue 的 t41 SSR 炸 → 新增"按真实导出+双向清单"判据(E11 系);`legendIconFor` 首版吃继承键(toString 命中原型方法)改 hasOwnProperty。
+
 - [前端执行票 —— 类型分布降序与统计条视觉密度优化](issues/44-untitled.md) — **已实施并关闭(2026-10-15,维护者确认收口)**。排查结论:**无遗漏块**,但票 37 只测一块不构成完备性证据——新增 [t44 普查闸门](verify/t44-distribution-ranking.mjs):乱序后端载荷灌真实适配层(14 面板视图 × 全部分布块),期望表**双向锁定**(表外新块必显式归类 RANK/FIXED 否则判红;表内块消失也判红),RANK 降序+并列保序、FIXED 固定语义序(状态/等级/年限/工单 0–7)不被强行降序且期望本身非单调才有牙;103/103 + [变异闸](verify/t44-mutations.mjs) 6/6 打红按字节还原。视觉密度:条形行内间距 8u→4u、**删 8px 越界辉光**(条段糊邻行的直接成因)、轨道 6u+发丝内描边、填充 ::after 高光+端帽装饰层;读数密度与交互不变,`EllipsisText`/`panel-action`/`data-bar-*` 三接缝由守卫锁定。改动随并行合批提交 `bd45f27` 入库(本地未推送,推送归维护者统筹)。**方法论**:「补齐遗漏块」的正确形状不是排查一次清单,而是把完备性做成机制——普查表双向锁定后,任何新分布块漏归类即红。
 
 - [管线构件 glTF 素材与实例化渲染(样条/角/阀门/支管四类)](issues/32-32.md) — 2026-10-15按用户“基本没有问题,可以收口”确认关闭功能票:四类示意件/GPU实例化、地面近景保线、排水network_level分色和缺径默认DN交付(前端`aa529db`、后端`7150d431`);部署字段、4K FPS及浏览器故障画面的未验证边界留在票面最终状态,不声称全门槛通过。

BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/01-point-level.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/02-device-archive-tab.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/03-alarms-page1.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/04-alarms-last-page.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/06-alarms-expanded.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/07-back-and-reenter.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/08-narrow-480.png


+ 28 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/build-map-next.mjs

@@ -0,0 +1,28 @@
+/**
+ * 票 46 收尾:map.md 的 HEAD blob + 只替换本票三处 → 输出到临时文件。
+ * 不动工作树(里面混有并行票 43/47 的未提交编辑)。
+ */
+import { execFileSync } from 'node:child_process'
+import { readFileSync, writeFileSync } from 'node:fs'
+
+const REPO = 'E:/City-Lifeline-Cockpit-TvT'
+const REL = '.scratch/cockpit-real-data-wayfinding/map.md'
+
+const head = execFileSync('git', ['-C', REPO, 'show', `HEAD:${REL}`], { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 })
+
+// ① 票 46 索引行:勾选 + 收口 gist(链接改指实际文件 46-untitled.md)
+const old46 = '- [ ] [46: 前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-point-device-detail-card-and-history-filters.md) — 详情面板优先用图表/卡片;设备卡片预警/报警/工单增加分类查询;历史条目可折叠且每条可展开详情'
+const new46 = '- [x] [46: 前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-untitled.md) — **已实施并浏览器走查关闭(2026-10-15,提交 `2e27b29` 已推送)**。三轮:① 点位摘要卡片化(状态卡+设备计数 KPI,统计缺失不画假 0)+ 预警/报警按等级、工单按状态字典 0–7 分类 chips(词表全单源)+ 历史条目默认折叠、展开保留数据时间/状态/来源/关联对象;② 维护者返工:设备详情弃六块长滚动改**标签页**(档案默认页、最新图片并入该页|指标|预警信息|报警信息|工单状态),预警/报警**每页 10 条**分页;状态图例与一张图顶部**视觉+词表双对齐**(图标登记提为 `legendIconFor` 单源);③ 真实后端 CDP 走查 27/27(设备 21119740:66 报警 → 第 1/7 页·末页余 6·到边禁用·先筛后翻回第 1 页·再点取消·返回零重复取数·480px 无溢出·零报错)。t46 闸门 53/53 + 变异 11/11;证据 [verify/artifacts/t46/](../verify/artifacts/t46/)。\n- [ ] [48: 前端执行票 —— 一张图正下方居中持久显示管线颜色图例](issues/48-untitled.md) — 维护者 2026-10-15 新增(票 46 收口反馈第 3 条):● 燃气 ● 供水 ● 污水 ● 雨水 ● 窨井 持久居中图例;色值必须与 `PIPE_PALETTE` 同源不得另抄;`DRAINAGE_UNKNOWN`/`UNKNOWN` 两桶地图真会画出,是否列入须在票面给结论'
+
+// ② Decisions so far 追加 gist(放在节头之后的第一行)
+const anchor = '## Decisions so far\n\n'
+const gist = '- [前端执行票 —— 监测点与设备详情卡片化、分类查询与历史折叠](issues/46-untitled.md) — **三轮收口(2026-10-15,真实后端走查 27/27 后关闭,提交 `2e27b29` 已推送)**。监测点摘要改状态卡+KPI 计数条(`pointDeviceCountsView`:统计缺失→不画,真 0→照画);预警/报警/工单加分类 chips(等级词表复用票 13 `eventLevelTag`、工单复用字典 0–7,认不出落 unknown 不猜);历史条目 `<details>` 默认折叠、展开必留数据时间/状态/来源/关联对象四件事;设备详情按维护者返工改**标签页**(档案默认页+图片并入、指标、预警信息、报警信息、工单状态;渲染计划仍六块独立分区不破 t11 D1/D9),预警/报警每页 10 条分页(`pagedRows` 越界夹回、单页不出现控件);状态图例与一张图顶部对齐=图标登记提为 `legendIconFor` 单源 + 详情 5 处 StatusTag 全绑定(设备侧仍走票 44 独立平行表,只借图标登记不借状态语义)。分类/页签/页码全为组件本地视图状态:不发请求、不动 selection、换设备复位。闸门 t46 53/53 + 变异 11/11(含 Windows 沙箱管道空输出的假信号排除记录);**教训**:具名 import 拼错文本守卫全绿、只有加载 .vue 的 t41 SSR 炸 → 新增"按真实导出+双向清单"判据(E11 系);`legendIconFor` 首版吃继承键(toString 命中原型方法)改 hasOwnProperty。\n\n'
+
+if (!head.includes(old46)) { console.error('MISS: 票46 索引行未在 HEAD 版中命中'); process.exit(1) }
+if (!head.includes(anchor)) { console.error('MISS: Decisions 节头未命中'); process.exit(1) }
+
+let out = head.replace(old46, new46)
+out = out.replace(anchor, anchor + gist)
+
+writeFileSync('E:/City-Lifeline-Cockpit-TvT/.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/map.next.md', out, 'utf8')
+console.log('OK bytes:', Buffer.byteLength(head), '->', Buffer.byteLength(out))

+ 44 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose.mjs

@@ -0,0 +1,44 @@
+/** 诊断:运行监测模式下,台账分页按钮与异常设备定位入口的真实 DOM 状态。 */
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
+
+const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
+const page = await browser.newPage()
+await page.goto('http://127.0.0.1:8000/#/cockpit', { timeout: 60000 })
+await sleep(2000)
+await page.click('[data-cockpit-intro]').catch(() => {})
+await page.waitForFunction(() => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''), { timeout: 60000 })
+await page.evaluate(() => { [...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '运行监测')?.click() })
+await sleep(3000)
+
+const info = await page.evaluate(() => {
+    const more = document.querySelector('[data-ledger-more]')
+    const locate = [...document.querySelectorAll('[data-locate-point]')].map((n) => n.dataset.locatePoint)
+    const cards = [...document.querySelectorAll('[data-ledger-card]')].map((n) => n.dataset.ledgerCard)
+    const panelIds = [...document.querySelectorAll('[data-panel-id]')].map((n) => n.dataset.panelId)
+    return {
+        hasMoreBtn: !!more,
+        moreText: more?.textContent?.trim() || '',
+        cards,
+        locateCodes: locate.slice(0, 20),
+        panelIds,
+        ledgerNotes: [...document.querySelectorAll('[data-panel-page-note]')].map((n) => n.textContent.trim()),
+    }
+})
+console.log(JSON.stringify(info, null, 2))
+
+// 试一次:点异常设备定位入口(21119740)看能否直接开设备级详情
+const clicked = await page.evaluate(() => {
+    const btn = [...document.querySelectorAll('[data-locate-point]')].find((n) => n.dataset.locatePoint === '21119740')
+    if (!btn) return 'no-button'
+    btn.click()
+    return 'clicked'
+})
+await sleep(4000)
+const after = await page.evaluate(() => {
+    const card = document.querySelector('[data-point-detail]')
+    return { clicked: card ? card.dataset.detailLevel : '(none)', device: card?.dataset.deviceCode || '', point: card?.dataset.pointCode || '' }
+})
+console.log('locate 21119740:', clicked, JSON.stringify(after))
+await browser.close()

+ 46 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose2.mjs

@@ -0,0 +1,46 @@
+/** 诊断 2:台账端点为什么没有 CBWS202610150001;地图覆盖层标记在 headless 里是否可点。 */
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
+const BASE = 'http://192.168.110.96:8301/api/cockpit/v1'
+const unwrap = (p) => (p && p.data && (p.code !== undefined || p.requestId) ? p.data : p)
+
+// ① 台账端点直查
+for (const q of ['?page=1&pageSize=20', '']) {
+    try {
+        const r = await fetch(`${BASE}/monitoring-points/ledger${q}`, { cache: 'no-store' })
+        const d = unwrap(await r.json())
+        const rows = d?.points || d?.items || d?.records || []
+        console.log(`ledger${q || '(default)'} http=${r.status} rows=${rows.length} codes=${rows.map((x) => x.pointCode || x.code || x.monitoringPointCode).join(',')} total=${d?.total ?? d?.pagination?.total}`)
+    } catch (e) { console.log(`ledger${q} ERR ${e.message}`) }
+}
+
+// ② headless 里覆盖层标记的可点性
+const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
+const page = await browser.newPage()
+await page.goto('http://127.0.0.1:8000/#/cockpit', { timeout: 60000 })
+await sleep(2000)
+await page.click('[data-cockpit-intro]').catch(() => {})
+await page.waitForFunction(() => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''), { timeout: 60000 })
+await sleep(4000)
+const overlay = await page.evaluate(() => {
+    const nodes = [...document.querySelectorAll('[data-marker-overlay-code]')]
+    const target = nodes.find((n) => n.dataset.markerOverlayCode === 'CBWS202610150001')
+    return {
+        total: nodes.length,
+        codes: nodes.map((n) => n.dataset.markerOverlayCode).slice(0, 20),
+        targetFound: !!target,
+        targetRect: target ? JSON.parse(JSON.stringify(target.getBoundingClientRect())) : null,
+    }
+})
+console.log('overlay:', JSON.stringify(overlay, null, 2))
+if (overlay.targetFound) {
+    await page.evaluate(() => document.querySelector('[data-marker-overlay-code="CBWS202610150001"]')?.click())
+    await sleep(3000)
+    const card = await page.evaluate(() => {
+        const c = document.querySelector('[data-point-detail]')
+        return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
+    })
+    console.log('after overlay click:', JSON.stringify(card))
+}
+await browser.close()

+ 39 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-dom-diagnose3.mjs

@@ -0,0 +1,39 @@
+/** 诊断 3:用 CDP 真鼠标事件点地图覆盖层标记(票 46 走查的进入路径)。 */
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
+
+const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
+const page = await browser.newPage()
+await page.goto('http://127.0.0.1:8000/#/cockpit', { timeout: 60000 })
+await sleep(2000)
+await page.click('[data-cockpit-intro]').catch(() => {})
+await page.waitForFunction(() => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''), { timeout: 60000 })
+await sleep(5000)
+
+const box = await page.evaluate(() => {
+    const node = document.querySelector('[data-marker-overlay-code="CBWS202610150001"]')
+    if (!node) return null
+    const inner = node.querySelector('svg') || node.firstElementChild || node
+    const r = inner.getBoundingClientRect()
+    const nr = node.getBoundingClientRect()
+    return { nodeRect: { x: nr.x, y: nr.y, w: nr.width, h: nr.height }, innerTag: inner.tagName, rect: { x: r.x, y: r.y, w: r.width, h: r.height }, cx: r.x + r.width / 2, cy: r.y + r.height / 2, pe: getComputedStyle(node).pointerEvents }
+})
+console.log('box:', JSON.stringify(box))
+
+if (box && Number.isFinite(box.cx)) {
+    const x = box.cx
+    const y = box.cy
+    await page._dispatchMouse('mouseMoved', x, y)
+    await sleep(120)
+    await page._dispatchMouse('mousePressed', x, y, { button: 'left', buttons: 1, clickCount: 1 })
+    await sleep(60)
+    await page._dispatchMouse('mouseReleased', x, y, { button: 'left', buttons: 0, clickCount: 1 })
+    await sleep(3500)
+    const card = await page.evaluate(() => {
+        const c = document.querySelector('[data-point-detail]')
+        return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode, kpis: [...c.querySelectorAll('[data-point-kpi]')].map((n) => `${n.dataset.pointKpi}=${n.querySelector('b')?.textContent?.trim()}`) } : null
+    })
+    console.log('after real-mouse click:', JSON.stringify(card))
+}
+await browser.close()

+ 15 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-find-point.mjs

@@ -0,0 +1,15 @@
+/**
+ * 走查前置只读探针:找出设备 21119740 挂在哪个监测点下(走查要点那个点位标记进详情)。
+ */
+const BASE = 'http://192.168.110.96:8301/api/cockpit/v1'
+const unwrap = (payload) => (payload && payload.data && (payload.code !== undefined || payload.requestId) ? payload.data : payload)
+
+const res = await fetch(`${BASE}/map-snapshot?page=1&pageSize=200`, { cache: 'no-store' })
+const data = unwrap(await res.json())
+const points = (data && data.monitoringPoints) || []
+console.log(`points=${points.length}`)
+for (const point of points) {
+    const codes = point.deviceCodes || []
+    const hit = codes.includes('21119740')
+    console.log(`${point.pointCode} | ${point.pointName} | status=${point.pointStatus} | devices=[${codes.join(',')}]${hit ? '   <== 含 21119740' : ''}`)
+}

+ 42 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-real-counts.mjs

@@ -0,0 +1,42 @@
+/**
+ * 走查前置只读探针:真实后端各设备的 预警/报警/工单 条数。
+ * 为什么不用 PowerShell:`@($null).Count` 恒为 1,会把"没有这一段"读成"有 1 条"(票 17 的教训)。
+ */
+const BASE = 'http://192.168.110.96:8301/api/cockpit/v1'
+
+const unwrap = (payload) => (payload && payload.data && (payload.code !== undefined || payload.requestId) ? payload.data : payload)
+
+const devices = ['21119740', 'TEST-CBYYL5432168', 'TEST-CNXHS0000001', 'TEST-TCLLJ002215', 'TC-PS-WS-202609020012']
+
+for (const code of devices) {
+    try {
+        const res = await fetch(`${BASE}/equipments/${encodeURIComponent(code)}/detail-aggregate`, { cache: 'no-store' })
+        const json = await res.json()
+        const data = unwrap(json)
+        const pick = (name) => {
+            const section = data && data[name]
+            const list = section && Array.isArray(section.data) ? section.data : null
+            return `${String((section && section.state) || '-')}/${list ? list.length : '-'}`
+        }
+        console.log(`${code} http=${res.status} code=${json.code} warnings=${pick('warnings')} alarms=${pick('alarms')} workOrders=${pick('workOrders')} archive=${pick('archive')}`)
+    } catch (error) {
+        console.log(`${code} ERR ${error.message}`)
+    }
+}
+
+// 未关联设备清单(真实快照里 kind=unassociated 的编码),找一台事件最多的进详情。
+try {
+    const res = await fetch(`${BASE}/map-snapshot?page=1&pageSize=200&pointStatus=UNASSOCIATED`, { cache: 'no-store' })
+    const json = unwrap(await res.json())
+    const list = (json && json.unassociatedEquipment) || []
+    console.log(`unassociated-in-snapshot=${list.length}`)
+    const codes = list.slice(0, 12).map((item) => item.equipmentCode || item.code).filter(Boolean)
+    for (const code of codes) {
+        const r = await fetch(`${BASE}/equipments/${encodeURIComponent(code)}/detail-aggregate`, { cache: 'no-store' })
+        const data = unwrap(await r.json())
+        const n = (name) => (data && Array.isArray(data[name]?.data) ? data[name].data.length : 0)
+        console.log(`  ${code} warnings=${n('warnings')} alarms=${n('alarms')} workOrders=${n('workOrders')}`)
+    }
+} catch (error) {
+    console.log(`snapshot probe ERR ${error.message}`)
+}

+ 367 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs

@@ -0,0 +1,367 @@
+/**
+ * 票 46 浏览器走查探针(真实后端 real,headless Chrome via CDP)。
+ *
+ * 为什么用探针而不是交互式会话:地图标记是 Cesium 实体(交互式会话点不到),
+ * 且并行会话写 src 会触发 vite 整页重载打断多步操作。本仓票 26/33/36 遇同题用同一手法。
+ * 针对"重载打断"的两层防御:
+ *   ① 进入路径可重入(ensurePointLevel / ensureDeviceTab:状态丢了就重新走进去);
+ *   ② 交互+读数合并进**单个页内 evaluate**(C/D/E、G 各一次往返),
+ *      把被重载打断的窗口从几十秒压到几秒;被打断则整块重试(幂等:全是视图操作)。
+ *
+ * 进入路径:顶部状态栏「报警」筛选(决策 02 冻结的接缝,把唯一报警点孤立出来)
+ * → setCameraFocus 飞近 → CDP 真鼠标点覆盖层标记的内部 SVG(宿主节点是 0×0 定位锚;
+ * 诊断 3 实测全球视角下邻近点位实体抢先拾取,按坐标重拾不认传入 code)。
+ * 目标:CBWS202610150001(设备 21119740:真实后端 5 预警/66 报警/7 工单)。
+ *
+ * 覆盖票面验收:
+ *   A 点位摘要卡片化(状态卡 + KPI 计数条 + t41 钉住的 dd/b 形状)
+ *   B 设备详情标签页(五页、默认档案、图片并入档案页、页签带条数)
+ *   C 报警分页(每页 10、第 1/7 页、末页余数 6、到边禁用、默认全收起)
+ *   D 分类查询(chips 计数、先筛后翻页码回 1、生效回显、再点取消)
+ *   E 历史折叠展开(数据时间/状态/来源/关联设备四件事、值非空白)
+ *   F 图例对齐(详情状态标签带图标、色键与一张图顶部状态栏同源)
+ *   G 返回再进(不重复取数、视图回默认页签)
+ *   H 窄面板 480px(页签/卡体不横向溢出)
+ *   Z 应用侧零 pageerror / 零 console error
+ *
+ * 运行(dev server 需已在 :8000):
+ *   node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs
+ */
+import { mkdir, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
+
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
+const URL = 'http://127.0.0.1:8000/#/cockpit'
+const OUT = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46')
+const POINT = 'CBWS202610150001'
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
+const results = []
+const shots = []
+function check(name, pass, evidence) {
+    results.push({ name, pass })
+    console.log(`${pass ? 'PASS' : 'FAIL'}  ${name}${evidence ? `  — ${evidence}` : ''}`)
+}
+
+async function main() {
+    await mkdir(OUT, { recursive: true })
+    const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
+    const page = await browser.newPage()
+    const pageErrors = []
+    const consoleErrors = []
+    page.on('pageerror', (e) => pageErrors.push(String(e)))
+    page.on('console', (m) => consoleErrors.push(String(m.text || '')))
+
+    await page.goto(URL, { timeout: 60000 })
+    await sleep(2500)
+    await page.click('[data-cockpit-intro]').catch(() => {})
+    await page.waitForFunction(
+        () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''),
+        { timeout: 60000 },
+    )
+
+    const shot = async (tag) => {
+        const b64 = await page.screenshot()
+        await writeFile(path.join(OUT, `${tag}.png`), Buffer.from(b64, 'base64'))
+        shots.push(`${tag}.png`)
+    }
+
+    /** 真鼠标点覆盖层标记的内部 SVG 中心;返回点后的详情层级/点位。 */
+    async function clickOverlay(code) {
+        const box = await page.evaluate((target) => {
+            const node = document.querySelector(`[data-marker-overlay-code="${target}"]`)
+            if (!node) return null
+            const inner = node.querySelector('svg') || node.firstElementChild || node
+            const r = inner.getBoundingClientRect()
+            return { cx: r.x + r.width / 2, cy: r.y + r.height / 2, w: r.width, h: r.height }
+        }, code)
+        if (!box || !(box.w > 0)) return null
+        await page._dispatchMouse('mouseMoved', box.cx, box.cy)
+        await sleep(120)
+        await page._dispatchMouse('mousePressed', box.cx, box.cy, { button: 'left', buttons: 1, clickCount: 1 })
+        await sleep(60)
+        await page._dispatchMouse('mouseReleased', box.cx, box.cy, { button: 'left', buttons: 0, clickCount: 1 })
+        await sleep(2500)
+        return page.evaluate(() => {
+            const c = document.querySelector('[data-point-detail]')
+            return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
+        })
+    }
+
+    let coordCache = null
+    /** 可重入:确保详情卡停在目标点位的点位级。 */
+    async function ensurePointLevel() {
+        for (let round = 0; round < 4; round += 1) {
+            const current = await page.evaluate(() => {
+                const c = document.querySelector('[data-point-detail]')
+                return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
+            })
+            if (current?.point === POINT && current?.level === 'point') return current
+            if (!coordCache) {
+                coordCache = await page.evaluate(async (code) => {
+                    const r = await fetch('http://192.168.110.96:8301/api/cockpit/v1/map-snapshot?page=1&pageSize=200&pointStatus=ALARM', { cache: 'no-store' })
+                    const j = await r.json()
+                    const d = j.data || j
+                    const found = (d.monitoringPoints || []).find((p) => (p.pointCode || p.code) === code)
+                    return found ? { lon: found.longitude, lat: found.latitude } : null
+                }, POINT)
+            }
+            const selected = await page.evaluate(() => document.querySelector('[data-legend-key="ALARM"]')?.dataset.statusSelected === 'yes')
+            if (!selected) {
+                await page.click('[data-legend-key="ALARM"]').catch(() => {})
+                await sleep(4000)
+            }
+            if (coordCache) {
+                await page.evaluate((c) => globalThis.__cockpitMap?.setCameraFocus({ lon: c.lon, lat: c.lat, height: 900 }) === true, coordCache)
+                await sleep(4000)
+            }
+            const hit = await clickOverlay(POINT)
+            if (hit?.point === POINT && hit?.level === 'point') return hit
+        }
+        return null
+    }
+
+    /**
+     * 可重入:确保在设备级并停在指定页签。
+     * 已在目标点位设备级时**只点页签按钮**——不重走标记拾取(详情浮层盖住标记,
+     * 真鼠标事件到不了覆盖层;实测 3 轮全被打断就是这个原因)。
+     */
+    async function ensureDeviceTab(tab) {
+        const read = () => page.evaluate(
+            (name) => ({
+                level: document.querySelector('[data-point-detail]')?.dataset.detailLevel || '',
+                point: document.querySelector('[data-point-detail]')?.dataset.pointCode || '',
+                active: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
+                hasSection: !!document.querySelector(`[data-equipment-section="${name}"]`),
+            }),
+            tab,
+        )
+        let state = await read()
+        if (state.level === 'equipment' && state.point === POINT) {
+            if (state.active !== tab) {
+                await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
+                await sleep(700)
+            }
+            state = await read()
+            if (state.active === tab && state.hasSection) return state
+        }
+        const opened = await ensurePointLevel()
+        if (opened?.point !== POINT) return { broken: true }
+        await page.evaluate(() => document.querySelector('[data-device-card]')?.click())
+        await page.waitForFunction(() => document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment', { timeout: 25000 }).catch(() => {})
+        await sleep(2000)
+        await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
+        await sleep(800)
+        return read()
+    }
+
+    /** 整块重试:ensure 状态 → 一次 evaluate 内完成全部交互+读数(重载打断则重来)。 */
+    async function atomicOnAlarms(body, rounds = 3) {
+        for (let round = 0; round < rounds; round += 1) {
+            try {
+                await ensureDeviceTab('alarms')
+                const out = await page.evaluate(body)
+                if (out && !out.__broken) return out
+            } catch {
+                /* 页被重载:整块重试 */
+            }
+            await sleep(800)
+        }
+        return { __broken: true }
+    }
+
+    // ---------------- A 点位摘要卡片化 ----------------
+    const opened = await ensurePointLevel()
+    check('A0-opened-target-point', opened?.point === POINT && opened?.level === 'point', `命中=${opened ? `${opened.point}/${opened.level}` : 'null'}`)
+    const level1 = await page.evaluate(() => {
+        const card = document.querySelector('[data-point-detail]')
+        const kpis = [...card.querySelectorAll('[data-point-kpi]')]
+        return {
+            hasSummary: !!card.querySelector('[data-point-summary]'),
+            codeShape: card.querySelector('[data-point-code-value]')?.tagName || '',
+            statusCard: !!card.querySelector('[data-point-status-card]'),
+            kpiCount: kpis.length,
+            kpi: kpis.map((n) => `${n.dataset.pointKpi}=${n.querySelector('b')?.textContent?.trim()}:${n.querySelector('b')?.dataset.pointKpiTone || ''}`),
+            deviceCount: card.querySelector('[data-point-device-count]')?.textContent?.trim(),
+            deviceCountTag: card.querySelector('[data-point-device-count]')?.tagName || '',
+            deviceCards: card.querySelectorAll('[data-device-card]').length,
+        }
+    })
+    check('A1-point-summary-card-ified', level1.hasSummary && level1.statusCard, JSON.stringify({ summary: level1.hasSummary, statusCard: level1.statusCard }))
+    check('A1b-kpi-strip-rendered', level1.kpiCount >= 5, level1.kpi.join(' '))
+    check('A1c-pinned-shapes-kept', level1.codeShape === 'DD' && level1.deviceCountTag === 'B', `code=<${level1.codeShape}> count=<${level1.deviceCountTag}>`)
+    await shot('01-point-level')
+
+    // ---------------- F 图例对齐(点位级:状态卡 + 设备卡片 >=2 个标签)----------------
+    // 顶部图例的图标类是 .map-holo__legend-icon(CockpitIcon),详情标签内是 .status-tag__icon。
+    const legend = await page.evaluate(() => {
+        const top = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => ({ key: n.dataset.legendKey, tone: n.querySelector('.status-tag')?.dataset.tone || '', icon: !!n.querySelector('.map-holo__legend-icon') }))
+        const card = [...document.querySelectorAll('[data-point-detail] .status-tag')].map((n) => ({ tone: n.dataset.tone, shape: n.dataset.shape, icon: !!n.querySelector('.status-tag__icon') }))
+        return { top, card }
+    })
+    const alarmRow = legend.top.find((r) => r.key === 'ALARM')
+    check('F1-top-legend-has-icon-rows', legend.top.length >= 8 && alarmRow?.icon === true, `顶部状态栏 ${legend.top.length} 行,报警行带图标=${alarmRow?.icon}`)
+    check('F2-detail-tags-carry-icon', legend.card.length >= 2 && legend.card.every((c) => c.icon === true), `详情内 ${legend.card.length} 个状态标签全部带图标=${legend.card.every((c) => c.icon)}`)
+    check('F3-tone-shared', legend.card.some((c) => c.tone === alarmRow?.tone), `详情标签色键 ${legend.card.map((c) => c.tone).join('/')} 与顶部报警行 ${alarmRow?.tone} 同源`)
+
+    // ---------------- B 进设备详情:标签页 ----------------
+    await ensureDeviceTab('archive')
+    const tabs = await page.evaluate(() => {
+        const btns = [...document.querySelectorAll('[data-equipment-tab]')]
+        const body = document.querySelector('[data-equipment-tab-body]')
+        return {
+            tabs: btns.map((b) => `${b.dataset.equipmentTab}:${b.getAttribute('aria-selected') === 'true' ? 'sel' : ''}:${b.querySelector('.equipment-tabs__count')?.textContent || ''}`),
+            activeTab: btns.find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
+            sections: [...body.querySelectorAll('[data-equipment-section]')].map((s) => s.dataset.equipmentSection),
+            deviceStatusIcon: !!document.querySelector('[data-device-status] .status-tag__icon'),
+        }
+    })
+    check('B1-five-tabs-default-archive', tabs.tabs.length === 5 && tabs.activeTab === 'archive', tabs.tabs.join(' | '))
+    check('B2-image-merged-into-archive-tab', tabs.sections.join(',') === 'archive,latestImage', `档案页含分区:${tabs.sections.join(',')}`)
+    check('B3-device-status-icon', tabs.deviceStatusIcon, '设备详情表头状态标签带图标')
+    await shot('02-device-archive-tab')
+
+    // ---------------- C+D+E 报警页:分页 / 分类 / 折叠展开(单 evaluate 原子块)----------------
+    const cde = await atomicOnAlarms(async () => {
+        const nap = (ms) => new Promise((r) => setTimeout(r, ms))
+        const sec = () => document.querySelector('[data-equipment-section="alarms"]')
+        if (!sec()) return { __broken: true }
+        const out = {}
+        out.tabCount = document.querySelector('[data-equipment-tab="alarms"]')?.querySelector('.equipment-tabs__count')?.textContent || ''
+
+        // C:第 1 页
+        const pager1 = sec().querySelector('[data-pager]')
+        out.page1 = {
+            rows: sec().querySelectorAll('[data-event]').length,
+            collapsedClosed: [...sec().querySelectorAll('[data-history-item]')].every((d) => !d.hasAttribute('open')),
+            pagerText: pager1 ? pager1.querySelector('[data-page-state]')?.textContent?.trim() : '(无控件)',
+            prevDisabled: pager1?.querySelector('[data-page-prev]')?.disabled,
+            nextDisabled: pager1?.querySelector('[data-page-next]')?.disabled,
+        }
+        // 翻到末页
+        for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(120) }
+        const last = sec().querySelector('[data-pager]')
+        out.lastPage = {
+            text: last?.querySelector('[data-page-state]')?.textContent?.trim(),
+            rows: sec().querySelectorAll('[data-event]').length,
+            nextDisabled: last?.querySelector('[data-page-next]')?.disabled,
+        }
+        // 回第 1 页
+        for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-prev]')?.click(); await nap(120) }
+
+        // D:点一个计数 >10 的分类 chip(此刻停在末页也验证"换分类回第 1 页")
+        for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(100) }
+        const chips = [...sec().querySelectorAll('[data-category-chip]')]
+        out.chips = chips.map((c) => `${c.dataset.categoryChip.split(':').pop()}=${c.querySelector('[data-category-count]')?.textContent}`)
+        const pick = chips.find((c) => !c.dataset.categoryChip.endsWith(':all') && Number(c.querySelector('[data-category-count]')?.textContent) > 10)
+        out.chipKey = pick?.dataset.categoryChip || ''
+        pick?.click()
+        await nap(300)
+        out.afterChip = {
+            rows: sec().querySelectorAll('[data-event]').length,
+            pager: sec().querySelector('[data-page-state]')?.textContent?.trim() || '(无控件)',
+            note: sec().querySelector('[data-category-note]')?.textContent?.trim() || '',
+            active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
+        }
+        // 再点同一 chip = 取消
+        sec().querySelector(`[data-category-chip="${out.chipKey}"]`)?.click()
+        await nap(300)
+        out.afterCancel = {
+            rows: sec().querySelectorAll('[data-event]').length,
+            note: sec().querySelector('[data-category-note]')?.textContent || '',
+            active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
+        }
+
+        // E:展开第一条
+        const item = sec().querySelector('[data-history-item]')
+        item?.querySelector('summary')?.click()
+        await nap(250)
+        out.expanded = item?.hasAttribute('open') === true
+        const d = sec().querySelector('[data-history-item][open] [data-history-detail]')
+        out.detail = {
+            labels: [...(d?.querySelectorAll('[data-detail-row]') || [])].map((n) => n.dataset.detailRow),
+            values: [...(d?.querySelectorAll('dd') || [])].map((n) => n.textContent.trim()),
+        }
+        return out
+    })
+    check('C0-atomic-block-completed', !cde.__broken, cde.__broken ? '整块 3 次都被重载打断(并行会话在写 src)' : '一次 evaluate 完成 C/D/E 全部交互与读数')
+    check('C1-tab-count-66-and-page10', cde.tabCount === '66' && cde.page1?.rows === 10, `页签计数=${cde.tabCount} 第1页行数=${cde.page1?.rows}`)
+    check('C2-pager-reads-page-and-total', /第\s*1\/7\s*页/.test(cde.page1?.pagerText || '') && /66/.test(cde.page1?.pagerText || ''), cde.page1?.pagerText)
+    // 空集合的 every 恒真 —— 必须连"确实有行"一起判(本仓"守卫比它要守的东西宽"的同一课)。
+    check('C3-default-collapsed', (cde.page1?.rows || 0) > 0 && cde.page1?.collapsedClosed === true, `第1页 ${cde.page1?.rows} 行全部默认收起=${cde.page1?.collapsedClosed}`)
+    check('C4-edge-disabled-prev', cde.page1?.prevDisabled === true && cde.page1?.nextDisabled === false, `上一页禁用=${cde.page1?.prevDisabled} 下一页禁用=${cde.page1?.nextDisabled}`)
+    check('C5-last-page-remainder', /第\s*7\/7\s*页/.test(cde.lastPage?.text || '') && cde.lastPage?.rows === 6 && cde.lastPage?.nextDisabled === true, `${cde.lastPage?.text} 行数=${cde.lastPage?.rows} 下一页禁用=${cde.lastPage?.nextDisabled}`)
+    check('D0-chips-counts', (cde.chips || []).length >= 2 && cde.chips.some((c) => c.startsWith('all=66')), (cde.chips || []).join(' '))
+    check('D1-chip-filters-and-resets-page', !!cde.chipKey && /第\s*1\//.test(cde.afterChip?.pager || '') && cde.afterChip?.active === cde.chipKey, `选中=${cde.afterChip?.active} 页码:${cde.afterChip?.pager} 行数=${cde.afterChip?.rows}`)
+    check('D2-active-note-visible', /再点同一项取消/.test(cde.afterChip?.note || ''), cde.afterChip?.note)
+    check('D3-reclick-cancels-filter', cde.afterCancel?.rows === 10 && cde.afterCancel?.note === '' && cde.afterCancel?.active?.endsWith(':all'), `取消后行数=${cde.afterCancel?.rows} 回显="${cde.afterCancel?.note}" 选中=${cde.afterCancel?.active}`)
+    const four = ['数据时间', '状态', '来源', '关联设备']
+    check('E1-expand-shows-four-facts', cde.expanded === true && four.every((l) => (cde.detail?.labels || []).includes(l)), `展开=${cde.expanded} 详情行:${(cde.detail?.labels || []).join('/')}`)
+    check('E2-values-not-blank', (cde.detail?.values || []).length > 0 && cde.detail.values.every((v) => v && v.length > 0), `值:${(cde.detail?.values || []).join(' | ').slice(0, 140)}`)
+    await shot('06-alarms-expanded')
+
+    // ---------------- G 返回再进:不重复取数、视图回默认(原子块)----------------
+    const g = await (async () => {
+        for (let round = 0; round < 3; round += 1) {
+            await ensureDeviceTab('alarms')
+            try {
+                const out = await page.evaluate(async () => {
+                    const nap = (ms) => new Promise((r) => setTimeout(r, ms))
+                    const before = performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length
+                    document.querySelector('[data-detail-back]')?.click()
+                    await nap(1200)
+                    document.querySelector('[data-device-card]')?.click()
+                    for (let i = 0; i < 30; i += 1) {
+                        await nap(200)
+                        if (document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment') break
+                    }
+                    return {
+                        before,
+                        after: performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length,
+                        activeTab: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
+                    }
+                })
+                if (out && out.after >= out.before) return out
+            } catch {
+                /* 重载打断:重试 */
+            }
+            await sleep(600)
+        }
+        return { __broken: true }
+    })()
+    check('G1-no-refetch-on-return', !g.__broken && g.after === g.before, `detail-aggregate 请求数 ${g.before} → ${g.after}`)
+    check('G2-view-state-back-to-default', g.activeTab === 'archive', `重进后停在默认页签 ${g.activeTab}(分类/页码也随组件重建归零)`)
+    await shot('07-back-and-reenter')
+
+    // ---------------- H 窄面板 480px ----------------
+    await ensureDeviceTab('alarms')
+    await page.conn.send('Emulation.setDeviceMetricsOverride', { width: 480, height: 820, deviceScaleFactor: 1, mobile: false }, page.sessionId)
+    await sleep(1500)
+    const narrow = await page.evaluate(() => {
+        const overflow = (sel) => { const el = document.querySelector(sel); return el ? { sw: el.scrollWidth, cw: el.clientWidth, over: el.scrollWidth - el.clientWidth } : null }
+        return { tabs: overflow('[data-equipment-tabs]'), aside: overflow('[data-point-detail]'), body: overflow('.equipment-sections'), chips: overflow('[data-category-bar]') }
+    })
+    check('H1-narrow-no-overflow', [narrow.tabs, narrow.aside, narrow.body, narrow.chips].every((n) => n && n.over <= 1), JSON.stringify(narrow))
+    await shot('08-narrow-480')
+    await page.conn.send('Emulation.clearDeviceMetricsOverride', {}, page.sessionId).catch(() => {})
+
+    // ---------------- Z 应用侧报错 ----------------
+    const appErrors = pageErrors.filter((e) => !/chrome-extension|baidu|Permissions policy/i.test(e))
+    const appConsole = consoleErrors.filter((e) => /error/i.test(e) && !/chrome-extension|baidu|Permissions policy|Video overlay|favicon/i.test(e))
+    check('Z1-no-pageerror', appErrors.length === 0, appErrors.slice(0, 3).join(' | '))
+    check('Z2-no-console-error', appConsole.length === 0, appConsole.slice(0, 3).join(' | '))
+
+    await writeFile(path.join(OUT, 'walkthrough-result.json'), JSON.stringify({ results, shots, level1, legend, tabs, cde, g, narrow }, null, 2))
+    const failed = results.filter((r) => !r.pass)
+    console.log('')
+    console.log(`T46 浏览器走查:${results.length - failed.length}/${results.length} 项通过。截图:${shots.join(', ')}`)
+    if (failed.length) console.log('未通过:' + failed.map((f) => f.name).join(', '))
+    await browser.close()
+    process.exitCode = failed.length ? 1 : 0
+}
+
+main().catch((err) => {
+    console.error('PROBE ERROR:', err)
+    process.exit(1)
+})

+ 292 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/walkthrough-result.json

@@ -0,0 +1,292 @@
+{
+  "results": [
+    {
+      "name": "A0-opened-target-point",
+      "pass": true
+    },
+    {
+      "name": "A1-point-summary-card-ified",
+      "pass": true
+    },
+    {
+      "name": "A1b-kpi-strip-rendered",
+      "pass": true
+    },
+    {
+      "name": "A1c-pinned-shapes-kept",
+      "pass": true
+    },
+    {
+      "name": "F1-top-legend-has-icon-rows",
+      "pass": true
+    },
+    {
+      "name": "F2-detail-tags-carry-icon",
+      "pass": true
+    },
+    {
+      "name": "F3-tone-shared",
+      "pass": true
+    },
+    {
+      "name": "B1-five-tabs-default-archive",
+      "pass": true
+    },
+    {
+      "name": "B2-image-merged-into-archive-tab",
+      "pass": true
+    },
+    {
+      "name": "B3-device-status-icon",
+      "pass": true
+    },
+    {
+      "name": "C0-atomic-block-completed",
+      "pass": true
+    },
+    {
+      "name": "C1-tab-count-66-and-page10",
+      "pass": true
+    },
+    {
+      "name": "C2-pager-reads-page-and-total",
+      "pass": true
+    },
+    {
+      "name": "C3-default-collapsed",
+      "pass": true
+    },
+    {
+      "name": "C4-edge-disabled-prev",
+      "pass": true
+    },
+    {
+      "name": "C5-last-page-remainder",
+      "pass": true
+    },
+    {
+      "name": "D0-chips-counts",
+      "pass": true
+    },
+    {
+      "name": "D1-chip-filters-and-resets-page",
+      "pass": true
+    },
+    {
+      "name": "D2-active-note-visible",
+      "pass": true
+    },
+    {
+      "name": "D3-reclick-cancels-filter",
+      "pass": true
+    },
+    {
+      "name": "E1-expand-shows-four-facts",
+      "pass": true
+    },
+    {
+      "name": "E2-values-not-blank",
+      "pass": true
+    },
+    {
+      "name": "G1-no-refetch-on-return",
+      "pass": true
+    },
+    {
+      "name": "G2-view-state-back-to-default",
+      "pass": true
+    },
+    {
+      "name": "H1-narrow-no-overflow",
+      "pass": true
+    },
+    {
+      "name": "Z1-no-pageerror",
+      "pass": true
+    },
+    {
+      "name": "Z2-no-console-error",
+      "pass": true
+    }
+  ],
+  "shots": [
+    "01-point-level.png",
+    "02-device-archive-tab.png",
+    "06-alarms-expanded.png",
+    "07-back-and-reenter.png",
+    "08-narrow-480.png"
+  ],
+  "level1": {
+    "hasSummary": true,
+    "codeShape": "DD",
+    "statusCard": true,
+    "kpiCount": 5,
+    "kpi": [
+      "ALARM=1:lv1",
+      "WARNING=0:lv2",
+      "WORK_ORDER=0:lv3",
+      "NORMAL=0:lv4",
+      "OFFLINE=0:point-offline"
+    ],
+    "deviceCount": "1",
+    "deviceCountTag": "B",
+    "deviceCards": 1
+  },
+  "legend": {
+    "top": [
+      {
+        "key": "ALARM",
+        "tone": "lv1",
+        "icon": true
+      },
+      {
+        "key": "WARNING",
+        "tone": "lv2",
+        "icon": true
+      },
+      {
+        "key": "WORK_ORDER",
+        "tone": "lv3",
+        "icon": true
+      },
+      {
+        "key": "NORMAL",
+        "tone": "lv4",
+        "icon": true
+      },
+      {
+        "key": "OFFLINE",
+        "tone": "point-offline",
+        "icon": true
+      },
+      {
+        "key": "EMPTY",
+        "tone": "point-empty",
+        "icon": true
+      },
+      {
+        "key": "UNKNOWN",
+        "tone": "point-unknown",
+        "icon": true
+      },
+      {
+        "key": "UNASSOCIATED",
+        "tone": "point-unassociated",
+        "icon": true
+      }
+    ],
+    "card": [
+      {
+        "tone": "lv1",
+        "shape": "triangle",
+        "icon": true
+      },
+      {
+        "tone": "lv1",
+        "shape": "triangle",
+        "icon": true
+      },
+      {
+        "tone": "lv1",
+        "shape": "triangle",
+        "icon": true
+      }
+    ]
+  },
+  "tabs": {
+    "tabs": [
+      "archive:sel:",
+      "metrics::",
+      "warnings::5",
+      "alarms::66",
+      "workOrders::7"
+    ],
+    "activeTab": "archive",
+    "sections": [
+      "archive",
+      "latestImage"
+    ],
+    "deviceStatusIcon": true
+  },
+  "cde": {
+    "tabCount": "66",
+    "page1": {
+      "rows": 10,
+      "collapsedClosed": true,
+      "pagerText": "第 1/7 页 · 共 66 条",
+      "prevDisabled": true,
+      "nextDisabled": false
+    },
+    "lastPage": {
+      "text": "第 7/7 页 · 共 66 条",
+      "rows": 6,
+      "nextDisabled": true
+    },
+    "chips": [
+      "all=66",
+      "4=66"
+    ],
+    "chipKey": "alarms:level:4",
+    "afterChip": {
+      "rows": 10,
+      "pager": "第 1/7 页 · 共 66 条",
+      "note": "当前分类:4级(再点同一项取消)",
+      "active": "alarms:level:4"
+    },
+    "afterCancel": {
+      "rows": 10,
+      "note": "",
+      "active": "alarms:all"
+    },
+    "expanded": true,
+    "detail": {
+      "labels": [
+        "数据时间",
+        "状态",
+        "来源",
+        "关联设备",
+        "编码",
+        "等级",
+        "量值",
+        "位置"
+      ],
+      "values": [
+        "2026-10-10T18:51:38+08:00",
+        "UNHANDLED",
+        "—",
+        "测试环境监测设备01",
+        "0b9f96b70ae8b3a07aaa89d51ae5e8fe",
+        "4 级",
+        "24 mg/L",
+        "湖南省怀化市沅陵县沅陵镇辰州东街35号"
+      ]
+    }
+  },
+  "g": {
+    "before": 0,
+    "after": 0,
+    "activeTab": "archive"
+  },
+  "narrow": {
+    "tabs": {
+      "sw": 396,
+      "cw": 396,
+      "over": 0
+    },
+    "aside": {
+      "sw": 426,
+      "cw": 426,
+      "over": 0
+    },
+    "body": {
+      "sw": 396,
+      "cw": 396,
+      "over": 0
+    },
+    "chips": {
+      "sw": 378,
+      "cw": 378,
+      "over": 0
+    }
+  }
+}