What to build: 在运行监测一级模式下同时展示监测概览、监测分布、运行概况、监测点台账四个二级面板,并支持地图状态筛选和点位联动。
Blocked by: 06, 08
Status: resolved Labels: ready-for-agent
industry(含井盖)与 region 已真的作用到地图点位
(票 25 冻结的 map-snapshot 路径);状态维度按后端契约现状只作用于面板与台账、
界面如实说明不假装生效 —— map-snapshot 补 pointStatus 归票 25 的 I1 / BT-7,不阻塞本票交付。四块面板已实现并全部通过:逻辑闸门 71/71、真机走查 38/38。
收口口径(维护者 2026-09-28 裁定):本票的交付面是前端页面;
后端接口联调与补契约由其他票(票 25 的 I1/I2 与交付包 BT-7/BT-8)承接,
不阻塞后续页面开发(票 20 依赖本票,见其 Blocked by: 17, 18, 19)。
故本票按交付面收口。
两处按契约现状如实呈现、并已转交票 25 的边界(不是遗留缺陷,是已知且已声明的边界):
map-snapshot 不收 pointStatus(票 25 的 I1 / BT-7)。
面板与台账的状态筛选是真的;地图那半在面板上常驻说明,没有在前端对已加载标记做过滤
(那只对翻到的页生效,会造成"地图点少了而摘要说已上图 N 个"的自相矛盾)。
后端补入参后由票 25 的联调收口。/monitoring/overview 无该响应字段,
metricFreshness 只在 /overview/monitoring(票 20 的域)上(票 25 的 I2 / BT-8)。
本面板以「数据时间」呈现并写明区别,不伪造采集时间。口径由票 25 裁定。本票不再有前端待办;票 25 的裁定若要求前端改接线或改呈现,按新票/新裁定再开一轮。
domain/panelBlocks.js(新):共享板块积木(KPI / 分布 / 趋势 / 键值行 / 条目清单 / 状态筛选
五种块构造器 + 码表 + 数值工具 + 分区状态口语)。从票 17 的 infrastructurePanels.js 提出来 ——
票 17 判定「四块面板的形状是同一件事」,本票沿用同一形状,于是「怎么构造一个块」只有一份实现、
各票只写「放哪些块」。不提出来的话 monitoringPanels.js 得 import「基础设施」模块,
正是票 17 警告的耦合。domain/monitoringPanels.js(新):三份契约适配 + 四块视图计划(纯函数)。domain/panelViews.js(新):panelId → 视图构造器的唯一注册表(两个模式的 8 块)。services/monitoringApi.js(新):三个端点的唯一出口 + 状态码白名单(statusParam)。stores/monitoringStore.js(新):三块分区 + 台账渐进加载 + 徽标 N+1 + 定位接缝复用。components/PanelBlocks.vue(票 17 的 InfrastructurePanel.vue 改名):吃算好的 view,
不认识 panelId、不认识任何模式 —— 所以票 19/20 接进来不用改它。domain/filterOptions.js:新增 regionQuery / hasRegionMapping / REGION_MAP_GAP /
POINT_STATUS_KEYS / isKnownPointStatus / DEFAULT_POINT_STATUS。stores/filterStore.js:新增 pointStatus 条件(setPointStatus / togglePointStatus)。services/mapApi.js + stores/mapStore.js:map-snapshot 收三个筛选入参 + applyFilters() 换新快照重载。mocks/contractMocks.js:三端点契约样例(按请求查询串真的过滤/分页)+ monitoring-no-devices 场景。CockpitShell.vue:四块接真面板 + 三个筛选 watcher + 状态筛选出口 + regionMap 配置读取。1. 「最近采集时间」不在本面板的端点上 —— 票 24 的字段表把它记错了地方。
票 24 把 metricFreshness.updatedAt(CockpitOverviewMonitoringResponse)记在「监测概览」面板名下,
但那是 /overview/monitoring(总体概览模式的「运行监测」面板,票 20 的域)的字段;
本模式「监测概览」对应的 /monitoring/overview 只有信封级 updatedAt 与 freshness。
逐类读过后确认:metricFreshness 在整个 cockpit 包里只出现在 CockpitOverviewMonitoringResponse。
处理:面板用 updatedAt 呈现「数据时间」,并把与「采集时间」的区别写成 METRIC_FRESHNESS_GAP 由界面说明,
不伪造一个逐设备采集时间。
2. 状态筛选是真的,但它到不了一张图** —— 这是后端契约的现状,不是取舍。**
pointStatus 只有 /monitoring/distribution 收;map-snapshot 的三个入参是
industry/region/deviceType(控制器逐行读过)。而票 25 已冻结「全局筛选落到地图以 map-snapshot 为准、
服务端完成过滤」,所以状态维度今天没有落图路径。
处理(Q19 裁定):面板与台账的状态筛选做真的(后端真支持),地图那半如实标缺口
(面板上常驻一条说明),缺口写进票 25 与交付包草案。没有在前端对已加载标记做过滤 ——
那只对翻到的页生效,会出现「地图上点少了而摘要说已上图 N 个不变」的自相矛盾。
3. 「范围」在后端是无枚举约束的自由文本,所以它的映射必须由配置声明。
map-snapshot 对 region 只做 isBlank(region) ? null : region.trim()(无白名单、无校验),
真实取值是数据源透传的自由串(演示载荷里同时有 城区A/B/C、CN/CB/TC、未分区)。
前端范围键是 south/north/taichang —— 两者之间没有可依的映射。
处理:新增配置 map.filters.regionMap(键为前端范围键),默认空=不传参并在界面标缺口;
猜一套(如 south → CN)只会在真实数据是 城区A 时静默筛空,那比不筛更坏。
另注:后端把空区域归一为字面量 未分区(CockpitMapSnapshotService 的 UNASSIGNED_REGION),
它是真实取值、可以筛,不是空值。
4. 点位状态的中文词有两张同名不同义的字典,混用会把「无设备」画成「无数据」。
panelBlocks.js 的 CODE_TEXT 里 EMPTY 是新鲜度哨兵(「无数据」),
而 pointStatus.js 里 EMPTY 是点位状态(「无设备」)。所以状态分布的中文与色键只取自
票 09 那张表,CODE_TEXT 不碰点位状态词(已写进两个文件的抬头,并有 C2b 断言钉住)。
5. 分母为 0 的在线率不是 0%:那是最容易被照抄的虚假零值。
后端分母为 0 时回 onlineRate.state = "EMPTY" 且 value = "0.0000" —— 那个 0 是「没有分母」的占位。
照 value 直画就是「在线率 0.0%」,而真相是「当前范围内没有可参与计算的设备」。
处理:先按 state/denominator 收口,不适用时显示占位符 + 一句说明。
专项场景 monitoring-no-devices 把它逼出来(变体测试 M3/M14 确认守卫有牙)。
6. 「已实现但还没取数」与「还没实现」必须分开,否则界面会说谎。
外壳原先用「视图算出来没有」当判据决定走组件还是走占位,于是已接数据但仍在加载的面板
会被画成「面板占位 · panelId」—— 那是在说「这块还没做」。
处理:判据改成注册表成员资格 hasPanelView(panelId);panelMeta 三态分明
(面板占位 / 正在取数 / N 组读数)。这是自查时真的犯过的错,H9 断言与变体 M11/M12 钉住它。
逻辑闸门 71/71(verify/t18-monitoring-panels.mjs,八段):
node .scratch/cockpit-shell-panel-layout/verify/t18-monitoring-panels.mjs
setup() 合并而不覆盖、
状态筛选不下发到 map-snapshot。openPoint。对抗式自查(变异测试,14 条全部以断言失败打红闸门):
① 状态筛选错误地下发到 map-snapshot → F5;② region 未配置也硬发前端键 → E4/E4b/E5/F2;
③ 在线率照 value 直画(分母 0 → 虚假 0.0%)→ C1c;④ 台账换页改成替换 → G2/G3;
⑤ 徽标取不到给 0 → G5;⑥ 状态词改用新鲜度哨兵词表 → C2b;⑦ setup() 整体替换注入项 → F4;
⑧ 状态筛选也去重载地图 → H11;⑨ 台账详情路径换成裸对象 → H12;
⑩ 渲染层又按 panelId 分支 → H2;⑪⑫ 把「已实现但未取数」判成「面板占位」→ H9(两种改法各一次);
⑬ 复验 H12 收窄后的牙 → H12;⑭ 在线率虚假零值 → C1c。
两次守卫自身的缺陷(都是变异测试逮到的,都已修):
detailStore.openPoint:本 store 里 locateCode 也调它,
于是「把台账的详情路径换掉」这件事测不出来(变异 M9 仍然 62/62 全绿)。
已按函数体截取收窄 —— 本票第二次栽在「守卫比它要守的东西宽」上。回归 16 个零依赖逻辑闸门全绿(exit=0)(末轮复跑见下): t01 / t04 / t05 218 / t06 / t07 39 / t08 / t09 38 / t10 57 / t11 61 / t12 49 / t17 57 / t21 / t21-view / t29 / t30,t18 67(本票)。
monitoringStore.js 注释里写了后端裸字段 deviceStatistics),已修。G8 的接缝断言(hasInfrastructurePanel →
panelViewOf + PanelBlocks);票 17 要守的三条契约一字未改,仍是 57/57。data-infrastructure-panel → data-panel-blocks 等),
未执行(票 17 的走查属票 17 的验收面,本票不动它)。node .scratch/cockpit-shell-panel-layout/verify/t18-monitoring-panels-walkthrough.mjs
维护者授权后执行(起 yarn dev,Chrome 152 headless)。六组:A 正常场景(DOM 实证四块画出来、
零后端代号、趋势桶是短标签)、B 台账渐进加载 + 徽标 N+1、C 点卡片开同一张两级详情卡且不动相机、
D 筛选联动、E 一块坏不遮其它两块、F 空场景与分母为 0。
首轮 32/36,四条红。这一层的价值正在于此:三条是纯逻辑闸门不可能发现的真缺陷。
loadLedgerBadge 在 store 里写好了、视图也留了徽标位,
但没有任何人调它 —— 界面上永远停在「设备统计取数中…」。
逻辑闸门全绿是因为它只验到 store 的取数与状态机(G4/G4b/G5 直接调函数),
而"这个函数在界面上被触发了吗"根本没被问过。
修:PanelBlocks.vue 用 IntersectionObserver 观察卡片、露出来时 emit badge-needed,
外壳接到 monitoringStore.loadLedgerBadge();并补 H13 断言(变异 M15/M16 确认有牙)。monitoringOverviewPayload 完全忽略
industry/deviceType,永远回同一份固定载荷 —— 请求串里参数是对的,
所以 E5「筛选真的进请求串」照样绿,而画面纹丝不动(假绿的反面:请求对了、界面没变)。
修:mock 照后端语义真的按行业/类型过滤并重算点位/设备/在线率;补 F0/F0b 端到端断言
(变异 M17/M19 确认有牙)。window.__COCKPIT_MOCK_MONITORING__ = { pageSize: 3 }
(与票 08 地图摘要的 __COCKPIT_MOCK_MAP__ 同一套手法);补 F0c 断言(变异 M18 确认有牙)。blockTitles)。修 1 时我自己又引入一个真 bug(这一轮第二次红):ref 挂在 v-for 里的 div 上,
Vue 3 收回来的是数组 → listRef.value.querySelectorAll is not a function,
而且抛在 onMounted 里把整页后续挂载打断,连带 A8b/D1/D2/D3/E5/D6 一起红(66 → 26/36)。
已把 ref 改挂根元素,并把这段教训写进组件注释。
末轮 36/36 全绿。几条值得留证的实测数字:
设备 2 · 报警 1 · 预警 0 · 离线 0(进视口后补上);监测点 6→2 / 设备 9→3 / 在线率 88.9%→100%,一张图点位 MP-001,MP-002 → MP-002;aria-pressed=true、台账 2 → 1 张卡,而地图点位不变(后端不收该入参);aria-pressed=false(退回全部);三次「守卫比它要守的东西宽」(全部由变异测试逮到,都已按函数体/回调体收窄):
① H12 对全文匹配 detailStore.openPoint,被同文件 locateCode 满足 → 台账换路径测不出来;
② H13 第一版对全文匹配 emit('badge-needed'),被 IntersectionObserver 不存在时的退化分支满足
→ 拿掉观察器里的按需触发仍全绿(变异 M16);
③ 变异脚本第一版把「崩溃」当成红 → 已改成要求跑到断言汇总且因断言失败而红。
16 个零依赖逻辑闸门全绿(exit=0):t01 / t04 / t05 218 / t06 / t07 39 / t08 / t09 38 / t10 57 / t11 61 / t12 49 / t17 57 / t21 / t21-view / t29 / t30,t18 71(本票)。 真机走查 t18 38/38(本票,2026-09-28 复跑)。
**运行监测模式四块面板落地:三块各接一个后端面板级聚合读模型端点、三块各自成败
(一块坏了其余照常画出真实内容),契约适配与视图计划收在一层纯函数里;
台账与监测分布共用一次取数并按端点分页渐进加载,卡片徽标走按需 N+1;
「筛选联动地图」复用票 25 冻结的落图路径(map-snapshot 的 industry/region 入参,
四个正式行业含井盖)。**
四条边界如实划清,不发明业务:
① 「最近采集时间」不在本面板端点上(票 24 把它记到了 /overview/monitoring),
以「数据时间」呈现并写明区别,不伪造采集时间;
② 状态筛选到不了一张图(map-snapshot 不收 pointStatus)—— 面板与台账做真的、
地图那半标缺口,缺口已记票 25 与交付包;
③ 范围筛选需配置声明映射(后端 region 是无枚举约束的自由文本),
默认不下发并如实标缺口,不猜一套映射去静默筛空;
④ 点位状态词与新鲜度哨兵词是两张同名不同义的字典,状态分布只取票 09 那张表。
② 与 ① 都归票 25 维护(它已接手并补正了口径,见其「现行补契约清单」的 I1/I2 与「当前基线」)。
本票只记一句结论:map-snapshot 的 industry/region/deviceType 三入参现在已提交
(HEAD 含 MANHOLE);后端联调与补契约由票 25 承接,本票不自行断言后端状态。
票面七条验收按本票的交付面(前端页面)全部达成;第 1 条与第 5 条各有一处后端契约边界 (见进度区)—— 那两处已转交票 25(I1/I2、BT-7/BT-8),按维护者 2026-09-28 裁定 不阻塞后续页面开发,故本票收口(100% / resolved)。
已认领(Status: claimed)。本票无 needs-triage 标签、Blocked by: 06, 08 且两张前置票均已
resolved,故不经过诊断阶段,直接进入澄清 + 实施。
读现状时确认:票 17 已把两处「不该猜」的地方显式推给本票 —— 「范围/区域」的映射(其正文原句「那需要先裁定一套映射 —— 属票 18 的域,本票不预支」) 与面板内筛选入口的形态。
分三轮问完,全部拿到裁定。归纳(括号内为裁定结果):
pagination 渐进加载)map.filters.regionMap,默认空,键为前端 scope 键)filterStore,与行业/范围同级)filterStore)PanelBlocks.vue,同步改票 17 的引用与守卫面)Q7 与 Q19 的关系记一笔:Q7 原选「状态筛选也要作用到地图」,但后端 map-snapshot
不收该入参;Q12/Q19 随后把它收成「面板+台账先做真、地图那半标缺口」。
两轮之间维护者还选了 Q17=C(等后端补完再做状态筛选),与 Q7/Q12/Q13/Q16/Q18 冲突,
在 Q19 里裁定以 Q12/Q16/Q18 为准 —— 即 Q17=C 只对地图那半生效,面板与台账照做。
实施中自查逮到一处自己引入的真 bug:「已实现但还没取数」的面板被画成
「面板占位 · panelId」(判据错用了「视图算出来没有」而不是注册表成员资格)。
已修为 hasPanelView(panelId) + panelMeta 三态分明,并补 H9 断言与两条变异确认有牙。
t05 扩守卫面时当场抓到一处真违规:新 store 的注释里写了后端裸字段
deviceStatistics 与 monitoringPoints(守卫要求页面/store 零后端裸字段名),已改成领域词。
变异测试第二版才抓到 H12 的守卫比它要守的东西宽(对全文匹配 detailStore.openPoint,
被同文件另一处调用满足),已按函数体截取收窄。
隔了一轮回来读现状(后端已从 c5a5b87 推进到 355a8af,cockpit 区 115 个文件大改),
核出两条我之前交付的东西是坏的,两条都是"守卫比它要守的东西宽"的同一族:
缺陷 1:井盖(MANHOLE)已可筛选,我却按旧事实不传它。
git show HEAD 确认 MANHOLE 现是第四个正式行业,且全部 11 个 cockpit 服务都收
(含 map-snapshot,第 39 行),认不出的行业仍 400 但文案已改为
「must be GAS, WATER_SUPPLY, DRAINAGE or MANHOLE」。
而我当初按票 17 的结论故意不传井盖(INDUSTRY_CODES 里没有 manhole),
依据是「井盖在后端正式枚举里没有契约位」——这条依据已被后端推翻。
后果:全局筛选条上那个「井盖」按钮,对面板与地图完全不生效,而它正是验收第 5 条的一个维度。
修:INDUSTRY_CODES 加 manhole: 'MANHOLE';清掉 5 处过时注释;删掉已成死代码的
MANHOLE_INDUSTRY_GAP;样例补一个井盖点位与类型(否则筛选筛出空、看起来像坏了);
补 F0d(筛得出真数据)与走查 D2c/D2d(界面真的筛出东西且地图跟着变)。
票 17 的 E2 断言同步改(它守的正是那个已被推翻的假设,见 Q5 裁定)。
缺陷 2:map.filters.regionMap 是死代码 —— 范围筛选端到端从未生效。
我实测:normalizeConfig({map:{filters:{regionMap:{...}}}}) 的 map.filters === undefined
(归一化后的 map 键里根本没有 filters)。外壳读它,所以永远读到 {}。
而我当时的 E4 断言只单独测了 regionQuery() 并手喂映射表,从没验过配置真的送得到 ——
又一次"测了函数、没测通路"。修:把 filters.regionMap 补进配置契约
(builtinConfig + normalizeConfig,校验口径与兄弟项一致、脏值逐项丢弃并记 issue、
值 trim);补 E4c/E4d/E4e 三条从原始配置载荷出发的端到端断言。
变异测试(本轮新增 5 条,全部以断言失败打红):
M20 井盖映射退回「无契约位」→ E1/F0/F0d;M21 归一化器不认 filters → E4c/E4d;
M22 坏键原样留下 → E4d/E4e;M23 不做 trim → E4d;M24 样例拿掉井盖 → F0c/F0d。
过程中又逮到两处自己的判据缺陷:① E4d 原来只查 length === 1,
放过"坏键被原样留下"(M22 全绿)→ 已加坏键存在性检查;
② F0d 在样例被删时会抛 Cannot read properties of null(崩溃不是有效的红,
本票已三次栽在这上面)→ 已改成防御式取值、干净判失败。
另外 G3 的判据也从"点数又涨了"改成分页终态:分页窗口横跨「监测点 + 未关联设备」,
最后一页可能整页都是设备(点数不涨),原判据会把正常的到底误判成失败。
真机走查复跑 38/38(新增 D2c/D2d 井盖两条)。 期间修掉走查里一处钉在别人票上的断言:A8b 原来断言「切到预警处置 → 恰好 4 块占位」, 而票 19 已把预警处置接上真面板(0 占位)→ 假红。已改为只对运行监测断言 0、 其余模式的数量只作汇报 —— 别人的票推进不该让本票的走查变红(票 17 的 A5 栽过同一个坑)。
另记(非本票):复核期间 t04-config 曾因另一会话在途的票 36/38 改动短暂变红
(空配置应等于内置默认,差在 locate.point/device 预设形状),其改动落地后已恢复绿。
本票未触碰那部分。
维护者裁定:本票的交付面是前端页面;后端接口联调与补契约由其他票承接,不阻塞后续页面开发。 据此把两处后端契约边界转交票 25(它已接手并补正口径),本票按交付面收口:
map-snapshot 补 pointStatus);/overview/monitoring)。这两条在界面上都是如实呈现的(不是藏着):状态筛选那半在监测分布面板常驻一条说明、 没有在前端对已加载标记做过滤(那只对翻到的页生效,会造成"地图点少了而摘要说已上图 N 个" 的自相矛盾);采集时间以「数据时间」呈现并写明它与「采集时间」的区别,不伪造。
收口动作(已执行):
Status: claimed → resolved、进度 95% → 100%、验收清单七条按前端交付面全部勾选
(两处后端边界在勾选项里写明归属与现状,不含糊);map.md 的本票条目与 Notes 状态同步为「已关闭」。Blocked by: 17, 18, 19,三张现均已 resolved)。票面七条验收按前端交付面的判定: 第 2/3/4/6/7 条完全达成;第 1 条(采集时间)与第 5 条(状态筛选落图)各自的后端部分已转交票 25、 前端部分(如实呈现 + 不假装)已达成。本票不再有前端待办; 票 25 的裁定若要求前端改接线或改呈现,按新票/新裁定再开一轮。