docs(design): close Bell prototype review gaps
This commit is contained in:
@@ -56,6 +56,8 @@
|
||||
- 升级链、超时、联系人和时段可按租户/站点配置。
|
||||
- 至少两条独立投递路径,其中一条可绕过互联网。
|
||||
- 区分已发出、已送达、已看到;没有回执不能当成功。
|
||||
- Alert 与 Event 保持可导航的多对多关系;被规则抑制且没有创建 Alert 的 Event 不伪装为已投递。
|
||||
- 班次交接覆盖未 ack、处置中与升级中的 Alert;接班确认留痕,交接过程不暂停或重置升级链。
|
||||
- 静默必须限时且自动恢复,单次不超过 4 小时,无永久静默。
|
||||
- 业务预警与运维告警使用不同通道和值班配置。
|
||||
|
||||
|
||||
+18
-2
@@ -23,7 +23,7 @@
|
||||
- 角色:家属/值班员。
|
||||
- 目标:收到异常后查看关联证据、确认接手并记录处置结果。
|
||||
- 价值:把“发出通知”变成“有人负责”。
|
||||
- 验收:首次投递、送达、看到、ack、升级分别可追溯;无 ack 自动升级;进程重启不丢失升级链。
|
||||
- 验收:通道的已发出、已送达、已看到,与 Alert 的 ack、升级分别可追溯;无回执、通道不支持、可重试失败和最终失败不伪装成功;Alert 可查看聚合的 Event,Event 可查看触发的 Alert;并发 ack 时后到者看到真实处置人且不能覆盖;无 ack 自动升级,进程重启不丢失升级链。
|
||||
- 关联:RQ-C-17~RQ-C-30,IX-005~IX-008。
|
||||
|
||||
## US-004 标记误报
|
||||
@@ -39,7 +39,7 @@
|
||||
- 角色:租户管理员/站点管理员。
|
||||
- 目标:按站点和设备覆盖场景模板,配置区域、时段、持续时间和联系人升级链。
|
||||
- 价值:同一通用底座适配不同场景。
|
||||
- 验收:继承来源清楚、变更可试运行/回滚、静默不超过 4 小时且自动恢复。
|
||||
- 验收:继承来源清楚、变更可试运行;回滚先展示版本差异与影响,并创建新的不可变版本,不改写历史;静默不超过 4 小时且自动恢复。
|
||||
- 关联:RQ-C-11~RQ-C-15、RQ-C-23~RQ-C-28,IX-010~IX-012。
|
||||
|
||||
## US-006 事件最小权限查看
|
||||
@@ -82,6 +82,22 @@
|
||||
- 验收:Bell 是 Site/Area/配额/策略真相源;策略变更影响已有成像设备时必须显式选择迁移或取消,不自动停用;Sense 展示策略版本与同步状态,投影不可用时仅拒绝相关新写入。
|
||||
- 关联:RQ-S1-05、NFR-CMP-05,IX-016、IX-020。
|
||||
|
||||
## US-011 完成值班交接
|
||||
|
||||
- 角色:交班值班员/接班值班员。
|
||||
- 目标:在班次结束前把所有未 ack、处置中和升级中的 Alert 连同备注交给下一班,并得到明确接手确认。
|
||||
- 价值:保证“有人负责”不在班次边界中断。
|
||||
- 验收:交接清单显示当前状态、处置人与下一次升级时间;接班人确认后写审计;交接过程不暂停、不重置升级链,未确认交接时原责任关系保持不变。
|
||||
- 关联:RQ-C-17~RQ-C-30,IX-021。
|
||||
|
||||
## US-012 导出规则验收效果
|
||||
|
||||
- 角色:项目负责人/租户管理员/验收人员。
|
||||
- 目标:按规则版本和冻结样本窗口查看可复核的召回率与每路每天误报数。
|
||||
- 价值:让站点验收使用场景相关、可追溯的效果口径,避免跨场景“准确率”误导。
|
||||
- 验收:报表展示规则版本、样本时间窗、正样本/漏检样本、通道天数、召回率和每路每天误报数;明确不提供跨场景统一准确率;导出保留筛选与口径说明。
|
||||
- 关联:RQ-C-22、`02-requirements` §8,IX-022。
|
||||
|
||||
## 追溯规则
|
||||
|
||||
新增 P0 UI 任务必须引用至少一个 US 和一个 IX;若没有 UI,任务文件明确写“不适用”。需求变化先更新用户故事和交互清单,再改页面。
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
| IX-002 | 配额与批量结果 | 显示已用/上限;超 16 不等于非法,按站点配置校验;部分成功可重试;配额不可读取时只禁用新增/启用并保留已有列表与流 | US-001 | M2 |
|
||||
| IX-003 | 设备列表 | 分页/筛选/批量选择;128 路不一次加载所有视频和详情 | US-001 | M2/M4 |
|
||||
| IX-004 | 设备健康 | 期望态、媒体/推理/存储实际态、认证失败、最后在线完整时间与站点时区、时间漂移、分片、重试进度;运维告警独立,凭据更新只允许写入新值且旧值永不回显 | US-002 | M2/M4 |
|
||||
| IX-005 | 预警到达 | 明确严重度、站点、时间、证据可用性;重复投递不产生重复处置 | US-003 | M3 |
|
||||
| IX-006 | ack | 一次点击可确认,显示确认人/时间;并发 ack 有清晰结果 | US-003 | M3 |
|
||||
| IX-007 | 升级 | 显示当前层级、下一次升级时间、每次投递状态;失败不能伪装成功 | US-003 | M3 |
|
||||
| IX-008 | 事件详情 | 结构化事实、抓拍、视频、时间线、处置;证据加载失败可重试且不丢元数据 | US-003 | M3/M4 |
|
||||
| IX-005 | 预警到达 | 明确严重度、站点、时间、证据可用性与关联事件数;重复投递不产生重复处置 | US-003 | M3 |
|
||||
| IX-006 | ack | 一次点击可确认,显示确认人/时间;服务端首个成功者成为处置人,并发后到者看到当前处置人和冲突结果,不静默覆盖或显示双成功 | US-003 | M3 |
|
||||
| IX-007 | 升级与投递 | 显示当前层级、下一次升级时间;每个通道分别展示已发出、已送达、已看到,Alert ack 单独展示;无回执/不支持/未知、可重试失败及最终失败不得伪装成功 | US-003 | M3 |
|
||||
| IX-008 | 事件详情 | 结构化事实、抓拍、视频、时间线、处置;证据加载失败可重试且不丢元数据;Alert 展示关联 Event 与聚合原因,Event 展示触发的 Alert 与最终状态,两边可深链;被抑制且未创建 Alert 的事件明确标为未触发 | US-003 | M3/M4 |
|
||||
| IX-009 | 误报反馈 | 确认动作、可选原因、提交成功反馈;只改变 outcome,不改原始事件 | US-004 | M3 |
|
||||
| IX-010 | 规则编辑 | 显示租户/站点/设备继承来源,支持区域/警戒线/时段/持续时间 | US-005 | M3/M4 |
|
||||
| IX-011 | 规则试运行 | 明确“未正式生效”,展示命中样本与影响范围,支持取消/回滚 | US-005 | M4 |
|
||||
| IX-011 | 规则试运行与回滚 | 明确“未正式生效”,展示命中样本与影响范围,支持取消;回滚前展示目标版本差异与影响,确认后创建并发布等价的新版本,历史版本保持不可变;版本冲突时阻止覆盖并提供刷新/另存路径 | US-005 | M4 |
|
||||
| IX-012 | 升级链/静默 | 联系人顺序、超时、双通道;静默最长 4h、显示自动恢复时间、无永久选项 | US-005 | M3/M4 |
|
||||
| IX-013 | 权限与隐私 | 越权统一处理;无授权租户不展示人脸入口;不泄露流 URL/凭据 | US-006 | M3/M4 |
|
||||
| IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;能力同时显示厂商/型号/固件/认证、探测来源与时间、探测值和生效值差异;视频设备显示画面/媒体/检测区域,未交付的非视频适配器显示 `adapter_not_ready`,不用虚构遥测或禁用的视频页签占位 | US-008 | M2/M6 |
|
||||
@@ -23,7 +23,9 @@
|
||||
| IX-017 | 检测区域几何编辑 | 多边形和方向警戒线支持绘制、显式完成、撤销、清空、顶点编辑;警戒线在草稿/已保存态均显示方向箭头并可反转;鼠标与键盘坐标操作等效;画面参数变化后标记待校准且不自动改坐标;区域版本与规则版本独立 | US-005 | M3 |
|
||||
| IX-018 | 草稿与跨系统上下文 | 几何草稿自动保存并可恢复;只有持久化失败或切换空间类型等会破坏草稿的动作才拦截确认;Sense↔Bell 双向携带并消费 `camera + zone + return_to` 上下文;返回后恢复草稿,版本已被他人更新时提示冲突且不覆盖 | US-005 | M3 |
|
||||
| IX-019 | 运维中心与写入降级 | Sense 用一个一级“运维中心”聚合对账差异/退避/下次重试/孤儿安全闸、媒体与推理分片、边缘节点/隧道/补传、运维告警和系统状态;配额或 Area 策略不可读取时页面继续可读且已有链路不受影响,只禁用相关写操作;不得混入 Bell 业务预警 | US-002、US-008 | M2/M4/M6 |
|
||||
| IX-020 | 管理上下文、RBAC 与审计归属 | 全局头部显示租户、站点、账号和角色;只读角色隐藏写操作,深链越权统一拒绝且不泄露资源是否存在;Tenant/Site/Area/RBAC/配额/全局审计由 Bell 管理,Sense 只呈现消费上下文和设备操作入口,携带 tenant/site/device/return_to 深链到 Bell 审计 | US-006、US-009、US-010 | M2/M4 |
|
||||
| IX-020 | 管理上下文、RBAC 与审计归属 | 全局头部显示租户、站点、账号和角色;只读角色隐藏写操作,深链越权统一拒绝且不泄露资源是否存在;Tenant/Site/Area/RBAC/配额/全局审计由 Bell 管理,Sense 只呈现消费上下文和设备操作入口,携带 tenant/site/device/return_to 深链到 Bell 审计;Bell 移动端底部导航不超过 5 项,并通过可发现的“管理”入口访问 Site/Area 与审计 | US-006、US-009、US-010 | M2/M4 |
|
||||
| IX-021 | 值班交接 | 清单覆盖未 ack、处置中和升级中的 Alert,显示交班人、接班人、备注及下一次升级时间;接班确认写审计,交接期间不暂停或重置升级链,未确认时不转移责任 | US-011 | M3 |
|
||||
| IX-022 | 规则验收报表 | 按规则版本与冻结样本窗口展示召回率、每路每天误报数和计算样本量;支持保留口径的导出;不提供跨场景统一准确率 | US-012 | M4 |
|
||||
|
||||
## 全局状态
|
||||
|
||||
|
||||
+384
-67
@@ -79,6 +79,7 @@
|
||||
.btn-danger { background: rgba(251,113,133,.1); border-color: rgba(251,113,133,.38); color: #ffb4c2; }
|
||||
.btn-quiet { border-color: transparent; background: transparent; color: var(--muted); }
|
||||
.icon-btn { width: 42px; padding: 0; }
|
||||
.text-button { min-height: 32px; padding: 2px 6px; border: 0; background: transparent; color: #b8e5ff; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.select, .field, .textarea { width: 100%; min-height: 40px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); }
|
||||
.textarea { min-height: 80px; resize: vertical; }
|
||||
.top-actions .select { width: auto; min-width: 150px; }
|
||||
@@ -132,10 +133,18 @@
|
||||
.fact span { display: block; color: var(--muted); font-size: 11px; }
|
||||
.fact strong { display: block; margin-top: 3px; font-size: 13px; font-variant-numeric: tabular-nums; }
|
||||
.privacy-note { margin: 0 12px 12px; padding: 9px 10px; display: flex; gap: 8px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); font-size: 12px; }
|
||||
.relation-panel { margin: 0 12px 12px; padding: 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
|
||||
.relation-panel h3 { margin: 0 0 8px; font-size: 13px; }
|
||||
.relation-list { display: grid; gap: 7px; }
|
||||
.relation-item { min-height: 40px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text); display: flex; align-items: center; justify-content: space-between; gap: 10px; text-align: left; }
|
||||
.relation-item span { color: var(--muted); font-size: 12px; }
|
||||
.action-scroll { max-height: calc(100dvh - 210px); overflow-y: auto; }
|
||||
.ack-box { padding: 14px; border-bottom: 1px solid var(--line); }
|
||||
.ack-box .btn { width: 100%; min-height: 48px; }
|
||||
.ack-box p { margin: 9px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.ack-tools { margin-top: 8px; display: flex; justify-content: flex-end; }
|
||||
.inline-alert { margin-top: 9px; padding: 9px 10px; border: 1px solid rgba(251,191,36,.4); border-radius: 8px; background: rgba(251,191,36,.09); color: #ffe8a8; font-size: 12px; }
|
||||
.inline-alert.danger { border-color: rgba(251,113,133,.42); background: rgba(251,113,133,.09); color: #ffc3ce; }
|
||||
.countdown { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; padding: 12px; margin: 12px; border: 1px solid rgba(251,191,36,.28); border-radius: 10px; background: rgba(251,191,36,.07); }
|
||||
.countdown strong { display: block; font-size: 20px; color: #ffe18a; font-variant-numeric: tabular-nums; }
|
||||
.countdown span { color: var(--muted); font-size: 12px; }
|
||||
@@ -149,6 +158,8 @@
|
||||
.step.current .step-dot { background: var(--warning); }
|
||||
.step strong { display: block; }
|
||||
.step span { color: var(--muted); font-size: 12px; }
|
||||
.delivery-facts { margin-top: 4px; display: grid; gap: 2px; }
|
||||
.delivery-facts span { display: block; }
|
||||
.disposition { padding: 14px; border-top: 1px solid var(--line); display: grid; gap: 9px; }
|
||||
.disposition label { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.disposition-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
@@ -167,6 +178,8 @@
|
||||
td { font-variant-numeric: tabular-nums; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
tr:hover td { background: color-mix(in srgb, var(--primary-soft) 50%, transparent); }
|
||||
.pager { min-height: 58px; padding: 10px 14px; border-top: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.pager-actions { display: flex; gap: 7px; }
|
||||
.rule-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.rule-card { padding: 15px; }
|
||||
.rule-card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
@@ -190,6 +203,9 @@
|
||||
.linked-zone strong, .linked-zone small { display: block; }
|
||||
.linked-zone small { color: var(--muted); }
|
||||
.workflow-note { grid-column: 1/-1; margin: 0; padding: 10px 11px; border-left: 3px solid var(--primary); background: var(--primary-soft); color: var(--muted); font-size: 12px; }
|
||||
.version-diff { display: grid; gap: 8px; }
|
||||
.version-diff article { padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||||
.version-diff article strong { display: block; }
|
||||
.chain-grid { grid-template-columns: minmax(0, 1.4fr) minmax(300px, .8fr); }
|
||||
.chain-row { padding: 13px 14px; display: grid; grid-template-columns: 40px 1fr auto; gap: 11px; align-items: center; border-bottom: 1px solid var(--line); }
|
||||
.chain-row:last-child { border-bottom: 0; }
|
||||
@@ -206,6 +222,7 @@
|
||||
.donut::after { content: "83%\A 24h 内闭环"; white-space: pre; position: absolute; inset: 27px; border-radius: 50%; display: grid; place-items: center; text-align: center; background: var(--surface); color: var(--text); font-weight: 800; }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 12px; }
|
||||
.legend i { display: inline-block; width: 9px; height: 9px; margin-right: 5px; border-radius: 2px; }
|
||||
.metric-note { margin: 12px 0 0; padding: 10px 12px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); }
|
||||
.state-layer { display: none; min-height: 420px; place-items: center; padding: 24px; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--surface); text-align: center; }
|
||||
.state-layer.show { display: grid; }
|
||||
.state-layer .icon { width: 40px; height: 40px; color: var(--primary); }
|
||||
@@ -216,7 +233,7 @@
|
||||
.skeleton i:nth-child(2) { width: 72%; }
|
||||
@keyframes shimmer { to { background-position: -200% 0; } }
|
||||
dialog { width: min(610px, calc(100% - 28px)); max-height: 88dvh; padding: 0; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); }
|
||||
#ruleDialog { width: min(760px, calc(100% - 28px)); }
|
||||
#ruleDialog, #eventDialog, #handoverDialog, #rollbackDialog { width: min(760px, calc(100% - 28px)); }
|
||||
dialog::backdrop { background: rgba(4,7,15,.68); backdrop-filter: blur(4px); }
|
||||
.dialog-head, .dialog-foot { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.dialog-head h2 { margin: 0; font-size: 17px; }
|
||||
@@ -232,6 +249,7 @@
|
||||
.muted { color: var(--muted); }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||||
.mobile-nav { display: none; }
|
||||
.mobile-manage { display: none; }
|
||||
@media (max-width: 1240px) {
|
||||
:root { --sidebar: 78px; }
|
||||
.brand-copy, .nav span:not(.count), .nav-label, .shift-card { display: none; }
|
||||
@@ -249,6 +267,7 @@
|
||||
.sidebar { display: none; }
|
||||
.topbar { padding: 8px 12px; }
|
||||
.site span, .top-actions .select { display: none; }
|
||||
.mobile-manage { display: inline-flex; min-width: 44px; }
|
||||
.content { padding: 12px; }
|
||||
.btn, .select, .field, .textarea { min-height: 44px; }
|
||||
.icon-btn { min-width: 44px; }
|
||||
@@ -260,6 +279,7 @@
|
||||
.site-grid { grid-template-columns: 1fr; }
|
||||
.rule-grid { grid-template-columns: 1fr 1fr; }
|
||||
.fact-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.pager { align-items: flex-start; flex-direction: column; }
|
||||
.mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||
.mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; }
|
||||
.mobile-nav button.active { color: #c9c1ff; background: var(--primary-soft); }
|
||||
@@ -302,13 +322,16 @@
|
||||
<div class="app">
|
||||
<aside class="sidebar" aria-label="Bell 主导航">
|
||||
<div class="brand"><div class="brand-mark">Y</div><div class="brand-copy"><strong>YoVision</strong><small>Bell · Duty Console</small></div></div>
|
||||
<div class="nav-label">Operations</div>
|
||||
<div class="nav-label">值班与业务</div>
|
||||
<nav class="nav">
|
||||
<button data-view="duty" aria-current="page"><svg class="icon"><use href="#i-inbox"/></svg><span>值班台</span><span class="count">6</span></button>
|
||||
<button data-view="events"><svg class="icon"><use href="#i-event"/></svg><span>事件中心</span></button>
|
||||
<button data-view="rules"><svg class="icon"><use href="#i-rule"/></svg><span>规则策略</span></button>
|
||||
<button data-view="chains"><svg class="icon"><use href="#i-phone"/></svg><span>升级链</span></button>
|
||||
<button data-view="reports"><svg class="icon"><use href="#i-chart"/></svg><span>运营报表</span></button>
|
||||
</nav>
|
||||
<div class="nav-label">管理</div>
|
||||
<nav class="nav">
|
||||
<button data-view="sites"><svg class="icon"><use href="#i-rule"/></svg><span>站点与 Area</span></button>
|
||||
<button data-view="audit"><svg class="icon"><use href="#i-shield"/></svg><span>审计日志</span></button>
|
||||
</nav>
|
||||
@@ -319,7 +342,8 @@
|
||||
<header class="topbar">
|
||||
<div class="site"><strong>青藤教育集团 · 青藤寄宿学校</strong><span>当前账号:林工 · 租户管理员 · Asia/Shanghai</span></div>
|
||||
<div class="top-actions">
|
||||
<select class="select" id="stateSelect" aria-label="原型状态演示"><option value="normal">正常状态</option><option value="loading">加载中</option><option value="weak">弱网 / 证据失败</option><option value="empty">队列为空</option><option value="denied">无权限</option></select>
|
||||
<select class="select" id="stateSelect" aria-label="原型状态演示"><option value="normal">正常状态</option><option value="loading">加载中</option><option value="weak">弱网 / 证据失败</option><option value="empty">队列为空</option><option value="denied">无权限</option><option value="expired">会话已过期</option></select>
|
||||
<button class="btn mobile-manage" id="mobileManage" aria-haspopup="dialog">管理</button>
|
||||
<button class="btn icon-btn" id="themeToggle" aria-label="切换明暗主题" title="切换明暗主题"><svg class="icon"><use href="#i-sun"/></svg></button>
|
||||
<button class="btn icon-btn" aria-label="查看通知" title="查看通知"><svg class="icon"><use href="#i-bell"/></svg></button>
|
||||
</div>
|
||||
@@ -329,7 +353,7 @@
|
||||
<section class="state-layer" id="stateLayer" aria-live="polite"></section>
|
||||
|
||||
<section class="view active" data-page="duty">
|
||||
<div class="page-head"><div><h1>实时值班台</h1><p>先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。</p></div><div class="page-actions"><span class="badge success">值班组在线</span><button class="btn" id="muteButton">限时静默</button></div></div>
|
||||
<div class="page-head"><div><h1>实时值班台</h1><p>先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。</p></div><div class="page-actions"><span class="badge success">值班组在线</span><button class="btn" id="handoverButton">发起交接班</button><button class="btn" id="muteButton">限时静默</button></div></div>
|
||||
<div class="duty-grid">
|
||||
<section class="card queue" aria-label="待处置预警队列">
|
||||
<div class="card-head"><h2>待处置预警</h2><span class="badge danger">6 个未 ack</span></div>
|
||||
@@ -350,18 +374,19 @@
|
||||
<div class="fact-grid">
|
||||
<div class="fact"><span>规则</span><strong id="factRule">非授权人员进入</strong></div><div class="fact"><span>置信度</span><strong>0.92</strong></div><div class="fact"><span>持续时间</span><strong>8.4s</strong></div><div class="fact"><span>证据状态</span><strong style="color:var(--success)">已校验</strong></div>
|
||||
</div>
|
||||
<div class="relation-panel"><h3>关联事件 · <span id="relatedEventCount">2</span></h3><div class="relation-list" id="relatedEvents"></div></div>
|
||||
<div class="privacy-note"><svg class="icon"><use href="#i-lock"/></svg><span>仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。</span></div>
|
||||
</section>
|
||||
|
||||
<aside class="card action-panel" aria-label="预警确认与处置">
|
||||
<div class="card-head"><h2>处置与升级</h2><span class="badge warning" id="ackState">等待 ack</span></div>
|
||||
<div class="action-scroll">
|
||||
<div class="ack-box"><button class="btn btn-primary" id="ackButton"><svg class="icon"><use href="#i-check"/></svg>确认接手</button><p id="ackHelp">一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</p></div>
|
||||
<div class="ack-box"><button class="btn btn-primary" id="ackButton"><svg class="icon"><use href="#i-check"/></svg>确认接手</button><p id="ackHelp">一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</p><div class="ack-tools"><button class="text-button" id="simulateAckConflict">演示另一值班员抢先 ack</button></div><div class="inline-alert" id="ackConflict" role="alert" hidden></div></div>
|
||||
<div><div class="countdown"><svg class="icon" style="color:var(--warning);width:26px;height:26px"><use href="#i-clock"/></svg><div><strong id="countdown">01:42</strong><span>未 ack 将升级到校级负责人</span></div></div>
|
||||
<div class="escalation"><h3>投递时间线</h3>
|
||||
<div class="step done"><i class="step-dot"></i><strong>值班室 Web + 本地声光</strong><span>23:14:10 · 已送达</span></div>
|
||||
<div class="step current"><i class="step-dot"></i><strong>值班员短信</strong><span>23:14:13 · 已送达,未确认</span></div>
|
||||
<div class="step"><i class="step-dot"></i><strong>校级负责人语音</strong><span>预计 23:16:10 · 尚未发送</span></div>
|
||||
<div class="step done"><i class="step-dot"></i><strong>值班室 Web + 本地声光</strong><div class="delivery-facts"><span>已发出 23:14:10</span><span>已送达 23:14:11</span><span>已看到 23:14:31</span></div></div>
|
||||
<div class="step current"><i class="step-dot"></i><strong>值班员短信 · 可重试</strong><div class="delivery-facts"><span>已发出 23:14:13</span><span style="color:var(--warning)">未收到送达回执 · 已等待 42 秒</span><span>通道不支持“已看到” · 重试 2/3,23:15:25</span></div></div>
|
||||
<div class="step"><i class="step-dot"></i><strong>校级负责人语音</strong><div class="delivery-facts"><span>预计 23:16:10 · 尚未发送</span><span>号码无效将标记最终失败,不继续伪装送达</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disposition"><label for="outcome">处置结果</label><select class="select" id="outcome"><option value="">待选择</option><option>已现场核查,无人员受伤</option><option>已通知安保人员处理</option><option>已转交校方负责人</option></select><label for="note">处置备注</label><textarea class="textarea" id="note" placeholder="记录已采取的动作,不改写原始事件事实"></textarea><div class="disposition-actions"><button class="btn btn-danger" id="falsePositive">标记误报</button><button class="btn btn-success" id="completeAlert">完成处置</button></div></div>
|
||||
@@ -375,18 +400,14 @@
|
||||
<div class="grid stats">
|
||||
<article class="card stat"><span>今日事件</span><strong>47</strong><span class="muted">较昨日 -8%</span></article><article class="card stat"><span>已触发预警</span><strong>18</strong><span class="muted">规则命中后独立创建</span></article><article class="card stat"><span>误报反馈</span><strong>5</strong><span class="muted">进入标注队列</span></article><article class="card stat"><span>证据可用率</span><strong>97.9%</strong><span class="muted">1 个片段可重试</span></article>
|
||||
</div>
|
||||
<article class="card"><div class="card-head"><div class="toolbar"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索事件" placeholder="事件编号、点位或规则"></div><select class="select" aria-label="严重度筛选"><option>全部严重度</option><option>S1 高危</option><option>S2 重要</option></select><select class="select" aria-label="结果筛选"><option>全部结果</option><option>已处置</option><option>误报</option></select></div><span class="muted">共 47 条</span></div><div class="table-wrap"><table><thead><tr><th>事件编号</th><th>时间</th><th>点位</th><th>规则</th><th>事实</th><th>预警</th><th>结果</th><th>证据</th></tr></thead><tbody>
|
||||
<tr><td class="mono">EVT-01J8…7A2</td><td>23:14:08</td><td>北围墙 01</td><td>非授权人员进入</td><td><span class="badge danger">已确认</span></td><td>Alert #1842</td><td>处置中</td><td><span class="badge success">可用</span></td></tr>
|
||||
<tr><td class="mono">EVT-01J8…69P</td><td>22:48:31</td><td>宿舍东通道</td><td>区域聚集</td><td><span class="badge info">已记录</span></td><td>未触发</td><td>试运行样本</td><td><span class="badge success">可用</span></td></tr>
|
||||
<tr><td class="mono">EVT-01J8…52C</td><td>22:19:04</td><td>实验楼入口</td><td>危险区域闯入</td><td><span class="badge danger">已确认</span></td><td>Alert #1839</td><td>误报</td><td><span class="badge warning">重试中</span></td></tr>
|
||||
</tbody></table></div></article>
|
||||
<article class="card"><div class="card-head"><div class="toolbar"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索事件" placeholder="事件编号、点位或规则"></div><select class="select" aria-label="严重度筛选"><option>全部严重度</option><option>S1 高危</option><option>S2 重要</option></select><select class="select" aria-label="结果筛选"><option>全部结果</option><option>已处置</option><option>误报</option></select></div><span class="muted">共 47 条</span></div><div class="table-wrap"><table><thead><tr><th>事件编号</th><th>时间</th><th>点位</th><th>规则</th><th>事实</th><th>预警</th><th>结果</th><th>证据</th></tr></thead><tbody id="eventRows"></tbody></table></div><div class="pager"><span id="eventPageSummary">第 1 / 16 页 · 每页 3 条</span><div class="pager-actions"><button class="btn" id="eventPrev">上一页</button><button class="btn" id="eventNext">下一页</button></div></div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-page="rules">
|
||||
<div class="rule-context" id="ruleContext" hidden><p><strong>已带入摄像头与区域上下文:</strong><span id="ruleContextCamera">北围墙 01</span> · <span id="ruleContextZone">ZONE-007</span>,可继续完成规则草稿。</p><button class="btn btn-primary" id="continueRuleDraft">继续配置</button></div>
|
||||
<div class="page-head"><div><h1>规则策略</h1><p>Bell 管理场景、范围、时段、持续时间、试运行与发布;检测区域来自对应摄像头的版本化空间配置。</p></div><div class="page-actions"><button class="btn btn-primary" id="openRuleDialog">新建规则</button></div></div>
|
||||
<div class="grid rule-grid">
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge success">正式生效</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>非授权人员进入</h2><p>危险区域 × 全天 × 持续 3 秒</p><dl><dt>覆盖范围</dt><dd>4 个点位</dd><dt>检测区域</dt><dd>北围墙危险区域 v7</dd><dt>继承来源</dt><dd>站点模板</dd><dt>今日命中</dt><dd>8</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>编辑规则</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule">校准检测区域</a></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge success" id="publishedRuleBadge">正式生效 · v7</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>非授权人员进入</h2><p>危险区域 × 全天 × 持续 3 秒</p><dl><dt>覆盖范围</dt><dd>4 个点位</dd><dt>检测区域</dt><dd>北围墙危险区域 v7</dd><dt>继承来源</dt><dd>站点模板</dd><dt>今日命中</dt><dd>8</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>编辑规则</button><button class="btn" id="openRollback">版本历史 / 回滚</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule">校准检测区域</a></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge warning" id="trialBadge">试运行</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>区域内聚集</h2><p>人数 ≥ 6 × 持续 20 秒 × 夜间</p><dl><dt>覆盖范围</dt><dd>3 个点位</dd><dt>检测区域</dt><dd>3 个区域版本</dd><dt>继承来源</dt><dd>设备覆盖</dd><dt>样本命中</dt><dd>14</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>查看试运行</button><button class="btn btn-primary" id="publishTrialRule">正式发布</button></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge violet">草稿</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>方向越线</h2><p>警戒线 A → B × 上学时段</p><dl><dt>覆盖范围</dt><dd>1 个点位</dd><dt>检测区域</dt><dd>宿舍东通道 · 警戒线 v3</dd><dt>继承来源</dt><dd>设备草稿</dd><dt>最近修改</dt><dd>2h 前</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>继续编辑</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-011&zone=ZONE-011&return_to=bell-rule">查看警戒线</a></div></article>
|
||||
</div>
|
||||
@@ -404,6 +425,7 @@
|
||||
<section class="view" data-page="reports">
|
||||
<div class="page-head"><div><h1>运营报表</h1><p>算法效果和系统 SLA 分开统计;图表同时提供表格导出。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-download"/></svg>导出 CSV</button></div></div>
|
||||
<div class="grid chart-grid"><article class="card chart"><h2>近 7 日预警量</h2><p>按站点日期汇总;可切换到每路每天误报数。</p><div class="bars" role="img" aria-label="近七日预警量依次为 18、22、15、31、27、24、19"><div class="bar" style="--h:58%"><span>周一</span></div><div class="bar" style="--h:70%"><span>周二</span></div><div class="bar" style="--h:48%"><span>周三</span></div><div class="bar" style="--h:100%"><span>周四</span></div><div class="bar" style="--h:87%"><span>周五</span></div><div class="bar" style="--h:77%"><span>周六</span></div><div class="bar" style="--h:61%"><span>周日</span></div></div></article><article class="card chart"><h2>处置结果</h2><p>本月已闭环事件。</p><div class="donut" role="img" aria-label="正常处置 61%,误报 22%,升级未闭环 17%"></div><div class="legend"><span><i style="background:var(--success)"></i>正常处置 61%</span><span><i style="background:var(--warning)"></i>误报 22%</span><span><i style="background:var(--danger)"></i>未闭环 17%</span></div></article></div>
|
||||
<article class="card" style="margin-top:12px"><div class="card-head"><div><h2>规则验收效果</h2><span class="muted">冻结样本窗口:2026-07-01~2026-07-31 · 现场复核集 v3</span></div><button class="btn">导出验收附件</button></div><div class="table-wrap"><table><thead><tr><th>规则版本</th><th>样本窗口</th><th>正样本 / 漏检</th><th>召回率</th><th>通道天数</th><th>误报数</th><th>每路每天误报数</th></tr></thead><tbody><tr><td>非授权人员进入 · v7</td><td>07-01~07-31</td><td>186 / 8</td><td><strong>95.7%</strong></td><td>496</td><td>19</td><td><strong>0.038</strong></td></tr><tr><td>区域内聚集 · v4 试运行</td><td>07-15~07-31</td><td>72 / 6</td><td><strong>91.7%</strong></td><td>272</td><td>16</td><td><strong>0.059</strong></td></tr></tbody></table></div><p class="metric-note"><strong>口径说明:</strong>按规则版本分别报告召回率与每路每天误报数;不提供跨场景统一“准确率”。样本量和冻结窗口随导出保留。</p></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-page="sites">
|
||||
@@ -416,9 +438,7 @@
|
||||
<section class="view" data-page="audit">
|
||||
<div class="context-strip" id="auditContext" hidden><p><strong>来自 Sense 设备日志:</strong>租户 <span id="auditTenant">—</span> · 站点 <span id="auditSite">—</span> · 设备 <span id="auditDevice">—</span>。已锁定筛选上下文。</p><a class="btn" id="auditReturn" href="../sense/index.html?view=device-detail">返回设备详情 ↗</a></div>
|
||||
<div class="page-head"><div><h1>审计日志</h1><p>查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。</p></div><div class="page-actions"><button class="btn">导出审计记录</button></div></div>
|
||||
<article class="card"><div class="card-head"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" id="auditSearch" aria-label="搜索审计日志" placeholder="操作、角色或对象编号"></div><span class="badge info">只读</span></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>角色</th><th>动作</th><th>对象</th><th>结果</th><th>来源</th></tr></thead><tbody id="auditRows">
|
||||
<tr><td>23:14:18</td><td>站点值班员</td><td>ACK_ALERT</td><td class="mono">ALT-1842</td><td><span class="badge success">成功</span></td><td>值班室 Web</td></tr><tr><td>22:52:06</td><td>站点管理员</td><td>PUBLISH_RULE</td><td class="mono">RULE-007</td><td><span class="badge success">成功</span></td><td>管理端</td></tr><tr><td>22:20:31</td><td>只读用户</td><td>VIEW_EVIDENCE</td><td class="mono">EVT-01J8…52C</td><td><span class="badge danger">拒绝</span></td><td>响应式 H5</td></tr>
|
||||
</tbody></table></div></article>
|
||||
<article class="card"><div class="card-head"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" id="auditSearch" aria-label="搜索审计日志" placeholder="操作、角色或对象编号"></div><span class="badge info">只读</span></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>角色</th><th>动作</th><th>对象</th><th>结果</th><th>来源</th></tr></thead><tbody id="auditRows"></tbody></table></div><div class="pager"><span id="auditPageSummary">第 1 / 28 页 · 每页 3 条</span><div class="pager-actions"><button class="btn" id="auditPrev">上一页</button><button class="btn" id="auditNext">下一页</button></div></div></article>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
@@ -435,8 +455,9 @@
|
||||
<div class="form-field"><label for="ruleSchedule">生效时段</label><select id="ruleSchedule" class="select"><option>全天</option><option>夜间 22:00–06:00</option><option>自定义课表</option></select></div>
|
||||
<div class="form-field"><label for="ruleDuration">持续时间</label><select id="ruleDuration" class="select"><option>超过 3 秒</option><option>立即</option><option>超过 10 秒</option><option>超过 20 秒</option></select></div>
|
||||
<div class="form-field full"><label><input id="startDryRun" type="checkbox" checked aria-label="保存后开始试运行"> 保存后开始试运行,不触发正式预警</label></div>
|
||||
<div class="inline-alert danger form-field full" id="ruleConflict" role="alert" hidden><strong>规则已由周管理员发布为 v8。</strong><br>当前草稿基于 v7,不能覆盖。请刷新到 v8 后重新合并,或另存为新草稿。</div>
|
||||
<p class="workflow-note">产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。</p>
|
||||
</div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveRuleDraft" value="default">保存并开始试运行</button></div></form></dialog>
|
||||
</div></div><div class="dialog-foot"><button type="button" class="btn" id="simulateRuleConflict">演示版本冲突</button><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveRuleDraft" value="default">保存并开始试运行</button></div></form></dialog>
|
||||
|
||||
<dialog id="falseDialog" aria-labelledby="falseTitle"><form method="dialog"><div class="dialog-head"><h2 id="falseTitle">标记为误报</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><p>这只会追加事件 outcome,不会修改原始事件事实和证据。</p><div class="form-grid"><div class="form-field full"><label for="falseReason">误报原因</label><select id="falseReason" class="select" required><option value="">请选择</option><option>区域配置不准确</option><option>目标分类错误</option><option>时段策略不准确</option><option>其他</option></select></div><div class="form-field full"><label for="falseNote">补充说明(可选)</label><textarea id="falseNote" class="textarea" placeholder="便于算法团队归因,不填写真实个人信息"></textarea></div><div class="form-field full"><label><input type="checkbox" checked> 进入标注复核队列</label></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-danger" id="confirmFalse" value="default">确认标记</button></div></form></dialog>
|
||||
|
||||
@@ -445,78 +466,374 @@
|
||||
<dialog id="areaDialog" aria-labelledby="areaTitle"><form method="dialog"><div class="dialog-head"><h2 id="areaTitle">编辑 Area 准入策略</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><div class="form-grid"><div class="form-field"><label for="areaName">Area</label><input id="areaName" class="field" value="北围墙"></div><div class="form-field"><label for="areaParent">父级</label><select id="areaParent" class="select"><option>校园</option><option>教学区</option><option>宿舍楼</option></select></div><div class="form-field full"><label for="areaPolicy">capture_policy</label><select id="areaPolicy" class="select"><option value="video_allowed">允许成像设备</option><option value="non_imaging_only">仅允许非成像设备</option></select></div><div class="form-field full"><label for="areaReason">变更原因</label><textarea id="areaReason" class="textarea" placeholder="必填,进入全局审计"></textarea></div></div><p class="muted">保存生成新策略版本;Sense 投影同步前会显示旧版本,不允许绕过后端准入检查。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="saveAreaPolicy">保存策略版本</button></div></form></dialog>
|
||||
|
||||
<dialog id="policyConflictDialog" aria-labelledby="policyConflictTitle"><form method="dialog"><div class="dialog-head"><h2 id="policyConflictTitle">策略与已有设备冲突</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><p>北围墙当前有 4 台成像设备。改为“仅非成像”前必须选择显式处置;系统不会自动停用、移动或保留违规设备。</p><label class="radio-card"><input type="radio" name="conflictAction" value="move"> <strong>先迁移 4 台设备到允许成像的 Area</strong><br><span class="muted">创建迁移任务,全部成功后再发布策略。</span></label><label class="radio-card"><input type="radio" name="conflictAction" value="cancel" checked> <strong>取消本次策略变更</strong><br><span class="muted">保持 v42 和现有设备不变。</span></label></div><div class="dialog-foot"><button class="btn" value="cancel">返回编辑</button><button class="btn btn-primary" value="default" id="confirmPolicyConflict">确认选择</button></div></form></dialog>
|
||||
|
||||
<dialog id="manageDialog" aria-labelledby="manageTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="manageTitle">管理</h2><span class="muted">移动端保留 5 个主入口,低频管理能力从这里进入。</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭管理菜单">×</button></div><div class="dialog-body"><div class="relation-list"><button type="button" class="relation-item" data-manage-view="sites"><strong>站点与 Area</strong><span>配额、capture_policy、RBAC →</span></button><button type="button" class="relation-item" data-manage-view="audit"><strong>审计日志</strong><span>只读、筛选、分页 →</span></button></div></div></form></dialog>
|
||||
|
||||
<dialog id="eventDialog" aria-labelledby="eventDialogTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="eventDialogTitle">事件详情</h2><span class="muted" id="eventDialogSubtitle">不可变事实</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭事件详情">×</button></div><div class="dialog-body"><div class="fact-grid" style="margin:0 0 14px"><div class="fact"><span>事件编号</span><strong class="mono" id="eventDetailId">—</strong></div><div class="fact"><span>点位</span><strong id="eventDetailSite">—</strong></div><div class="fact"><span>规则</span><strong id="eventDetailRule">—</strong></div><div class="fact"><span>证据</span><strong id="eventDetailEvidence">—</strong></div></div><div class="relation-panel" style="margin:0"><h3>触发的预警</h3><div class="relation-list" id="eventAlerts"></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">关闭</button></div></form></dialog>
|
||||
|
||||
<dialog id="handoverDialog" aria-labelledby="handoverTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="handoverTitle">值班交接</h2><span class="muted">白班 · 林值班员 → 晚班</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭交接班">×</button></div><div class="dialog-body"><div class="form-grid"><div class="form-field"><label for="handoverTo">接班人</label><select class="select" id="handoverTo"><option value="">请选择并确认本人已在场</option><option>王值班员 · 晚班</option><option>赵值班员 · 晚班</option></select></div><div class="form-field"><label for="handoverAt">交接时间</label><input class="field" id="handoverAt" value="2026-08-04 20:00" readonly></div><div class="form-field full"><label>待交接 Alert</label><div class="relation-list"><div class="relation-item"><strong>ALT-1842 · 未 ack</strong><span>下一次升级 01:42</span></div><div class="relation-item"><strong>ALT-1841 · 处置中</strong><span>王值班员现场核查</span></div><div class="relation-item"><strong>ALT-1839 · 升级中</strong><span>短信重试 2/3</span></div></div></div><div class="form-field full"><label for="handoverNote">交接备注</label><textarea class="textarea" id="handoverNote" placeholder="记录现场状态、已采取动作和待跟进事项"></textarea></div></div><p class="metric-note">交接期间升级链继续计时,不暂停、不重置。接班人确认前,当前责任关系保持不变。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="confirmHandover">接班人确认并留痕</button></div></form></dialog>
|
||||
|
||||
<dialog id="rollbackDialog" aria-labelledby="rollbackTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="rollbackTitle">版本历史与回滚</h2><span class="muted">RULE-007 · 历史版本不可变</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭版本历史">×</button></div><div class="dialog-body"><div class="version-diff"><article><strong>当前 v7 · 2026-08-01 正式发布</strong><span class="muted">持续 3 秒 · 全天 · 4 个点位 · 区域 v7</span></article><article><strong>目标 v6 · 2026-07-18 历史版本</strong><span class="muted">持续 5 秒 · 夜间 22:00–06:00 · 3 个点位 · 区域 v6</span></article><article><strong>影响评估</strong><span class="muted">将减少 1 个点位并收窄时段;预计每日预警 -18%。确认后创建并发布 v8,使其配置等价于 v6,v6/v7 均保持不变。</span></article></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-danger" id="confirmRollback">创建并发布回滚版本 v8</button></div></form></dialog>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toast = $('#toast'); let toastTimer;
|
||||
function notify(message) { toast.textContent = message; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 3800); }
|
||||
const toast = $('#toast');
|
||||
let toastTimer;
|
||||
function notify(message) {
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => toast.classList.remove('show'), 3800);
|
||||
}
|
||||
|
||||
const alerts = [
|
||||
{id:'ALT-1842',severity:'S1 高危',badge:'danger',title:'危险区域闯入',site:'北围墙 01',time:'23:14:08',age:'1 分钟',rule:'非授权人员进入',delivery:'短信已送达'},
|
||||
{id:'ALT-1841',severity:'S1 高危',badge:'danger',title:'方向越线',site:'宿舍东通道',time:'23:12:46',age:'3 分钟',rule:'夜间越线 A → B',delivery:'声光已触发'},
|
||||
{id:'ALT-1839',severity:'S2 重要',badge:'warning',title:'区域聚集',site:'操场西北角',time:'23:08:21',age:'7 分钟',rule:'人数 ≥ 6 持续 20 秒',delivery:'Web 已看到'},
|
||||
{id:'ALT-1836',severity:'S2 重要',badge:'warning',title:'危险区域滞留',site:'配电房门口',time:'22:57:03',age:'18 分钟',rule:'区域内停留 ≥ 10 秒',delivery:'等待短信回执'}
|
||||
{id:'ALT-1842',severity:'S1 高危',badge:'danger',title:'危险区域闯入',site:'北围墙 01',time:'23:14:08',age:'1 分钟',rule:'非授权人员进入',delivery:'短信等待回执',events:[{id:'EVT-01J8…7A2',reason:'同点位 30 秒窗口聚合'},{id:'EVT-01J8…7B4',reason:'同规则连续命中聚合'}]},
|
||||
{id:'ALT-1841',severity:'S1 高危',badge:'danger',title:'方向越线',site:'宿舍东通道',time:'23:12:46',age:'3 分钟',rule:'夜间越线 A → B',delivery:'声光已看到',events:[{id:'EVT-01J8…71M',reason:'单事件触发'}]},
|
||||
{id:'ALT-1839',severity:'S2 重要',badge:'warning',title:'区域聚集',site:'操场西北角',time:'23:08:21',age:'7 分钟',rule:'人数 ≥ 6 持续 20 秒',delivery:'Web 已看到',events:[{id:'EVT-01J8…52C',reason:'同站点 60 秒窗口聚合'}]},
|
||||
{id:'ALT-1836',severity:'S2 重要',badge:'warning',title:'危险区域滞留',site:'配电房门口',time:'22:57:03',age:'18 分钟',rule:'区域内停留 ≥ 10 秒',delivery:'短信最终失败',events:[{id:'EVT-01J8…44K',reason:'单事件触发'}]}
|
||||
];
|
||||
const eventRecords = [
|
||||
{id:'EVT-01J8…7A2',time:'23:14:08',site:'北围墙 01',rule:'非授权人员进入',fact:'已确认',factBadge:'danger',result:'处置中',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1842',state:'等待 ack · 聚合 2 个事件'}]},
|
||||
{id:'EVT-01J8…7B4',time:'23:14:21',site:'北围墙 01',rule:'非授权人员进入',fact:'已确认',factBadge:'danger',result:'已聚合',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1842',state:'等待 ack · 同点位窗口聚合'}]},
|
||||
{id:'EVT-01J8…69P',time:'22:48:31',site:'宿舍东通道',rule:'区域内聚集',fact:'已记录',factBadge:'info',result:'试运行样本',evidence:'可用',evidenceBadge:'success',alerts:[]},
|
||||
{id:'EVT-01J8…52C',time:'22:19:04',site:'实验楼入口',rule:'危险区域闯入',fact:'已确认',factBadge:'danger',result:'误报',evidence:'重试中',evidenceBadge:'warning',alerts:[{id:'ALT-1839',state:'处置完成 · 误报'}]},
|
||||
{id:'EVT-01J8…71M',time:'21:58:20',site:'宿舍东通道',rule:'夜间越线 A → B',fact:'已确认',factBadge:'danger',result:'处置中',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1841',state:'已 ack · 现场核查'}]},
|
||||
{id:'EVT-01J8…44K',time:'21:31:02',site:'配电房门口',rule:'区域内停留 ≥ 10 秒',fact:'已确认',factBadge:'warning',result:'升级中',evidence:'仅抓拍',evidenceBadge:'warning',alerts:[{id:'ALT-1836',state:'短信最终失败 · 语音待发送'}]}
|
||||
];
|
||||
const auditRecords = [
|
||||
{time:'23:14:18',role:'站点值班员',action:'ACK_ALERT',object:'ALT-1842',result:'成功',badge:'success',source:'值班室 Web'},
|
||||
{time:'22:52:06',role:'站点管理员',action:'PUBLISH_RULE',object:'RULE-007',result:'成功',badge:'success',source:'管理端'},
|
||||
{time:'22:20:31',role:'只读用户',action:'VIEW_EVIDENCE',object:'EVT-01J8…52C',result:'拒绝',badge:'danger',source:'响应式 H5'},
|
||||
{time:'21:56:04',role:'站点值班员',action:'HANDOVER_SHIFT',object:'SHIFT-20260804-DAY',result:'成功',badge:'success',source:'值班室 Web'},
|
||||
{time:'21:41:12',role:'租户管理员',action:'ROLLBACK_RULE',object:'RULE-007:v8',result:'成功',badge:'success',source:'管理端'},
|
||||
{time:'20:05:55',role:'站点管理员',action:'UPDATE_AREA_POLICY',object:'area-lab-gate:v41',result:'成功',badge:'success',source:'管理端'}
|
||||
];
|
||||
|
||||
function makeTextCell(row, value, className = '') {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = value;
|
||||
if (className) cell.className = className;
|
||||
row.append(cell);
|
||||
return cell;
|
||||
}
|
||||
function makeBadgeCell(row, value, badge) {
|
||||
const cell = document.createElement('td');
|
||||
const span = document.createElement('span');
|
||||
span.className = `badge ${badge}`;
|
||||
span.textContent = value;
|
||||
cell.append(span);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
let selectedAlertIndex = 0;
|
||||
let ackOwner = null;
|
||||
const alertList = $('#alertList');
|
||||
function renderAlerts(selected=0) { alertList.innerHTML = alerts.map((a,i)=>`<button class="alert-card ${i===selected?'selected':''}" data-index="${i}"><div class="alert-card-top"><span class="badge ${a.badge}"><i class="severity-dot"></i>${a.severity}</span><span class="muted">${a.age}</span></div><h3>${a.title}</h3><p>${a.site} · ${a.time}</p><div class="alert-card-foot"><span>${a.delivery}</span><span class="mono">${a.id}</span></div></button>`).join(''); $$('.alert-card', alertList).forEach(b=>b.addEventListener('click',()=>selectAlert(Number(b.dataset.index)))); }
|
||||
function selectAlert(index) { renderAlerts(index); const a=alerts[index]; $('#selectedSeverity').className=`badge ${a.badge}`; $('#selectedSeverity').textContent=a.severity; $('#selectedTitle').textContent=a.title; $('#selectedMeta').textContent=`${a.site} · ${a.time} · ${a.id}`; $('#factRule').textContent=a.rule; $('#ackState').className='badge warning'; $('#ackState').textContent='等待 ack'; $('#ackButton').disabled=false; $('#ackButton').innerHTML='<svg class="icon"><use href="#i-check"/></svg>确认接手'; $('#ackHelp').textContent='一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。'; }
|
||||
renderAlerts();
|
||||
function renderAlerts(selected = selectedAlertIndex) {
|
||||
alertList.innerHTML = alerts.map((alert, index) => `<button class="alert-card ${index === selected ? 'selected' : ''}" data-index="${index}"><div class="alert-card-top"><span class="badge ${alert.badge}"><i class="severity-dot"></i>${alert.severity}</span><span class="muted">${alert.age}</span></div><h3>${alert.title}</h3><p>${alert.site} · ${alert.time}</p><div class="alert-card-foot"><span>${alert.delivery}</span><span class="mono">${alert.id}</span></div></button>`).join('');
|
||||
$$('.alert-card', alertList).forEach(button => button.addEventListener('click', () => selectAlert(Number(button.dataset.index))));
|
||||
}
|
||||
function renderRelatedEvents(alert) {
|
||||
const container = $('#relatedEvents');
|
||||
container.replaceChildren();
|
||||
alert.events.forEach(event => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'relation-item';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'mono';
|
||||
strong.textContent = event.id;
|
||||
const reason = document.createElement('span');
|
||||
reason.textContent = `${event.reason} →`;
|
||||
button.append(strong, reason);
|
||||
button.addEventListener('click', () => showEventDetail(event.id));
|
||||
container.append(button);
|
||||
});
|
||||
$('#relatedEventCount').textContent = String(alert.events.length);
|
||||
}
|
||||
function resetAck() {
|
||||
ackOwner = null;
|
||||
$('#ackState').className = 'badge warning';
|
||||
$('#ackState').textContent = '等待 ack';
|
||||
$('#ackButton').disabled = false;
|
||||
$('#ackButton').innerHTML = '<svg class="icon"><use href="#i-check"/></svg>确认接手';
|
||||
$('#ackHelp').textContent = '一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。';
|
||||
$('#ackConflict').hidden = true;
|
||||
}
|
||||
function selectAlert(index) {
|
||||
selectedAlertIndex = index;
|
||||
renderAlerts(index);
|
||||
const alert = alerts[index];
|
||||
$('#selectedSeverity').className = `badge ${alert.badge}`;
|
||||
$('#selectedSeverity').textContent = alert.severity;
|
||||
$('#selectedTitle').textContent = alert.title;
|
||||
$('#selectedMeta').textContent = `${alert.site} · ${alert.time} · ${alert.id}`;
|
||||
$('#factRule').textContent = alert.rule;
|
||||
renderRelatedEvents(alert);
|
||||
resetAck();
|
||||
}
|
||||
function openAlert(alertId) {
|
||||
const index = alerts.findIndex(alert => alert.id === alertId);
|
||||
if (index < 0) {
|
||||
notify(`预警 ${alertId} 不在当前队列,请从历史预警查询。`);
|
||||
return;
|
||||
}
|
||||
if ($('#eventDialog').open) $('#eventDialog').close();
|
||||
switchView('duty');
|
||||
selectAlert(index);
|
||||
notify(`已从事件打开关联预警 ${alertId}。`);
|
||||
}
|
||||
|
||||
function switchView(name) { $$('.view').forEach(v=>v.classList.toggle('active',v.dataset.page===name)); $$('[data-view]').forEach(b=>{const on=b.dataset.view===name;b.toggleAttribute('aria-current',on);b.classList.toggle('active',on);}); $('#stateSelect').value='normal'; renderState('normal'); $('#main-content').focus({preventScroll:true}); }
|
||||
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));
|
||||
$$('.queue-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.queue-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-pressed',x===b)});notify(`队列已切换到“${b.textContent}”`);}));
|
||||
$$('.evidence-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.evidence-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-pressed',x===b)});notify(`正在演示“${b.textContent}”标签结构`);}));
|
||||
function showEventDetail(eventId) {
|
||||
const event = eventRecords.find(item => item.id === eventId) || eventRecords[0];
|
||||
$('#eventDialogTitle').textContent = `${event.id} · 事件详情`;
|
||||
$('#eventDialogSubtitle').textContent = `${event.time} · 事实不可由处置改写`;
|
||||
$('#eventDetailId').textContent = event.id;
|
||||
$('#eventDetailSite').textContent = event.site;
|
||||
$('#eventDetailRule').textContent = event.rule;
|
||||
$('#eventDetailEvidence').textContent = event.evidence;
|
||||
const container = $('#eventAlerts');
|
||||
container.replaceChildren();
|
||||
if (!event.alerts.length) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-alert';
|
||||
message.textContent = '未创建 Alert:当前为试运行样本或已被规则抑制;不伪装成投递成功。';
|
||||
container.append(message);
|
||||
} else {
|
||||
event.alerts.forEach(alert => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'relation-item';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'mono';
|
||||
strong.textContent = alert.id;
|
||||
const state = document.createElement('span');
|
||||
state.textContent = `${alert.state} →`;
|
||||
button.append(strong, state);
|
||||
button.addEventListener('click', () => openAlert(alert.id));
|
||||
container.append(button);
|
||||
});
|
||||
}
|
||||
$('#eventDialog').showModal();
|
||||
}
|
||||
|
||||
function renderState(state) { const layer=$('#stateLayer'), views=$$('.view'); layer.classList.toggle('show',state!=='normal'); views.forEach(v=>v.style.display=state==='normal'?'':'none'); if(state==='normal')return; const states={
|
||||
loading:`<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>`,
|
||||
weak:`<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal');notify('证据重试任务已加入后台队列')">后台重试证据</button></div>`,
|
||||
empty:`<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" onclick="switchView('events')">查看事件中心</button></div>`,
|
||||
denied:`<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal')">返回可访问范围</button></div>`}; layer.innerHTML=states[state]; }
|
||||
$('#stateSelect').addEventListener('change',e=>renderState(e.target.value));
|
||||
$('#themeToggle').addEventListener('click',()=>{const h=document.documentElement;h.dataset.theme=h.dataset.theme==='dark'?'light':'dark';notify(`已切换为${h.dataset.theme==='dark'?'深色':'浅色'}主题`);});
|
||||
let eventPage = 1;
|
||||
const eventTotalPages = 16;
|
||||
function renderEventPage() {
|
||||
const start = ((eventPage - 1) * 3) % eventRecords.length;
|
||||
const pageItems = [0, 1, 2].map(offset => eventRecords[(start + offset) % eventRecords.length]);
|
||||
const body = $('#eventRows');
|
||||
body.replaceChildren();
|
||||
pageItems.forEach(event => {
|
||||
const row = document.createElement('tr');
|
||||
const idCell = document.createElement('td');
|
||||
const eventButton = document.createElement('button');
|
||||
eventButton.type = 'button';
|
||||
eventButton.className = 'text-button mono';
|
||||
eventButton.textContent = event.id;
|
||||
eventButton.addEventListener('click', () => showEventDetail(event.id));
|
||||
idCell.append(eventButton);
|
||||
row.append(idCell);
|
||||
makeTextCell(row, event.time);
|
||||
makeTextCell(row, event.site);
|
||||
makeTextCell(row, event.rule);
|
||||
makeBadgeCell(row, event.fact, event.factBadge);
|
||||
const alertCell = document.createElement('td');
|
||||
if (event.alerts.length) {
|
||||
event.alerts.forEach(alert => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'text-button mono';
|
||||
button.textContent = alert.id;
|
||||
button.addEventListener('click', () => openAlert(alert.id));
|
||||
alertCell.append(button);
|
||||
});
|
||||
} else {
|
||||
alertCell.textContent = '未触发';
|
||||
}
|
||||
row.append(alertCell);
|
||||
makeTextCell(row, event.result);
|
||||
makeBadgeCell(row, event.evidence, event.evidenceBadge);
|
||||
body.append(row);
|
||||
});
|
||||
$('#eventPageSummary').textContent = `第 ${eventPage} / ${eventTotalPages} 页 · 每页 3 条 · 共 47 条`;
|
||||
$('#eventPrev').disabled = eventPage === 1;
|
||||
$('#eventNext').disabled = eventPage === eventTotalPages;
|
||||
}
|
||||
|
||||
$('#ackButton').addEventListener('click',async e=>{const b=e.currentTarget;b.disabled=true;b.textContent='确认中…';await new Promise(r=>setTimeout(r,650));b.innerHTML='<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手';$('#ackState').className='badge success';$('#ackState').textContent='已 ack';$('#ackHelp').textContent='23:15:02 · 当前值班员 · 服务端确认成功';notify('确认成功,升级计时已停止并记录审计。');});
|
||||
$('#completeAlert').addEventListener('click',()=>{if(!$('#outcome').value){notify('请先选择处置结果。');$('#outcome').focus();return;}if($('#ackState').textContent!=='已 ack'){notify('请先确认接手,再完成处置。');$('#ackButton').focus();return;}notify('处置结果已追加,原始事件事实未被修改。');});
|
||||
$('#falsePositive').addEventListener('click',()=>$('#falseDialog').showModal());
|
||||
$('#confirmFalse').addEventListener('click',e=>{if(!$('#falseReason').value){e.preventDefault();notify('请选择误报原因。');$('#falseReason').focus();return;}notify('已标记误报并进入复核队列,原始事件保持不变。');});
|
||||
$('#muteButton').addEventListener('click',()=>$('#muteDialog').showModal());
|
||||
$('#confirmMute').addEventListener('click',e=>{if(!$('#muteCause').value.trim()){e.preventDefault();notify('请填写静默原因。');$('#muteCause').focus();return;}notify('已创建限时静默,到期将自动恢复并通知值班组。');});
|
||||
let auditPage = 1;
|
||||
const auditTotalPages = 28;
|
||||
function renderAuditPage() {
|
||||
const start = ((auditPage - 1) * 3) % auditRecords.length;
|
||||
const pageItems = [0, 1, 2].map(offset => auditRecords[(start + offset) % auditRecords.length]);
|
||||
const body = $('#auditRows');
|
||||
body.replaceChildren();
|
||||
pageItems.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
makeTextCell(row, item.time);
|
||||
makeTextCell(row, item.role);
|
||||
makeTextCell(row, item.action);
|
||||
makeTextCell(row, item.object, 'mono');
|
||||
makeBadgeCell(row, item.result, item.badge);
|
||||
makeTextCell(row, item.source);
|
||||
body.append(row);
|
||||
});
|
||||
$('#auditPageSummary').textContent = `第 ${auditPage} / ${auditTotalPages} 页 · 每页 3 条 · 共 84 条`;
|
||||
$('#auditPrev').disabled = auditPage === 1;
|
||||
$('#auditNext').disabled = auditPage === auditTotalPages;
|
||||
}
|
||||
|
||||
$$('[data-edit-area]').forEach(button=>button.addEventListener('click',()=>{const isDorm=button.dataset.editArea==='dorm';$('#areaName').value=isDorm?'宿舍卧室':button.dataset.editArea==='lab'?'实验楼入口':'北围墙';$('#areaPolicy').value=isDorm?'non_imaging_only':'video_allowed';$('#areaDialog').showModal();}));
|
||||
$('#newArea').addEventListener('click',()=>{$('#areaName').value='';$('#areaPolicy').value='video_allowed';$('#areaDialog').showModal();});
|
||||
$('#newSite').addEventListener('click',()=>notify('新建站点需先填写时区、场景包、配额和管理员,本原型只枚举入口。'));
|
||||
$('#saveAreaPolicy').addEventListener('click',()=>{if(!$('#areaReason').value.trim()){notify('请填写变更原因。');$('#areaReason').focus();return;}if($('#areaName').value==='北围墙'&&$('#areaPolicy').value==='non_imaging_only'){$('#areaDialog').close();$('#policyConflictDialog').showModal();return;}$('#areaDialog').close();notify('Area 策略新版本已保存,等待 Sense 拉取投影。');});
|
||||
$('#confirmPolicyConflict').addEventListener('click',()=>{const action=$('input[name="conflictAction"]:checked').value;notify(action==='move'?'已创建 4 台设备迁移任务;完成前不会发布策略。':'已取消策略变更,v42 和已有设备保持不变。');});
|
||||
const validViews = new Set($$('.view').map(view => view.dataset.page));
|
||||
function switchView(requested) {
|
||||
const name = validViews.has(requested) ? requested : 'duty';
|
||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.page === name));
|
||||
$$('[data-view]').forEach(button => {
|
||||
const active = button.dataset.view === name;
|
||||
button.toggleAttribute('aria-current', active);
|
||||
button.classList.toggle('active', active);
|
||||
});
|
||||
$('#stateSelect').value = 'normal';
|
||||
renderState('normal');
|
||||
$('#main-content').focus({preventScroll:true});
|
||||
}
|
||||
$$('[data-view]').forEach(button => button.addEventListener('click', () => switchView(button.dataset.view)));
|
||||
$$('.queue-tabs button').forEach(button => button.addEventListener('click', () => {
|
||||
$$('.queue-tabs button').forEach(item => { const active = item === button; item.classList.toggle('active', active); item.setAttribute('aria-pressed', String(active)); });
|
||||
notify(`队列已切换到“${button.textContent}”`);
|
||||
}));
|
||||
$$('.evidence-tabs button').forEach(button => button.addEventListener('click', () => {
|
||||
$$('.evidence-tabs button').forEach(item => { const active = item === button; item.classList.toggle('active', active); item.setAttribute('aria-pressed', String(active)); });
|
||||
notify(`正在演示“${button.textContent}”标签结构`);
|
||||
}));
|
||||
|
||||
function renderState(state) {
|
||||
const layer = $('#stateLayer');
|
||||
const views = $$('.view');
|
||||
layer.classList.toggle('show', state !== 'normal');
|
||||
views.forEach(view => { view.style.display = state === 'normal' ? '' : 'none'; });
|
||||
if (state === 'normal') return;
|
||||
const states = {
|
||||
loading:'<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>',
|
||||
weak:'<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" data-state-recover="weak">后台重试证据</button></div>',
|
||||
empty:'<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" data-empty-events>查看事件中心</button></div>',
|
||||
denied:'<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" data-state-recover="denied">返回可访问范围</button></div>',
|
||||
expired:'<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>会话已过期</h2><p>为保护预警和证据,页面已停止提交 ack 与处置。重新登录后从服务端读取最新责任人和升级状态,不重放旧操作。</p><button class="btn" data-state-recover="expired">重新登录并刷新状态</button></div>'
|
||||
};
|
||||
layer.innerHTML = states[state];
|
||||
$('[data-state-recover]', layer)?.addEventListener('click', () => { $('#stateSelect').value = 'normal'; renderState('normal'); notify(state === 'weak' ? '证据重试任务已加入后台队列。' : '已刷新当前权限与服务端状态。'); });
|
||||
$('[data-empty-events]', layer)?.addEventListener('click', () => switchView('events'));
|
||||
}
|
||||
|
||||
$('#stateSelect').addEventListener('change', event => renderState(event.target.value));
|
||||
$('#themeToggle').addEventListener('click', () => { const root = document.documentElement; root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; notify(`已切换为${root.dataset.theme === 'dark' ? '深色' : '浅色'}主题`); });
|
||||
$('#eventPrev').addEventListener('click', () => { if (eventPage > 1) { eventPage -= 1; renderEventPage(); } });
|
||||
$('#eventNext').addEventListener('click', () => { if (eventPage < eventTotalPages) { eventPage += 1; renderEventPage(); } });
|
||||
$('#auditPrev').addEventListener('click', () => { if (auditPage > 1) { auditPage -= 1; renderAuditPage(); } });
|
||||
$('#auditNext').addEventListener('click', () => { if (auditPage < auditTotalPages) { auditPage += 1; renderAuditPage(); } });
|
||||
|
||||
$('#ackButton').addEventListener('click', async event => {
|
||||
const button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
button.textContent = '确认中…';
|
||||
await new Promise(resolve => setTimeout(resolve, 650));
|
||||
if (ackOwner && ackOwner !== 'current') {
|
||||
button.textContent = '当前预警已由他人接手';
|
||||
$('#ackConflict').hidden = false;
|
||||
$('#ackConflict').textContent = '确认未生效:王值班员已于 23:15:01 抢先 ack。已刷新为服务端结果,没有覆盖对方。';
|
||||
notify('并发确认未覆盖:请与当前处置人协作。');
|
||||
return;
|
||||
}
|
||||
ackOwner = 'current';
|
||||
button.innerHTML = '<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手';
|
||||
$('#ackState').className = 'badge success';
|
||||
$('#ackState').textContent = '已 ack';
|
||||
$('#ackHelp').textContent = '23:15:02 · 当前值班员 · 服务端确认成功;通道回执仍单独保留。';
|
||||
notify('确认成功,升级计时已停止并记录审计。');
|
||||
});
|
||||
$('#simulateAckConflict').addEventListener('click', () => {
|
||||
ackOwner = 'other';
|
||||
$('#ackButton').disabled = true;
|
||||
$('#ackButton').textContent = '已由王值班员接手';
|
||||
$('#ackState').className = 'badge info';
|
||||
$('#ackState').textContent = '已 ack · 他人';
|
||||
$('#ackHelp').textContent = '23:15:01 · 王值班员 · 服务端首个成功结果';
|
||||
$('#ackConflict').hidden = false;
|
||||
$('#ackConflict').textContent = '你的 ack 未生效:王值班员抢先成功。页面已刷新实际处置人,没有静默覆盖或显示双成功。';
|
||||
});
|
||||
$('#completeAlert').addEventListener('click', () => {
|
||||
if (!$('#outcome').value) { notify('请先选择处置结果。'); $('#outcome').focus(); return; }
|
||||
if (ackOwner !== 'current') { notify(ackOwner === 'other' ? '当前处置人为王值班员,你不能覆盖其处置结果。' : '请先确认接手,再完成处置。'); $('#ackButton').focus(); return; }
|
||||
notify('处置结果已追加,原始事件事实未被修改。');
|
||||
});
|
||||
$('#falsePositive').addEventListener('click', () => $('#falseDialog').showModal());
|
||||
$('#confirmFalse').addEventListener('click', event => { if (!$('#falseReason').value) { event.preventDefault(); notify('请选择误报原因。'); $('#falseReason').focus(); return; } notify('已标记误报并进入复核队列,原始事件保持不变。'); });
|
||||
$('#muteButton').addEventListener('click', () => $('#muteDialog').showModal());
|
||||
$('#confirmMute').addEventListener('click', event => { if (!$('#muteCause').value.trim()) { event.preventDefault(); notify('请填写静默原因。'); $('#muteCause').focus(); return; } notify('已创建限时静默,到期将自动恢复并通知值班组。'); });
|
||||
$('#handoverButton').addEventListener('click', () => $('#handoverDialog').showModal());
|
||||
$('#confirmHandover').addEventListener('click', () => { if (!$('#handoverTo').value) { notify('请选择接班人并由本人确认。'); $('#handoverTo').focus(); return; } $('#handoverDialog').close(); notify('交接已由接班人确认并写入审计;3 条 Alert 的升级链继续运行。'); });
|
||||
|
||||
$('#mobileManage').addEventListener('click', () => $('#manageDialog').showModal());
|
||||
$$('[data-manage-view]').forEach(button => button.addEventListener('click', () => { $('#manageDialog').close(); switchView(button.dataset.manageView); notify(`已进入${button.textContent.trim()}。`); }));
|
||||
|
||||
$$('[data-edit-area]').forEach(button => button.addEventListener('click', () => { const isDorm = button.dataset.editArea === 'dorm'; $('#areaName').value = isDorm ? '宿舍卧室' : button.dataset.editArea === 'lab' ? '实验楼入口' : '北围墙'; $('#areaPolicy').value = isDorm ? 'non_imaging_only' : 'video_allowed'; $('#areaDialog').showModal(); }));
|
||||
$('#newArea').addEventListener('click', () => { $('#areaName').value = ''; $('#areaPolicy').value = 'video_allowed'; $('#areaDialog').showModal(); });
|
||||
$('#newSite').addEventListener('click', () => notify('新建站点需先填写时区、场景包、配额和管理员,本原型只枚举入口。'));
|
||||
$('#saveAreaPolicy').addEventListener('click', () => { if (!$('#areaReason').value.trim()) { notify('请填写变更原因。'); $('#areaReason').focus(); return; } if ($('#areaName').value === '北围墙' && $('#areaPolicy').value === 'non_imaging_only') { $('#areaDialog').close(); $('#policyConflictDialog').showModal(); return; } $('#areaDialog').close(); notify('Area 策略新版本已保存,等待 Sense 拉取投影。'); });
|
||||
$('#confirmPolicyConflict').addEventListener('click', () => { const action = $('input[name="conflictAction"]:checked').value; notify(action === 'move' ? '已创建 4 台设备迁移任务;完成前不会发布策略。' : '已取消策略变更,v42 和已有设备保持不变。'); });
|
||||
|
||||
const ruleDialog = $('#ruleDialog');
|
||||
function openRuleEditor() { ruleDialog.showModal(); }
|
||||
$('#openRuleDialog').addEventListener('click', openRuleEditor);
|
||||
$('#continueRuleDraft').addEventListener('click', openRuleEditor);
|
||||
$$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor));
|
||||
function syncSenseZoneLink(){ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent($('#ruleCamera').value)}&zone=${encodeURIComponent($('#ruleZone').value)}&return_to=bell-rule`; }
|
||||
$('#ruleCamera').addEventListener('change',()=>{ syncSenseZoneLink(); notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
|
||||
$('#ruleZone').addEventListener('change',syncSenseZoneLink);
|
||||
$('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。'));
|
||||
$('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}});
|
||||
$$('[data-edit-rule]').forEach(button => button.addEventListener('click', openRuleEditor));
|
||||
function syncSenseZoneLink() { $('#editRuleZone').href = `../sense/index.html?view=device-detail&camera=${encodeURIComponent($('#ruleCamera').value)}&zone=${encodeURIComponent($('#ruleZone').value)}&return_to=bell-rule`; }
|
||||
$('#ruleCamera').addEventListener('change', () => { syncSenseZoneLink(); notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
|
||||
$('#ruleZone').addEventListener('change', syncSenseZoneLink);
|
||||
$('#simulateRuleConflict').addEventListener('click', () => { $('#ruleConflict').hidden = false; $('#saveRuleDraft').disabled = true; notify('已读取服务端 v8;当前 v7 草稿被阻止覆盖。'); });
|
||||
$('#saveRuleDraft').addEventListener('click', event => { if (!$('#ruleConflict').hidden) { event.preventDefault(); notify('版本冲突未解决,不能覆盖 v8。'); return; } notify($('#startDryRun').checked ? '规则草稿已保存并进入试运行,不会触发正式预警。' : '规则草稿已保存,尚未开始试运行。'); });
|
||||
$('#publishTrialRule').addEventListener('click', () => { if (confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')) { $('#trialBadge').className = 'badge success'; $('#trialBadge').textContent = '正式生效'; notify('规则版本已正式发布,变更已记录审计并可回滚。'); } });
|
||||
$('#openRollback').addEventListener('click', () => $('#rollbackDialog').showModal());
|
||||
$('#confirmRollback').addEventListener('click', () => { $('#rollbackDialog').close(); $('#publishedRuleBadge').textContent = '正式生效 · v8(回滚)'; notify('已创建并发布 v8,其配置等价于 v6;v6、v7 历史保持不变。'); });
|
||||
|
||||
let remain = 102;
|
||||
setInterval(() => { if (ackOwner || remain <= 0) return; remain -= 1; const minutes = String(Math.floor(remain / 60)).padStart(2, '0'); const seconds = String(remain % 60).padStart(2, '0'); $('#countdown').textContent = `${minutes}:${seconds}`; }, 1000);
|
||||
|
||||
renderAlerts();
|
||||
renderRelatedEvents(alerts[0]);
|
||||
renderEventPage();
|
||||
renderAuditPage();
|
||||
|
||||
let remain=102;
|
||||
setInterval(()=>{if($('#ackState').textContent==='已 ack'||remain<=0)return;remain--;const m=String(Math.floor(remain/60)).padStart(2,'0'),s=String(remain%60).padStart(2,'0');$('#countdown').textContent=`${m}:${s}`;},1000);
|
||||
const routeParams = new URLSearchParams(location.search);
|
||||
if (routeParams.get('view')) switchView(routeParams.get('view'));
|
||||
if (routeParams.get('view')==='audit' && (routeParams.has('device')||routeParams.has('site'))) {
|
||||
const tenant=routeParams.get('tenant')||'当前租户',site=routeParams.get('site')||'当前站点',device=routeParams.get('device')||'全部设备';
|
||||
$('#auditContext').hidden=false;$('#auditTenant').textContent=tenant;$('#auditSite').textContent=site;$('#auditDevice').textContent=device;$('#auditSearch').value=device;
|
||||
$('#auditRows').insertAdjacentHTML('afterbegin',`<tr><td>10:47:36</td><td>站点管理员</td><td>PAUSE_INFERENCE</td><td class="mono">${device}</td><td><span class="badge success">成功</span></td><td>Sense outbox · relay 已送达</td></tr>`);
|
||||
$('#auditReturn').href=`../sense/index.html?view=device-detail&device=${encodeURIComponent(device)}&return_to=bell-audit`;
|
||||
notify('已应用来自 Sense 的租户、站点和设备审计筛选。');
|
||||
if (routeParams.get('view') === 'audit' && (routeParams.has('device') || routeParams.has('site'))) {
|
||||
const tenant = routeParams.get('tenant') || '当前租户';
|
||||
const site = routeParams.get('site') || '当前站点';
|
||||
const device = routeParams.get('device') || '全部设备';
|
||||
$('#auditContext').hidden = false;
|
||||
$('#auditTenant').textContent = tenant;
|
||||
$('#auditSite').textContent = site;
|
||||
$('#auditDevice').textContent = device;
|
||||
$('#auditSearch').value = device;
|
||||
auditRecords.unshift({time:'10:47:36',role:'站点管理员',action:'PAUSE_INFERENCE',object:device,result:'成功',badge:'success',source:'Sense outbox · relay 已送达'});
|
||||
renderAuditPage();
|
||||
$('#auditReturn').href = `../sense/index.html?view=device-detail&device=${encodeURIComponent(device)}&return_to=bell-audit`;
|
||||
notify('已安全应用来自 Sense 的租户、站点和设备审计筛选。');
|
||||
}
|
||||
if (routeParams.has('event')) {
|
||||
switchView('events');
|
||||
showEventDetail(routeParams.get('event'));
|
||||
}
|
||||
if (routeParams.has('alert')) openAlert(routeParams.get('alert'));
|
||||
if (routeParams.has('camera')) {
|
||||
switchView('rules'); $('#ruleContext').hidden=false;
|
||||
const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera;
|
||||
const zone=routeParams.get('zone'); if(zone&&[...$('#ruleZone').options].some(option=>option.value===zone)) $('#ruleZone').value=zone;
|
||||
$('#ruleContextCamera').textContent=$('#ruleCamera').selectedOptions[0].textContent; $('#ruleContextZone').textContent=zone||$('#ruleZone').value;
|
||||
switchView('rules');
|
||||
$('#ruleContext').hidden = false;
|
||||
const camera = routeParams.get('camera');
|
||||
if ([...$('#ruleCamera').options].some(option => option.value === camera)) $('#ruleCamera').value = camera;
|
||||
const zone = routeParams.get('zone');
|
||||
if (zone && [...$('#ruleZone').options].some(option => option.value === zone)) $('#ruleZone').value = zone;
|
||||
$('#ruleContextCamera').textContent = $('#ruleCamera').selectedOptions[0].textContent;
|
||||
$('#ruleContextZone').textContent = zone || $('#ruleZone').value;
|
||||
syncSenseZoneLink();
|
||||
notify('已保留摄像头与检测区域上下文,可继续配置规则。');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Bell 原型评审(复核定稿)
|
||||
|
||||
> 评审对象:`docs/design/bell/index.html`
|
||||
> 输入:Claude 首轮评审草稿;YoVision 需求、架构、US、IX 与路由文档
|
||||
> 复核日期:2026-08-04
|
||||
|
||||
## 1. 结论
|
||||
|
||||
Claude 指出的投递状态、Event↔Alert 关系、交接班、分页、规则回滚和验收报表缺口成立,应在 T-004 内修复。Area 归属、并发 ack、失败态和重启可观测性的部分结论需要校正;另外原评审遗漏了移动端管理入口与审计深链注入风险。
|
||||
|
||||
## 2. 本轮采纳
|
||||
|
||||
| 优先级 | 项目 | 最终要求 |
|
||||
| --- | --- | --- |
|
||||
| P0 | 投递事实 | `sent`、`delivered`、`seen`、`ack` 分开;不支持回执、状态未知、可重试失败和最终失败不得伪装为成功。ack 属于 Alert 处置事实,不是通道投递状态。 |
|
||||
| P0 | Event↔Alert | Alert 详情展示关联 Event 与聚合原因;Event 详情展示触发的 Alert 与最终状态;两边可导航。抑制可能不创建 Alert,不能把“已处置抑制”误写成聚合原因。 |
|
||||
| P1 | 并发 ack | 首个服务端成功者成为处置人;后到者看到当前处置人与确认时间,不能覆盖,也不能显示双成功。 |
|
||||
| P1 | 交接班 | 展示未 ack、处置中和升级中的 Alert;明确交出人、接手人、备注和确认审计;交接期间升级链不中断。 |
|
||||
| P1 | 局部失败 | 保留弱网证据降级;新增会话过期、ack 竞争结果、规则版本冲突,以及投递的可重试/最终失败。失败应靠近对应操作,不扩展成无意义的全页状态集合。 |
|
||||
| P1 | 分页 | 事件中心和审计日志均提供总数、页码、上一页与下一页,不一次加载全部记录。 |
|
||||
| P2 | 规则回滚 | 查看版本差异与影响范围;回滚通过创建并发布新版本完成,历史版本不可变。 |
|
||||
| P2 | 验收报表 | 按规则版本和冻结样本窗口报告召回率、每路每天误报数及样本量;明确不提供跨场景统一“准确率”。 |
|
||||
|
||||
## 3. 校正与不采纳
|
||||
|
||||
1. Area 已在 `raw/12`、US-010、IX-016 和架构文档中冻结为 Bell 管理,Sense 只消费版本化投影;“重写 S-03”属于过期建议,本轮不重复修改。
|
||||
2. 《13》A-2 的同库只读视图是独立架构裁决,不因原型评审直接采用。本轮继续保持 Bell 真相源与受控 API/投影边界,不修改 schema、API 或跨库访问方式。
|
||||
3. 原型已有并发 ack 文案,但没有可演示的竞争结果,因此补交互而不是从零新增概念。
|
||||
4. `weak` 已覆盖结构化事件成功、视频证据失败的部分成功态;“一个失败态都没有”不准确。本轮补操作级失败和会话过期,不机械增加三个全局错误页。
|
||||
5. 进程重启续跑是后端恢复、指标和测试要求。正常值班时间线不展示内部服务重启;只有确实影响用户的异常恢复才进入业务时间线,本轮不添加常态重启节点。
|
||||
|
||||
## 4. 原评审遗漏
|
||||
|
||||
- 移动端底部主导航继续限制为 5 项,但通过顶部“管理”入口访问站点与 Area、审计日志,避免管理能力在窄屏不可发现。
|
||||
- 桌面侧栏分成“值班与业务”和“管理”两组,避免权限与运营入口混在同一层级。
|
||||
- Sense→Bell 审计深链的 `device` 参数不得通过 `innerHTML`/`insertAdjacentHTML` 拼接;必须按文本节点写入并对返回链接编码,防止原型把不可信查询参数变成 DOM 注入。
|
||||
|
||||
## 5. 实施顺序
|
||||
|
||||
1. 修复审计深链注入。
|
||||
2. 补投递状态模型和 Event↔Alert 双向关系。
|
||||
3. 补并发 ack、交接班与操作级失败。
|
||||
4. 补事件/审计分页和移动端管理入口。
|
||||
5. 补规则回滚与按规则版本的验收报表。
|
||||
6. 用桌面、竖屏手机和横屏手机验证可访问性、无横向溢出及运行时安全;人工确认前 T-004 继续保持 `DOING`。
|
||||
+8
-5
@@ -9,13 +9,14 @@
|
||||
| `/overview` | 站点、设备、事件和预警摘要 | 业务预警与运维告警分区 |
|
||||
| `/sites` | 站点列表、配额与状态 | 显示 16 默认/128 上限,不暗示单机能力 |
|
||||
| `/sites/:siteId/devices` | 设备列表、批量导入/启停、健康 | 分页/虚拟列表、逐项结果、不泄露凭据 |
|
||||
| `/events` | 事件筛选与批量处置入口 | 事件与预警状态分开显示 |
|
||||
| `/events/:eventId` | 事实、证据、时间线、outcome | 权限最小化;弱网渐进加载 |
|
||||
| `/alerts` | 待 ack、升级中、已结束预警 | 清楚区分投递/送达/看到/ack |
|
||||
| `/rules` | 场景包、规则继承、区域/时段配置 | 显示继承来源与试运行状态 |
|
||||
| `/events` | 事件筛选与批量处置入口 | 事件与预警状态分开显示;服务端分页,不一次加载全量 |
|
||||
| `/events/:eventId` | 事实、证据、时间线、outcome 与触发的 Alert | 权限最小化;弱网渐进加载;与 Alert 双向导航 |
|
||||
| `/alerts` | 待 ack、升级中、已结束预警 | 清楚区分已发出/已送达/已看到/ack;支持值班交接 |
|
||||
| `/alerts/:alertId` | Alert 处置、投递/升级事实与关联 Event | 展示聚合原因;ack 竞争不覆盖;与 Event 双向导航 |
|
||||
| `/rules` | 场景包、规则继承、区域/时段配置 | 显示继承来源、试运行、不可变版本和回滚状态 |
|
||||
| `/escalations` | 联系人、通道、超时与静默 | 双路径;静默 ≤4h,无永久项 |
|
||||
| `/operations` | 设备/流/分片/对账运维 | 不与业务预警混在同一队列 |
|
||||
| `/audit` | 审计查询 | 只读、分页、按权限脱敏 |
|
||||
| `/audit` | 审计查询 | 只读、服务端分页、按权限脱敏;深链筛选参数仅按文本渲染 |
|
||||
|
||||
这些是页面职责占位,不等于已冻结 URL;实现任务须更新本文件后再编码。
|
||||
|
||||
@@ -26,6 +27,8 @@
|
||||
- 设备:最小健康状态,不提供未经授权的常态监控。
|
||||
- 我的:联系人、通知偏好和限时静默。
|
||||
|
||||
Bell 响应式管理端的底部主导航最多 5 项;Site/Area、RBAC 与审计通过顶部“管理”菜单进入,不能因为窄屏隐藏侧栏而失去入口。桌面侧栏把值班/事件/规则/升级/报表与 Site/Area/审计分组显示。
|
||||
|
||||
## 值班台/大屏
|
||||
|
||||
- 实时事件流与高优先级预警。
|
||||
|
||||
+19
-7
@@ -16,6 +16,8 @@ write_paths:
|
||||
- docs/raw/09-Sense原型评审-IX草稿.md
|
||||
- docs/raw/11-Sense原型-第二轮缺口.md
|
||||
- docs/raw/12-Sense原型-待办清单.md
|
||||
- docs/raw/14-Bell原型评审.md
|
||||
- docs/routes.md
|
||||
- docs/08-interaction-checklist.md
|
||||
- docs/07-user-stories.md
|
||||
- docs/02-requirements.md
|
||||
@@ -33,33 +35,34 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前,
|
||||
|
||||
## 关联需求与交互(如适用)
|
||||
|
||||
- 用户故事:Sense 关联 US-001、US-002、US-008、US-009,并为 US-005 提供视频设备空间配置;Bell 关联 US-003~US-006、US-010,并拥有 US-005 的业务规则工作流及 Tenant/Site/Area/RBAC/配额/全局审计真相。
|
||||
- 交互清单:Sense 关联 IX-001~IX-004、IX-014~IX-020,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013、IX-016、IX-020,负责规则继承/试运行/发布、站点与 Area 策略、全局审计。
|
||||
- 相关页面 / 路由:原型阶段为 `docs/design/sense/index.html` 与 `docs/design/bell/index.html`,不定义生产路由。
|
||||
- 用户故事:Sense 关联 US-001、US-002、US-008、US-009,并为 US-005 提供视频设备空间配置;Bell 关联 US-003~US-006、US-010~US-012,并拥有 US-005 的业务规则工作流及 Tenant/Site/Area/RBAC/配额/全局审计真相。
|
||||
- 交互清单:Sense 关联 IX-001~IX-004、IX-014~IX-020,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013、IX-016、IX-020~IX-022,负责规则继承/试运行/发布/回滚、站点与 Area 策略、值班交接、验收报表和全局审计。
|
||||
- 相关页面 / 路由:原型阶段为 `docs/design/sense/index.html` 与 `docs/design/bell/index.html`;同步补充 `docs/routes.md` 的候选页面职责,但不冻结或实现生产 URL。
|
||||
|
||||
## 方案
|
||||
|
||||
1. 使用统一的 YoVision 深色安防控制台设计系统,Sense 与 Bell 保持品牌、状态色、控件和导航规律一致。
|
||||
2. Sense 制作为单页可交互工作台:总览、监控墙、设备、接入任务和运维中心,移动端也固定为这 5 个入口;不设置一级“区域规则”或全局管理页。运维中心聚合对账、分片、边缘/隧道、运维告警和系统状态。统一设备台账使用 `modality + capabilities` 决定详情页和操作,M1~M5 完整展示视频设备,非视频设备在 M6 前明确显示 `adapter_not_ready`,不得虚构遥测。
|
||||
3. 视频设备在“画面与检测区域”页签完成多边形、方向警戒线、显式完成、方向反转、逐点编辑、会话草稿恢复、乐观并发冲突和待校准反馈;Sense 只保存空间几何并只读显示关联规则,不提供业务规则绑定、时段、持续时间、试运行或正式发布。
|
||||
4. Bell 制作为单页可交互值班台和统一管理端:除预警队列、事件证据、ack/处置、升级时间线、误报反馈和升级链外,规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和回滚;Bell 同时管理 Site/Area/配额/`capture_policy`,策略与已有成像设备冲突时必须显式迁移或取消。
|
||||
4. Bell 制作为单页可交互值班台和统一管理端:通道的已发出/已送达/已看到与 Alert ack 分开,Event↔Alert 可双向导航;并发 ack 显示真实处置人,班次交接不暂停升级链;规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和以新版本完成回滚;Bell 同时管理 Site/Area/配额/`capture_policy`,策略与已有成像设备冲突时必须显式迁移或取消。
|
||||
5. Sense 摄像头详情与 Bell 规则草稿通过可演示的相对深链互相进入,携带摄像头/区域上下文;原型可以跨文件,但生产产品不得要求用户重新查找同一摄像头或丢失未完成草稿。
|
||||
6. 两个原型均内联 CSS/JS,使用假数据,不加载真实视频、客户信息、生产地址或外部依赖;顶部固定标注原型用途。
|
||||
7. 添加设备时先选择类型,依据能力渐进展示字段;区域的 `capture_policy` 负责隐私准入,视频设备在“仅非成像设备”区域中的新增或启用必须被阻止,策略不可用时对新写入失败关闭。
|
||||
8. 全局头部展示租户、站点、账号和角色;只读角色隐藏写操作,深链越权不泄露资源是否存在。Sense 设备日志带租户/站点/设备条件跳到 Bell 全局审计并可返回。
|
||||
9. 完成桌面、窄屏、横屏、键盘焦点、加载/空态/错误/依赖降级切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。
|
||||
10. Bell 事件与审计列表必须分页;规则验收报表按规则版本和冻结样本窗显示召回率与每路每天误报数,不提供跨场景统一准确率;移动端底部主导航保持 5 项,通过顶部“管理”入口访问 Site/Area 与审计。
|
||||
|
||||
## 不可变约束
|
||||
|
||||
- 阈值 / 数值边界:默认 16 路视频、单站点视频上限 128 路;非视频设备不占视频路数;单媒体分片初始配置建议 `max_streams=32`、当前示例承载 16、最多 4 个分片,最终以压测为准;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。
|
||||
- 判定式 / 状态转换:事件事实不可被处置改写;Alert 的首次投递、送达、看到、ack 和升级必须分开展示;检测区域版本与告警规则版本是不同对象;规则正式发布与试运行必须可区分,保存区域不得自动发布规则。
|
||||
- 安全边界:不得展示摄像头密码、完整 RTSP/ONVIF 地址、token、真实客户/未成年人数据或人脸入口;破坏性动作须确认。
|
||||
- 既有契约:原型不修改事件 v0.1、API、路由和生产技术栈,不作为生产实现代码。
|
||||
- 既有契约:原型不修改事件 v0.1、API、schema、生产路由实现和生产技术栈;仅补充候选路由职责,不作为生产实现代码。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航固定 5 项且无“区域规则”,设备列表可按类型/生命周期筛选与分页,视频详情可编辑检测区域,非视频适配器未交付时不伪装在线;运维中心覆盖对账/分片/边缘/运维告警/系统状态;Bell 可选择摄像头/区域并演示试运行与正式发布,且可管理 Site/Area 策略冲突;Sense↔Bell 规则深链保留 `camera + zone + return_to`,审计深链保留 `tenant + site + device + return_to`;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。
|
||||
- 完整门禁:本任务不修改 schema/API/路由,因此不触发契约与导航实现门禁;若范围变化则先更新任务。
|
||||
- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航固定 5 项且无“区域规则”,设备列表可按类型/生命周期筛选与分页,视频详情可编辑检测区域,非视频适配器未交付时不伪装在线;运维中心覆盖对账/分片/边缘/运维告警/系统状态;Bell 可演示投递事实、Event↔Alert、并发 ack、交接班、事件/审计分页、试运行、正式发布、新版本回滚、验收报表、移动端管理入口和 Site/Area 策略冲突;Sense↔Bell 规则深链保留 `camera + zone + return_to`,审计深链保留 `tenant + site + device + return_to` 且只按文本渲染参数;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。
|
||||
- 完整门禁:本任务不修改 schema/API 或生产路由实现,因此不触发契约实现门禁;候选路由职责已随 IX 更新。若实现范围变化则另立任务。
|
||||
- 人工 / 设备验收:必需。项目负责人分别打开 Sense 与 Bell 原型,确认页面结构、控件集合和主要工作流;未确认前不得标记 `DONE` 或合并为最终设计。
|
||||
- 构建产物:`docs/design/sense/index.html`、`docs/design/bell/index.html`;双击浏览器打开,零构建依赖。
|
||||
|
||||
@@ -78,6 +81,15 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前,
|
||||
|
||||
## 执行记录
|
||||
|
||||
### 2026-08-04 Bell 原型评审复核与修复
|
||||
|
||||
- 将 Claude 评审复核为最终裁决并保存到 `raw/14`:采纳投递状态、Event↔Alert、交接班、分页、回滚和验收报表;纠正 Area 归属、A-2、并发 ack 现状、弱网失败态和重启 UI 建议;额外识别移动端管理入口与审计深链 DOM 注入。
|
||||
- Bell 桌面侧栏分为“值班与业务”和“管理”,移动端底部导航继续固定 5 项并增加顶部“管理”菜单。投递时间线分开展示已发出、已送达、已看到、无回执、通道不支持、可重试与最终失败,Alert ack 继续独立。
|
||||
- Alert 详情与 Event 详情支持双向打开并展示聚合/未触发原因;补齐服务端首个成功者语义的并发 ack 演示、值班交接、事件/审计分页、会话过期、规则版本冲突、不可变新版本回滚和按规则版本的验收效果表。
|
||||
- 审计深链不再把 `device` 查询参数拼入 HTML,改为 `textContent` 与 DOM 节点构造;恶意 `<img onerror>` 参数专项验证未创建节点、未执行脚本且保持安全文本显示。本轮不采纳 A-2 数据库视图,不修改 API/schema,也不在正常值班时间线展示内部服务重启。
|
||||
- Node.js 内联脚本语法、124 个唯一 ID、按钮可访问名、外部依赖和危险 DOM API 静态检查通过。Edge/CDP 在 1440×900、375×812、812×375 验证投递事实、双向关系、并发 ack、交接、分页、规则冲突/回滚、验收报表、会话过期、移动管理与深链注入:页面级无横向溢出,移动导航与管理入口不小于 44px,运行时 0 exception;桌面值班台和窄屏管理页已目检,临时 QA 脚本未写入仓库。
|
||||
- 自动化验收完成后任务仍保持 `DOING`;项目负责人必须打开 Bell 原型确认信息架构、投递语义和交接/回滚流程,确认前不合并 PR、不标记 `DONE`。
|
||||
|
||||
### 2026-08-04 第二轮缺口复核与最终实现
|
||||
|
||||
- 复核 `raw/11`、`raw/12` 后保留对账、能力探测、设备生命周期、RBAC、写入降级、边缘隧道与运维告警等真实缺口;纠正“原型无分页”“分片 8 / 32”“雷达已有真实能力”“T-004 只允许 3 个写路径”等过期事实,并把 M4/M6 能力标为后续阶段而非本轮失败。
|
||||
|
||||
Reference in New Issue
Block a user