feat: Web哨兵监控面板桌面端视图改进三栏布局·低噪声·渐进披露 (#1025)
- 桌面端(≥1024px)三栏 master-detail:Runs | Detail | Findings,点击 run 无需滚动 - 状态摘要条替换 4 metric cards,单行 ~48px,合并 status-pill/overall 重复值 - overview checks 全绿折叠为单 pill,点击展开明细 - runs/findings filter 默认折叠为单行摘要 chip,展开功能不变 - Detail 分 5 tab(概要/发现项/多轮摘要/Trace/证据与命令),URL 支持 ?run=&tab= - Finding 默认显示聚合,点击 chip drill-down 到列表;item 默认折叠只显示 title+severity+count - Runs 行级 severity 色条(red/amber/info 左 border),一眼定位问题行 - Timeline 升级为底部紧凑条,加相对时间刻度,可折叠 - auto-refresh 交互暂停:hover runs/detail 时暂停刷新 - CLI 命令改可复制按钮+toast,detailCommands 不再纯文本 - 快捷键 j/k 上下选 run、Enter 打开、r 刷新、/ 聚焦搜索 - 降权标签:减少 uppercase/font-weight:800 滥用,只在 red/blocked 强调 - 去冗余 box-shadow:panel 用 1px border,只在 modal/overlay 用阴影 - contract version 升级 draft-2026-06-26-p9-desktop-view-density - 保持纯 vanilla JS + 原生 CSS,无框架/无构建步骤,API 契约不变
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density.
|
||||
// Responsibility: Static dashboard shell and asset serving for the web-probe sentinel frontend.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
@@ -11,7 +11,7 @@ interface DashboardShellConfig {
|
||||
}
|
||||
|
||||
const DASHBOARD_ASSET_ROOT = "scripts/assets/web-probe-sentinel-dashboard";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p8-web-probe-sentinel-recovery";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p9-desktop-view-density";
|
||||
|
||||
export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig): string {
|
||||
const publicOrigin = stringOrNull(config.publicExposure.publicBaseUrl) ?? "";
|
||||
@@ -32,126 +32,105 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
data-public-origin="${escapeAttr(publicOrigin)}"
|
||||
data-config-ready="${config.plan.ok ? "true" : "false"}"
|
||||
data-contract-version="${DASHBOARD_CONTRACT_VERSION}"
|
||||
data-viewport=""
|
||||
>
|
||||
<section class="sentinel-topbar" aria-label="Web哨兵概览">
|
||||
<div class="sentinel-title">
|
||||
<div class="sentinel-mark" aria-hidden="true"></div>
|
||||
<div>
|
||||
<h1>HWLAB Web哨兵</h1>
|
||||
<p id="sentinel-subtitle">${escapeHtml(config.node)} / ${escapeHtml(config.lane)}</p>
|
||||
<p id="sentinel-subtitle">${escapeHtml(config.node)} / ${escapeHtml(config.lane)}<span id="sentinel-origin-note"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sentinel-toolbar" aria-label="监控面板控制">
|
||||
<span id="status-pill" class="status-pill status-idle">空闲</span>
|
||||
<label class="refresh-toggle">
|
||||
<input id="auto-refresh-enabled" type="checkbox">
|
||||
<span>自动刷新</span>
|
||||
<span>自动</span>
|
||||
</label>
|
||||
<select id="auto-refresh-interval" aria-label="自动刷新间隔">
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
</select>
|
||||
<button id="manual-refresh" class="icon-button" type="button" title="刷新" aria-label="刷新">
|
||||
<span aria-hidden="true">刷新</span>
|
||||
</button>
|
||||
<button id="manual-refresh" class="icon-button" type="button" title="刷新 (r)" aria-label="刷新">刷新</button>
|
||||
<button id="latest-run" class="icon-button" type="button" title="跳转最新运行" aria-label="跳转最新">最新</button>
|
||||
<button id="filter-red" class="icon-button" type="button" title="只看红色发现" aria-label="只看红色">红</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="loading-banner" class="banner banner-muted">加载中</section>
|
||||
<section id="status-summary" class="status-summary" aria-label="状态摘要条" hidden>
|
||||
<span class="summary-item"><span class="summary-label">状态</span><strong id="summary-status">-</strong></span>
|
||||
<span class="summary-item"><span class="summary-label">最近运行</span><strong id="summary-latest">-</strong><small id="summary-latest-age"></small></span>
|
||||
<span class="summary-item"><span class="summary-label">发现</span><strong id="summary-findings">0</strong><small id="summary-findings-note"></small></span>
|
||||
<span class="summary-item"><span class="summary-label">调度器</span><strong id="summary-scheduler">-</strong><small id="summary-budget"></small></span>
|
||||
<span class="summary-item summary-checks" id="summary-checks" hidden></span>
|
||||
</section>
|
||||
|
||||
<section id="loading-banner" class="banner banner-muted" hidden>加载中</section>
|
||||
<section id="error-banner" class="banner banner-danger" hidden></section>
|
||||
|
||||
<section class="metric-grid" aria-label="哨兵状态指标">
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">当前状态</span>
|
||||
<strong id="metric-overall">-</strong>
|
||||
<small id="metric-origin">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">最近运行</span>
|
||||
<strong id="metric-latest-run">-</strong>
|
||||
<small id="metric-latest-age">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">历史发现项</span>
|
||||
<strong id="metric-findings">0</strong>
|
||||
<small id="metric-findings-note">-</small>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-label">调度器</span>
|
||||
<strong id="metric-scheduler">-</strong>
|
||||
<small id="metric-budget">-</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="overview-checks" aria-label="哨兵健康检查">
|
||||
<span id="check-config" class="check-chip">config -</span>
|
||||
<span id="check-pvc" class="check-chip">pvc -</span>
|
||||
<span id="check-analyzer" class="check-chip">analyzer -</span>
|
||||
<span id="check-public" class="check-chip">public -</span>
|
||||
<span id="check-maintenance" class="check-chip">maintenance -</span>
|
||||
</section>
|
||||
|
||||
<section class="panel timeline-panel" aria-labelledby="timeline-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="timeline-heading">运行时间线</h2>
|
||||
<span id="timeline-count" class="panel-subtitle">-</span>
|
||||
<section class="overview-checks overview-checks-collapsed" id="overview-checks" aria-label="哨兵健康检查" hidden>
|
||||
<button type="button" class="check-summary-pill" id="check-summary-pill">检查 -</button>
|
||||
<div class="overview-checks-detail" id="overview-checks-detail">
|
||||
<span id="check-config" class="check-chip">config -</span>
|
||||
<span id="check-pvc" class="check-chip">pvc -</span>
|
||||
<span id="check-analyzer" class="check-chip">analyzer -</span>
|
||||
<span id="check-public" class="check-chip">public -</span>
|
||||
<span id="check-maintenance" class="check-chip">maintenance -</span>
|
||||
</div>
|
||||
<div id="run-timeline" class="run-timeline"></div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<section class="panel panel-wide" aria-labelledby="runs-heading">
|
||||
<section class="panel panel-runs" aria-labelledby="runs-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="runs-heading">运行历史</h2>
|
||||
<span id="runs-count" class="panel-subtitle">-</span>
|
||||
</div>
|
||||
<form id="runs-filter" class="runs-filter">
|
||||
<label>
|
||||
<span>状态</span>
|
||||
<select id="filter-status" name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="planned">已计划</option>
|
||||
<option value="running">运行中</option>
|
||||
<option value="analyzed">已分析</option>
|
||||
<option value="blocked">阻塞</option>
|
||||
<option value="interrupted">已中断</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>严重级别</span>
|
||||
<select id="filter-severity" name="severity">
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>时间</span>
|
||||
<select id="filter-window" name="window">
|
||||
<option value="">全部</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="24h">24h</option>
|
||||
<option value="7d">7d</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>排序</span>
|
||||
<select id="filter-sort" name="sort">
|
||||
<option value="updated">按更新时间</option>
|
||||
<option value="created">按创建时间</option>
|
||||
<option value="findings">按发现数量</option>
|
||||
<option value="severity">按严重级别</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-search">
|
||||
<span>搜索</span>
|
||||
<input id="filter-search" name="search" type="search" placeholder="run、observer、report">
|
||||
</label>
|
||||
<button id="clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
<div class="filter-collapse">
|
||||
<button type="button" class="filter-summary" id="runs-filter-summary" aria-expanded="false">筛选: 全部</button>
|
||||
<form id="runs-filter" class="runs-filter" hidden>
|
||||
<label><span>状态</span>
|
||||
<select id="filter-status" name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="planned">已计划</option>
|
||||
<option value="running">运行中</option>
|
||||
<option value="analyzed">已分析</option>
|
||||
<option value="blocked">阻塞</option>
|
||||
<option value="interrupted">已中断</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>严重级别</span>
|
||||
<select id="filter-severity" name="severity">
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>时间</span>
|
||||
<select id="filter-window" name="window">
|
||||
<option value="">全部</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="24h">24h</option>
|
||||
<option value="7d">7d</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>排序</span>
|
||||
<select id="filter-sort" name="sort">
|
||||
<option value="updated">按更新时间</option>
|
||||
<option value="created">按创建时间</option>
|
||||
<option value="findings">按发现数量</option>
|
||||
<option value="severity">按严重级别</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="filter-search"><span>搜索</span>
|
||||
<input id="filter-search" name="search" type="search" placeholder="run、observer、report">
|
||||
</label>
|
||||
<button id="clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-frame">
|
||||
<table class="runs-table">
|
||||
<thead>
|
||||
@@ -159,8 +138,8 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
<th>运行</th>
|
||||
<th>状态</th>
|
||||
<th>场景</th>
|
||||
<th>发现项</th>
|
||||
<th>更新时间</th>
|
||||
<th>发现</th>
|
||||
<th>更新</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="runs-body"></tbody>
|
||||
@@ -168,53 +147,71 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="findings-heading">
|
||||
<section class="panel panel-detail" aria-labelledby="detail-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="detail-heading">运行详情</h2>
|
||||
<span id="detail-subtitle" class="panel-subtitle">未选择运行</span>
|
||||
</div>
|
||||
<nav class="detail-tabs" id="detail-tabs" aria-label="详情分页" hidden>
|
||||
<button type="button" class="detail-tab active" data-detail-tab="overview">概要</button>
|
||||
<button type="button" class="detail-tab" data-detail-tab="findings">发现项</button>
|
||||
<button type="button" class="detail-tab" data-detail-tab="turn">多轮摘要</button>
|
||||
<button type="button" class="detail-tab" data-detail-tab="trace">Trace</button>
|
||||
<button type="button" class="detail-tab" data-detail-tab="evidence">证据与命令</button>
|
||||
</nav>
|
||||
<div id="detail-content" class="detail-content"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-findings" aria-labelledby="findings-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="findings-heading">发现分析</h2>
|
||||
<span id="findings-count" class="panel-subtitle">-</span>
|
||||
</div>
|
||||
<form id="findings-filter" class="findings-filter">
|
||||
<label>
|
||||
<span>严重级别</span>
|
||||
<select id="finding-filter-severity" name="fseverity">
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>窗口</span>
|
||||
<select id="finding-filter-window" name="fwindow">
|
||||
<option value="24h">24h</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="7d">7d</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>代码</span>
|
||||
<input id="finding-filter-code" name="fcode" type="search" placeholder="finding code">
|
||||
</label>
|
||||
<label>
|
||||
<span>场景</span>
|
||||
<input id="finding-filter-scenario" name="fscenario" type="search" placeholder="scenario">
|
||||
</label>
|
||||
<button id="finding-clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
<div class="filter-collapse">
|
||||
<button type="button" class="filter-summary" id="findings-filter-summary" aria-expanded="false">筛选: 全部</button>
|
||||
<form id="findings-filter" class="findings-filter" hidden>
|
||||
<label><span>严重级别</span>
|
||||
<select id="finding-filter-severity" name="fseverity">
|
||||
<option value="">全部</option>
|
||||
<option value="red">红色</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>窗口</span>
|
||||
<select id="finding-filter-window" name="fwindow">
|
||||
<option value="24h">24h</option>
|
||||
<option value="1h">1h</option>
|
||||
<option value="6h">6h</option>
|
||||
<option value="7d">7d</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>代码</span>
|
||||
<input id="finding-filter-code" name="fcode" type="search" placeholder="finding code">
|
||||
</label>
|
||||
<label><span>场景</span>
|
||||
<input id="finding-filter-scenario" name="fscenario" type="search" placeholder="scenario">
|
||||
</label>
|
||||
<button id="finding-clear-filters" class="icon-button" type="button">清除</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="finding-aggregation" class="finding-aggregation"></div>
|
||||
<div id="findings-list" class="finding-list"></div>
|
||||
<div id="findings-drilldown" class="findings-drilldown" hidden></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel detail-panel" aria-labelledby="detail-heading" role="dialog" aria-modal="false">
|
||||
<section class="panel timeline-panel" aria-labelledby="timeline-heading">
|
||||
<div class="panel-header">
|
||||
<h2 id="detail-heading">运行详情</h2>
|
||||
<span id="detail-subtitle" class="panel-subtitle">未选择运行</span>
|
||||
<h2 id="timeline-heading">运行时间线</h2>
|
||||
<span id="timeline-count" class="panel-subtitle">-</span>
|
||||
<button type="button" class="timeline-toggle" id="timeline-toggle" aria-expanded="true">折叠</button>
|
||||
</div>
|
||||
<div id="detail-content" class="detail-content"></div>
|
||||
<div id="run-timeline" class="run-timeline"></div>
|
||||
</section>
|
||||
|
||||
<div id="copy-toast" class="copy-toast" hidden>已复制</div>
|
||||
</main>
|
||||
<script type="module" src="/dashboard/assets/dashboard.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p7-web-probe-sentinel-dashboard.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density.
|
||||
// Responsibility: Persistent HTTP wrapper service for web-probe observe scheduling, index, health, metrics, maintenance, and dashboard.
|
||||
import { Buffer } from "node:buffer";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
@@ -12,7 +12,7 @@ import { renderWebProbeSentinelDashboardHtml, webProbeSentinelDashboardAssetResp
|
||||
import { webProbeSentinelConfigPlan, type WebProbeSentinelConfigPlan } from "./hwlab-node-web-sentinel-config";
|
||||
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p8-web-probe-sentinel-recovery";
|
||||
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p9-desktop-view-density";
|
||||
const DASHBOARD_MAX_TEXT_BYTES = 16_000;
|
||||
|
||||
export interface WebProbeSentinelServiceConfig {
|
||||
|
||||
Reference in New Issue
Block a user