fix: expose sentinel run timing in monitor

This commit is contained in:
Codex
2026-07-01 00:25:27 +00:00
parent 100a846899
commit e48d77f83a
4 changed files with 188 additions and 9 deletions
+24 -2
View File
@@ -619,6 +619,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
const latestRunCounts = {
runId: latestRun?.id || latestRun?.runId || null,
typeCount: numberValue(latestRun?.findingTypeCount ?? latestRun?.findingCount ?? latestRun?.finding_count),
durationMinutes: numberValue(latestRun?.runDurationMinutes ?? latestRun?.durationMinutes ?? latestRun?.timing?.durationMinutes),
error: 0,
warning: 0,
total: 0,
@@ -661,6 +662,15 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
latestRunCounts.warning = latestDetailSummary.warningTypeCount;
latestRunCounts.total = latestDetailSummary.alertTypeCount;
latestRunCounts.all = latestDetailSummary.typeCount;
const trendTooltipSummary = tooltipSummary(trendTooltip);
const detailText = text(".pane-detail");
const runListText = text(".run-list");
const timingVisibility = {
latestRunMinutes: latestRunCounts.durationMinutes,
detailHasDuration: /运行分钟\s+[\d.]+\s*分钟/u.test(detailText),
listHasDuration: /运行\s+[\d.]+\s*分钟/u.test(runListText),
tooltipHasDuration: trendTooltipSummary.hasDuration,
};
const workspaceRect = workspace?.getBoundingClientRect();
const checksRect = checksPanel?.getBoundingClientRect();
const heightSummary = (rect) => {
@@ -851,11 +861,12 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
badCardBodyCount: badCardBodies.length,
trendCurve: Boolean(trend),
trendDotCount: document.querySelectorAll(".trend-dot-hit").length,
trendTooltip: tooltipSummary(trendTooltip),
trendTooltip: trendTooltipSummary,
trendPanelText: text("#trend-heading"),
chartCounts,
latestRunCounts,
latestDetailSummary,
timingVisibility,
checkScope,
selectedRunTags,
trendPanelCompact,
@@ -896,6 +907,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
text: body.slice(0, 240),
hasValues: /错误\s+\d+/u.test(body) && /告警\s+\d+/u.test(body) && /合计\s+\d+/u.test(body),
hasTime: /UTC/u.test(body) || /\d{4}-\d{2}-\d{2}/u.test(body),
hasDuration: /运行\s+[\d.]+\s*分钟/u.test(body),
};
}
@@ -1059,7 +1071,9 @@ const ok = !navigationError
&& dom.checkScope?.fullWidth === true
&& dom.scopeLabels?.latestPointLegend === true
&& dom.scopeLabels?.historicalSamples === true
&& (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true))
&& (dom.trendDotCount === 0 || (dom.trendTooltip?.visible === true && dom.trendTooltip?.hasValues === true && dom.trendTooltip?.hasTime === true && dom.trendTooltip?.hasDuration === true))
&& dom.timingVisibility?.detailHasDuration === true
&& dom.timingVisibility?.listHasDuration === true
&& dom.trendPanelCompact?.ok === true
&& (dom.checkScope?.expectsAlertRows !== true || (dom.checkRows > 0 && dom.checkDialog?.opened === true && dom.checkDialog?.large === true))
&& dom.badCardTitleCount === 0
@@ -1515,6 +1529,7 @@ function renderDashboardResult(result: Record<string, unknown>): string {
const chartCounts = record(dom.chartCounts);
const latestRunCounts = record(dom.latestRunCounts);
const latestDetailSummary = record(dom.latestDetailSummary);
const timingVisibility = record(dom.timingVisibility);
const checkScope = record(dom.checkScope);
const selectedRunTags = record(dom.selectedRunTags);
const trendPanelCompact = record(dom.trendPanelCompact);
@@ -1569,6 +1584,13 @@ function renderDashboardResult(result: Record<string, unknown>): string {
overviewSamples.warning ?? "-",
]]),
"",
table(["RUN_MIN", "DETAIL_MIN_VISIBLE", "LIST_MIN_VISIBLE", "TOOLTIP_MIN_VISIBLE"], [[
latestRunCounts.durationMinutes ?? "-",
timingVisibility.detailHasDuration ?? "-",
timingVisibility.listHasDuration ?? "-",
timingVisibility.tooltipHasDuration ?? "-",
]]),
"",
table(["CHECK_SCOPE", "CHECK_RUN", "CHECK_TYPES", "CHECK_ERR_TYPES", "CHECK_ALERT_TYPES", "SAMPLE_ERR", "SAMPLE_ALERT", "CHECK_MATCH_LATEST", "CHECK_MATCH_DETAIL"], [[
checkScope.scope ?? "-",
checkScope.runId ?? "-",
+109 -5
View File
@@ -947,11 +947,14 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database
const findingTypeCount = numberOr(row.finding_count, 0);
const findingSampleCount = Object.values(severityCounts).reduce((sum, value) => sum + numberOr(value, 0), 0);
const findingAlertSampleCount = alertSeveritySampleCount(severityCounts);
const stored = id === null ? {} : record(readMetadata(db, `run.report.${id}`));
const scenarioId = stringOrNull(row.scenario_id);
const timing = dashboardRunTiming(config, row, stored);
return {
id,
runId: id,
scenario_id: stringOrNull(row.scenario_id),
scenarioId: stringOrNull(row.scenario_id),
scenario_id: scenarioId,
scenarioId,
status: stringOrNull(row.status),
node: stringOrNull(row.node) ?? config.node,
lane: stringOrNull(row.lane) ?? config.lane,
@@ -976,6 +979,18 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database
updatedAt: stringOrNull(row.updated_at),
interrupted_at: stringOrNull(row.interrupted_at),
interruptedAt: stringOrNull(row.interrupted_at),
startedAt: stringOrNull(timing.startedAt),
finishedAt: stringOrNull(timing.finishedAt),
durationMs: numberOrNull(timing.durationMs),
runDurationMs: numberOrNull(timing.durationMs),
durationMinutes: numberOrNull(timing.durationMinutes),
runDurationMinutes: numberOrNull(timing.durationMinutes),
durationSource: stringOrNull(timing.durationSource),
scenarioCadence: stringOrNull(timing.scenarioCadence),
scenarioCadenceMinutes: numberOrNull(timing.scenarioCadenceMinutes),
scenarioMaxRunMinutes: numberOrNull(timing.scenarioMaxRunMinutes),
schedulerIntervalMinutes: numberOrNull(timing.schedulerIntervalMinutes),
timing,
severityCounts,
maxSeverity,
traceability: runTraceability(config, row),
@@ -983,6 +998,41 @@ function dashboardRunSummary(config: WebProbeSentinelServiceConfig, db: Database
};
}
function dashboardRunTiming(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, stored: Record<string, unknown>): Record<string, unknown> {
const scenarioId = stringOrNull(row.scenario_id);
const scenario = scenarioId === null ? null : config.scenarios.find((item) => stringOrNull(item.id) === scenarioId) ?? null;
const summary = record(stored.summary);
const summaryElapsedMs = numberOrNull(summary.elapsedMs);
const startedAt = stringOrNull(row.created_at);
const finishedAt = stringOrNull(row.interrupted_at) ?? stringOrNull(row.updated_at);
const timestampDurationMs = durationMsBetween(startedAt, finishedAt);
const durationMs = summaryElapsedMs ?? timestampDurationMs;
const durationSource = summaryElapsedMs !== null
? "report-summary-elapsedMs"
: timestampDurationMs !== null
? "sqlite-created-updated"
: "unknown";
const scenarioCadence = scenario === null ? null : stringOrNull(scenario.cadence);
const scenarioCadenceSeconds = durationStringSeconds(scenarioCadence);
const scenarioMaxRunSeconds = scenario === null ? null : numberOrNull(scenario.maxRunSeconds);
return {
startedAt,
finishedAt,
durationMs,
durationMinutes: minutesFromMs(durationMs),
durationSource,
scenarioCadence,
scenarioCadenceSeconds,
scenarioCadenceMinutes: minutesFromSeconds(scenarioCadenceSeconds),
scenarioMaxRunSeconds,
scenarioMaxRunMinutes: minutesFromSeconds(scenarioMaxRunSeconds),
schedulerIntervalMs: config.schedulerIntervalMs,
schedulerIntervalMinutes: minutesFromMs(config.schedulerIntervalMs),
sourceOfTruth: "sqlite-run-timestamps+report-summary+yaml-scenario-runtime",
valuesRedacted: true,
};
}
function alertSeveritySampleCount(counts: Record<string, number>): number {
return numberOr(counts.red, 0)
+ numberOr(counts.critical, 0)
@@ -1453,6 +1503,43 @@ function ageSeconds(iso: string): number | null {
return Number.isFinite(parsed) ? Math.max(0, Math.round((Date.now() - parsed) / 1000)) : null;
}
function durationMsBetween(startIso: string | null, endIso: string | null): number | null {
if (startIso === null || endIso === null) return null;
const start = Date.parse(startIso);
const end = Date.parse(endIso);
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
return end - start;
}
function durationStringSeconds(value: string | null): number | null {
if (value === null) return null;
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/iu.exec(value.trim());
if (match === null) return null;
const amount = Number(match[1]);
if (!Number.isFinite(amount)) return null;
const unit = match[2].toLowerCase();
const seconds = unit === "ms" ? amount / 1000
: unit === "s" ? amount
: unit === "m" ? amount * 60
: unit === "h" ? amount * 3600
: amount * 86400;
return Math.round(seconds * 1000) / 1000;
}
function minutesFromMs(value: unknown): number | null {
const ms = numberOrNull(value);
return ms === null ? null : roundMinutes(ms / 60_000);
}
function minutesFromSeconds(value: unknown): number | null {
const seconds = numberOrNull(value);
return seconds === null ? null : roundMinutes(seconds / 60);
}
function roundMinutes(value: number): number {
return Math.round(value * 100) / 100;
}
function maxSeverityFromCounts(counts: Record<string, number>): string | null {
let best: string | null = null;
for (const [severity, count] of Object.entries(counts)) {
@@ -1576,14 +1663,14 @@ function reportRunView(config: WebProbeSentinelServiceConfig, db: Database, view
const findings = findingsForRun(config, db, selectedRunId, 50);
const views = record(stored.views);
const storedView = record(views[view]);
const renderedText = view === "findings" ? renderStoredFindings(row, findings) : typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(row, stored, findings) : null;
const renderedText = view === "findings" ? renderStoredFindings(row, findings) : typeof storedView.renderedText === "string" ? storedView.renderedText : view === "summary" ? renderStoredSummary(config, row, stored, findings) : null;
if (renderedText === null) {
return { ok: false, error: "report-view-not-indexed", runId: selectedRunId, view, availableViews: Object.keys(views), valuesRedacted: true };
}
return {
ok: true,
view,
run: row,
run: dashboardRunSummary(config, db, row),
summary: record(stored.summary),
findings,
renderedText,
@@ -1622,14 +1709,16 @@ function formatStoredFindingTiming(item: Record<string, unknown>): string | null
return `timing=${pieces.join(" ")}`;
}
function renderStoredSummary(row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
function renderStoredSummary(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, stored: Record<string, unknown>, findings: readonly Record<string, unknown>[]): string {
const summary = record(stored.summary);
const timing = dashboardRunTiming(config, row, stored);
return [
"Web Probe Sentinel Report",
"=======================================================",
`run=${stringOrNull(row.id) ?? "-"} scenario=${stringOrNull(row.scenario_id) ?? "-"} status=${stringOrNull(row.status) ?? "-"}`,
`observer=${stringOrNull(row.observer_id) ?? "-"} stateDir=${stringOrNull(row.state_dir) ?? "-"}`,
`report=${stringOrNull(row.report_json_sha256) ?? "-"} artifacts=${String(row.artifact_count ?? 0)} findings=${String(row.finding_count ?? findings.length)}`,
`timing=${formatRunTimingSummary(timing)}`,
`publicOrigin=${stringOrNull(stored.publicOrigin) ?? "-"}`,
`analysisWindow=${formatStoredAnalysisWindow(summary.analysisWindow)}`,
"",
@@ -1658,10 +1747,25 @@ function formatStoredAnalysisWindow(value: unknown): string {
return fields.length === 0 ? "-" : fields.map(([key, item]) => `${key}=${item}`).join(" ");
}
function formatRunTimingSummary(value: Record<string, unknown>): string {
const fields = [
["durationMinutes", numberOrNull(value.durationMinutes)],
["durationSource", stringOrNull(value.durationSource)],
["scenarioCadenceMinutes", numberOrNull(value.scenarioCadenceMinutes)],
["scenarioMaxRunMinutes", numberOrNull(value.scenarioMaxRunMinutes)],
["schedulerIntervalMinutes", numberOrNull(value.schedulerIntervalMinutes)],
].filter((entry): entry is [string, string | number] => entry[1] !== null);
return fields.length === 0 ? "-" : fields.map(([key, item]) => `${key}=${item}`).join(" ");
}
function thisMaintenanceFlag(input: Record<string, unknown>): number {
return input.maintenance === true ? 1 : 0;
}
function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function numberOr(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}