fix: restore web sentinel cadence and monitor hover

This commit is contained in:
Codex
2026-06-27 07:28:31 +00:00
parent a02fa6f6e5
commit 9ffb781c8b
5 changed files with 775 additions and 15 deletions
@@ -1,3 +1,4 @@
/* SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p12-cadence-scheduler-monitor-web. */
:root {
color-scheme: light;
--bg: #f5f7f8;
@@ -320,7 +321,7 @@ select {
border: 1px solid var(--line);
border-radius: 8px;
background: linear-gradient(180deg, #ffffff 0%, #f7faf9 100%);
overflow: hidden;
overflow: visible;
}
.trend-chart {
@@ -377,6 +378,47 @@ select {
fill: var(--amber);
}
.trend-dot-hit {
cursor: default;
outline: none;
}
.trend-dot-hit:focus-visible .trend-hit-area {
stroke: var(--blue);
stroke-width: 1.5;
}
.trend-hit-area {
fill: transparent;
stroke: transparent;
}
.trend-tooltip {
position: absolute;
z-index: 8;
display: grid;
width: min(218px, calc(100% - 16px));
max-width: calc(100% - 16px);
gap: 3px;
transform: translate(-50%, -100%);
border: 1px solid var(--line-strong);
border-radius: 8px;
background: #ffffff;
box-shadow: 0 12px 28px rgba(32, 51, 48, 0.16);
color: var(--muted);
padding: 9px 10px;
pointer-events: none;
font-size: 12px;
line-height: 1.35;
}
.trend-tooltip strong {
overflow: hidden;
color: var(--text);
text-overflow: ellipsis;
white-space: nowrap;
}
.trend-legend {
display: flex;
flex-wrap: wrap;
@@ -513,6 +555,8 @@ select {
}
.pane {
position: relative;
isolation: isolate;
display: flex;
min-width: 0;
min-height: 0;
@@ -523,16 +567,27 @@ select {
.pane-header {
position: sticky;
top: 0;
z-index: 2;
top: -12px;
z-index: 12;
display: flex;
align-items: start;
justify-content: space-between;
gap: 10px;
margin: -12px -12px 10px;
padding: 12px;
padding: 12px 12px 13px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.96);
background: var(--panel);
box-shadow: 0 10px 16px rgba(32, 51, 48, 0.08);
}
.pane-header::before {
position: absolute;
right: 0;
left: 0;
top: -18px;
height: 18px;
background: var(--panel);
content: "";
}
.pane-header h2 {
@@ -1,3 +1,5 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p12-cadence-scheduler-monitor-web.
// Responsibility: Vue monitor-web runtime for sentinel trend, timeline, detail and finding observability.
import { createApp, computed, onMounted, ref } from "./vendor/vue.esm-browser.prod.js";
const bootstrap = readBootstrap();
@@ -17,6 +19,7 @@ createApp({
const autoRefresh = ref(true);
const refreshSeconds = ref(30);
const lastLoadedAt = ref("");
const hoveredTrendDot = ref(null);
let lastAutoRefreshAt = 0;
const sentinels = computed(() => {
@@ -43,14 +46,34 @@ createApp({
warning: trendPolyline((run) => warningCount(run)),
total: trendPolyline((run) => findingCount(run)),
}));
const trendDots = computed(() => trendRows.value.map((run, index) => ({
id: run.id || String(index),
x: trendX(index, trendRows.value.length),
redY: trendY(redCount(run)),
warningY: trendY(warningCount(run)),
severity: severityClass(run),
title: `${shortId(run.id)} ${formatDate(run.updatedAt || run.createdAt)}`,
})));
const trendDots = computed(() => trendRows.value.map((run, index) => {
const red = redCount(run);
const warning = warningCount(run);
const total = findingCount(run);
const x = trendX(index, trendRows.value.length);
const redY = trendY(red);
const warningY = trendY(warning);
const rawTime = run.updatedAt || run.createdAt || "";
return {
id: run.id || String(index),
runId: run.id || "",
x,
redY,
warningY,
tooltipLeft: `${clamp((x / 720) * 100, 16, 84)}%`,
tooltipTop: `${clamp(((Math.min(redY, warningY) + 18) / 142) * 100, 24, 76)}%`,
red,
warning,
total,
status: run.status || "-",
severity: severityClass(run),
rawTime,
timeLabel: formatDate(rawTime),
absoluteTime: formatAbsoluteDate(rawTime),
reportSha: shortHash(run.reportJsonSha256 || run.report_json_sha256 || run.reportSha256 || ""),
title: `${shortId(run.id)} ${formatAbsoluteDate(rawTime)} 红色 ${red} 警告 ${warning} 总量 ${total}`,
};
}));
const timelineRuns = computed(() => runs.value.slice(0, 16));
const rootCauseFindings = computed(() => {
const rows = findings.value.filter((item) => item.rootCause || item.nextAction || ["red", "warning"].includes(severityClass(item)));
@@ -142,6 +165,14 @@ createApp({
return Math.round(126 - (Number(value || 0) / trendMax.value) * 102);
}
function showTrendTooltip(dot) {
hoveredTrendDot.value = dot;
}
function hideTrendTooltip() {
hoveredTrendDot.value = null;
}
onMounted(() => {
void loadAll();
window.setInterval(() => {
@@ -165,6 +196,7 @@ createApp({
autoRefresh,
refreshSeconds,
lastLoadedAt,
hoveredTrendDot,
sentinels,
currentStatus,
latestRun,
@@ -182,11 +214,14 @@ createApp({
selectRun,
refreshNow,
currentHref,
showTrendTooltip,
hideTrendTooltip,
redCount,
warningCount,
findingCount,
severityClass,
formatDate,
formatAbsoluteDate,
formatDuration,
shortId,
rootCauseText,
@@ -258,7 +293,19 @@ createApp({
<polyline v-if="trendPolylines.total" class="trend-total" :points="trendPolylines.total"></polyline>
<polyline v-if="trendPolylines.warning" class="trend-warning" :points="trendPolylines.warning"></polyline>
<polyline v-if="trendPolylines.red" class="trend-red" :points="trendPolylines.red"></polyline>
<g v-for="dot in trendDots" :key="dot.id">
<g
v-for="dot in trendDots"
:key="dot.id"
class="trend-dot-hit"
tabindex="0"
role="button"
:aria-label="dot.title"
@mouseenter="showTrendTooltip(dot)"
@focusin="showTrendTooltip(dot)"
@mouseleave="hideTrendTooltip"
@focusout="hideTrendTooltip"
>
<circle class="trend-hit-area" :cx="dot.x" :cy="Math.min(dot.redY, dot.warningY)" r="12"></circle>
<circle class="trend-dot-warning" :cx="dot.x" :cy="dot.warningY" r="3">
<title>{{ dot.title }}</title>
</circle>
@@ -267,6 +314,18 @@ createApp({
</circle>
</g>
</svg>
<div
v-if="hoveredTrendDot"
class="trend-tooltip"
data-monitor-trend-tooltip="true"
:style="{ left: hoveredTrendDot.tooltipLeft, top: hoveredTrendDot.tooltipTop }"
>
<strong>{{ shortId(hoveredTrendDot.runId) }}</strong>
<span>{{ hoveredTrendDot.absoluteTime }}</span>
<span>状态 {{ hoveredTrendDot.status }}</span>
<span>红色 {{ hoveredTrendDot.red }} / 警告 {{ hoveredTrendDot.warning }} / 总量 {{ hoveredTrendDot.total }}</span>
<span v-if="hoveredTrendDot.reportSha">report {{ hoveredTrendDot.reportSha }}</span>
</div>
<div v-if="trendRows.length === 0" class="trend-empty">暂无运行数据</div>
</div>
<div class="trend-legend">
@@ -515,6 +574,13 @@ function formatDate(value) {
return date.toISOString().slice(5, 16).replace("T", " ");
}
function formatAbsoluteDate(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return `${date.toISOString().slice(0, 19).replace("T", " ")} UTC`;
}
function formatDuration(seconds) {
const value = Math.max(0, Number(seconds || 0));
if (value < 90) return `${Math.round(value)}s`;
@@ -528,6 +594,16 @@ function shortId(value) {
return text.length > 18 ? `${text.slice(0, 10)}...${text.slice(-6)}` : text || "-";
}
function shortHash(value) {
const text = String(value || "");
if (text.length === 0) return "";
return text.length > 12 ? text.slice(0, 12) : text;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function rootCauseText(item) {
return item?.rootCause || item?.evidenceSummary || item?.summary || "尚未记录根因,等待下一次 OTel/报告归因。";
}