fix: restore web sentinel cadence and monitor hover
This commit is contained in:
@@ -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/报告归因。";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user