Merge pull request #1349 from pikasTech/fix/1338-dashboard-verify-contract
fix: simplify sentinel dashboard verify
This commit is contained in:
@@ -467,6 +467,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
|
||||
artifactCount: transport.artifactCount ?? null,
|
||||
expectedArtifactCount: transport.expectedArtifactCount ?? null,
|
||||
downloadFailure: transport.downloadFailure ?? null,
|
||||
pollFailure: transport.pollFailure ?? null,
|
||||
},
|
||||
result: compactCommand(result),
|
||||
degradedReason: ok ? null : dashboardDegradedReason(result, transport, page, screenshotOk),
|
||||
@@ -517,7 +518,7 @@ page.on("requestfailed", (request) => {
|
||||
let httpStatus = null;
|
||||
let navigationError = null;
|
||||
let navigationAttempts = 0;
|
||||
const maxNavigationAttempts = 3;
|
||||
const maxNavigationAttempts = 2;
|
||||
const perAttemptTimeout = Math.max(5000, Math.floor(timeout / maxNavigationAttempts));
|
||||
for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) {
|
||||
navigationAttempts = attempt;
|
||||
@@ -525,17 +526,14 @@ for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await page.goto(url, { timeout: perAttemptTimeout, waitUntil: "domcontentloaded" });
|
||||
httpStatus = response?.status() ?? null;
|
||||
await page.waitForLoadState("networkidle", { timeout: Math.min(10000, perAttemptTimeout) }).catch(() => {});
|
||||
await page.waitForFunction(() => {
|
||||
const root = document.querySelector("#monitor-web-root");
|
||||
if (!root) return false;
|
||||
const ready = root.getAttribute("data-monitor-ready") === "true";
|
||||
const error = document.querySelector("#monitor-web-error");
|
||||
const runs = document.querySelectorAll(".run-list .run-row").length;
|
||||
const trend = document.querySelector("[data-monitor-trend-curve]");
|
||||
return ready && (error || runs > 0 || trend);
|
||||
}, null, { timeout: Math.min(15000, perAttemptTimeout) }).catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
return ready || Boolean(error);
|
||||
}, null, { timeout: Math.min(10000, perAttemptTimeout) }).catch(() => {});
|
||||
await page.waitForTimeout(300);
|
||||
const appReady = await page.evaluate(() => document.querySelector("#monitor-web-root")?.getAttribute("data-monitor-ready") === "true").catch(() => false);
|
||||
if (appReady || attempt === maxNavigationAttempts) break;
|
||||
} catch (error) {
|
||||
@@ -545,24 +543,6 @@ for (let attempt = 1; attempt <= maxNavigationAttempts; attempt += 1) {
|
||||
await page.waitForTimeout(750 * attempt);
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
const detailPane = document.querySelector(".workspace-grid .pane-detail");
|
||||
if (detailPane instanceof HTMLElement) detailPane.scrollTop = Math.min(96, Math.max(0, detailPane.scrollHeight - detailPane.clientHeight));
|
||||
}).catch(() => {});
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
const trendHoverPoint = await page.evaluate(() => {
|
||||
const target = document.querySelector(".trend-dot-hit .trend-dot-duration");
|
||||
if (!(target instanceof SVGElement)) return null;
|
||||
const rect = target.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return null;
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
||||
}).catch(() => null);
|
||||
if (trendHoverPoint) {
|
||||
await page.mouse.move(trendHoverPoint.x, trendHoverPoint.y);
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
if (captureScreenshot && screenshotPath) {
|
||||
await page.screenshot({ path: screenshotPath, fullPage, animations: "disabled" }).catch((error) => {
|
||||
pageErrors.push({ message: "screenshot failed: " + String(error?.message || error).slice(0, 400) });
|
||||
@@ -570,53 +550,48 @@ if (captureScreenshot && screenshotPath) {
|
||||
}
|
||||
|
||||
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId }) => {
|
||||
const visible = (element) => Boolean(element && !element.hidden);
|
||||
const text = (selector) => String(document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim();
|
||||
const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
||||
const errorSampleCount = (counts) => numberValue(counts?.red) + numberValue(counts?.critical) + numberValue(counts?.error);
|
||||
const warningSampleCount = (counts) => numberValue(counts?.warning) + numberValue(counts?.warn) + numberValue(counts?.amber);
|
||||
const allSampleCount = (counts) => Object.values(counts || {}).reduce((sum, value) => sum + numberValue(value), 0);
|
||||
const objectOrNull = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
||||
const root = document.querySelector("#monitor-web-root");
|
||||
const shell = document.querySelector("[data-monitor-shell='true']");
|
||||
const error = document.querySelector("#monitor-web-error");
|
||||
const trend = document.querySelector("[data-monitor-trend-curve]");
|
||||
const trendTooltip = document.querySelector("[data-monitor-trend-tooltip='true']");
|
||||
const timeline = document.querySelector("[data-monitor-timeline='true']");
|
||||
const workspace = document.querySelector("[data-monitor-independent-scroll='true']");
|
||||
const checksPanel = document.querySelector("[data-monitor-checks='true']");
|
||||
const panes = Array.from(document.querySelectorAll(".workspace-grid .pane, [data-monitor-checks='true']"));
|
||||
const detailPane = document.querySelector(".workspace-grid .pane-detail");
|
||||
const detailHeader = detailPane?.querySelector(".pane-header");
|
||||
const checksHeader = checksPanel?.querySelector(".pane-header");
|
||||
const internalTextPattern = /水合|投影|Trace|trace|Shell|API|DOM|Console|console|Runner|runner|JSONL|steer|facts|分页|HTTP|http|requestfailed|pageerror|Final Response|Code Agent|web-probe|observe|analyzer|终态/u;
|
||||
const checkRows = Array.from(document.querySelectorAll("[data-check-row='true']"));
|
||||
const cards = checkRows.slice(0, 8).map((row) => ({
|
||||
code: String(row.querySelector(".check-code")?.textContent || "").trim(),
|
||||
title: String(row.querySelector(".check-title-cell strong")?.textContent || row.querySelector("strong")?.textContent || "").trim(),
|
||||
body: String(row.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180),
|
||||
}));
|
||||
const badCardTitles = cards.filter((card) => internalTextPattern.test(card.title));
|
||||
const badCardBodies = cards.filter((card) => internalTextPattern.test(card.body));
|
||||
const legendTexts = Array.from(document.querySelectorAll(".trend-legend .legend-item")).map((item) => String(item.textContent || "").replace(/\s+/g, " ").trim());
|
||||
const legendNumber = (label) => {
|
||||
const row = legendTexts.find((item) => item.includes(label)) || "";
|
||||
const match = /(\d+(?:\.\d+)?)/u.exec(row);
|
||||
return match ? Number(match[1]) : null;
|
||||
};
|
||||
const chartTiming = {
|
||||
latestMinutes: legendNumber("最新运行耗时"),
|
||||
maxMinutes: legendNumber("最近最高耗时"),
|
||||
title: text("#trend-heading"),
|
||||
legendHasDuration: legendTexts.some((item) => item.includes("最新运行耗时")),
|
||||
};
|
||||
const dataset = root ? {
|
||||
node: root.getAttribute("data-node"),
|
||||
lane: root.getAttribute("data-lane"),
|
||||
sentinelId: root.getAttribute("data-sentinel-id"),
|
||||
basePath: root.getAttribute("data-base-path"),
|
||||
contractVersion: root.getAttribute("data-contract-version"),
|
||||
ready: root.getAttribute("data-monitor-ready"),
|
||||
} : {};
|
||||
const apiUrl = (path) => {
|
||||
const basePath = root?.getAttribute("data-base-path") || expectedRoutePrefix || "";
|
||||
const prefix = basePath.replace(/\/+$/u, "");
|
||||
return (prefix + (path.startsWith("/") ? path : "/" + path)) || path;
|
||||
};
|
||||
const runsPayload = await fetch(apiUrl("/api/runs?limit=30&sort=updated"), { cache: "no-store" }).then((item) => item.json()).catch(() => null);
|
||||
const overviewPayload = await fetch(apiUrl("/api/overview"), { cache: "no-store" }).then((item) => item.json()).catch(() => null);
|
||||
const runs = Array.isArray(runsPayload?.runs) ? runsPayload.runs : Array.isArray(runsPayload?.items) ? runsPayload.items : [];
|
||||
const fetchJson = async (path) => {
|
||||
try {
|
||||
const response = await fetch(apiUrl(path), { cache: "no-store" });
|
||||
return {
|
||||
ok: response.ok,
|
||||
httpStatus: response.status,
|
||||
body: await response.json().catch(() => null),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
httpStatus: null,
|
||||
body: null,
|
||||
error: String(error?.message || error).slice(0, 300),
|
||||
};
|
||||
}
|
||||
};
|
||||
const [overviewResult, runsResult] = await Promise.all([
|
||||
fetchJson("/api/overview"),
|
||||
fetchJson("/api/runs?limit=30&sort=updated"),
|
||||
]);
|
||||
const overviewPayload = objectOrNull(overviewResult.body) || {};
|
||||
const runsPayload = objectOrNull(runsResult.body) || {};
|
||||
const runs = Array.isArray(runsPayload.runs) ? runsPayload.runs : Array.isArray(runsPayload.items) ? runsPayload.items : [];
|
||||
const latestRun = runs[0] || null;
|
||||
const latestCounts = latestRun && latestRun.severityCounts && typeof latestRun.severityCounts === "object" && !Array.isArray(latestRun.severityCounts)
|
||||
? latestRun.severityCounts
|
||||
@@ -625,178 +600,9 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
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,
|
||||
all: 0,
|
||||
errorSamples: errorSampleCount(latestCounts),
|
||||
warningSamples: warningSampleCount(latestCounts),
|
||||
alertSamples: errorSampleCount(latestCounts) + warningSampleCount(latestCounts),
|
||||
allSamples: allSampleCount(latestCounts),
|
||||
severityKeys: Object.keys(latestCounts).sort(),
|
||||
};
|
||||
const latestDetailPayload = latestRunCounts.runId
|
||||
? await fetch(apiUrl("/api/runs/" + encodeURIComponent(latestRunCounts.runId)), { cache: "no-store" }).then((item) => item.json()).catch(() => null)
|
||||
: null;
|
||||
const latestDetailRows = Array.isArray(latestDetailPayload?.findings) ? latestDetailPayload.findings : [];
|
||||
const rowSeverity = (row) => {
|
||||
const raw = String(row?.maxSeverity || row?.checkLevel || row?.severity || row?.level || "").toLowerCase();
|
||||
if (["red", "critical", "error", "blocked", "failed"].includes(raw)) return "red";
|
||||
if (["warning", "warn", "amber"].includes(raw)) return "warning";
|
||||
if (["info", "notice"].includes(raw)) return "info";
|
||||
return "healthy";
|
||||
};
|
||||
const sampleCount = (row) => Number.isFinite(Number(row?.count)) ? Number(row.count) : 1;
|
||||
const summarizeRows = (rows) => {
|
||||
const errorRows = rows.filter((row) => rowSeverity(row) === "red");
|
||||
const warningRows = rows.filter((row) => rowSeverity(row) === "warning");
|
||||
const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0);
|
||||
return {
|
||||
typeCount: rows.length,
|
||||
errorTypeCount: errorRows.length,
|
||||
warningTypeCount: warningRows.length,
|
||||
alertTypeCount: errorRows.length + warningRows.length,
|
||||
errorSamples: sum(errorRows),
|
||||
warningSamples: sum(warningRows),
|
||||
alertSamples: sum(errorRows) + sum(warningRows),
|
||||
};
|
||||
};
|
||||
const latestDetailSummary = summarizeRows(latestDetailRows);
|
||||
const expectsAlertRows = latestDetailSummary.alertTypeCount > 0;
|
||||
latestRunCounts.typeCount = latestDetailSummary.typeCount;
|
||||
latestRunCounts.error = latestDetailSummary.errorTypeCount;
|
||||
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) => {
|
||||
const viewportHeight = window.innerHeight || 1;
|
||||
const heightPx = rect ? Math.round(rect.height) : null;
|
||||
const ratio = rect ? Math.round((rect.height / viewportHeight) * 1000) / 1000 : null;
|
||||
return {
|
||||
present: Boolean(rect),
|
||||
heightPx,
|
||||
ratio,
|
||||
targetPx: Math.round(viewportHeight * 0.8),
|
||||
bounded80: Boolean(rect && rect.height <= viewportHeight * 0.82 + 1),
|
||||
near80: Boolean(rect && rect.height >= viewportHeight * 0.74 && rect.height <= viewportHeight * 0.82 + 1),
|
||||
};
|
||||
};
|
||||
const workspacePaneHeights = Array.from(document.querySelectorAll(".workspace-grid > .pane")).map((pane) => heightSummary(pane.getBoundingClientRect()));
|
||||
const stackedWorkspace = window.matchMedia("(max-width: 1120px)").matches;
|
||||
const panelHeights = {
|
||||
viewportHeight: window.innerHeight,
|
||||
stackedWorkspace,
|
||||
workspace: heightSummary(workspaceRect),
|
||||
checks: heightSummary(checksRect),
|
||||
workspacePanes: workspacePaneHeights,
|
||||
workspacePaneBounded: workspacePaneHeights.length >= 2 && workspacePaneHeights.every((item) => item.bounded80 === true),
|
||||
workspaceOk: false,
|
||||
checksOk: false,
|
||||
};
|
||||
panelHeights.workspaceOk = stackedWorkspace
|
||||
? panelHeights.workspacePaneBounded === true
|
||||
: panelHeights.workspace.near80 === true && panelHeights.workspacePaneBounded === true;
|
||||
panelHeights.checksOk = panelHeights.checks.near80 === true;
|
||||
const checkScope = {
|
||||
present: Boolean(checksPanel),
|
||||
scope: checksPanel?.getAttribute("data-check-scope") || null,
|
||||
runId: checksPanel?.getAttribute("data-check-run-id") || null,
|
||||
typeCount: numberValue(checksPanel?.getAttribute("data-check-type-count")),
|
||||
errorTypeCount: numberValue(checksPanel?.getAttribute("data-check-error-type-count")),
|
||||
warningTypeCount: numberValue(checksPanel?.getAttribute("data-check-warning-type-count")),
|
||||
alertTypeCount: numberValue(checksPanel?.getAttribute("data-check-alert-type-count")),
|
||||
errorSamples: numberValue(checksPanel?.getAttribute("data-check-error-samples")),
|
||||
warningSamples: numberValue(checksPanel?.getAttribute("data-check-warning-samples")),
|
||||
alertSamples: numberValue(checksPanel?.getAttribute("data-check-alert-samples")),
|
||||
visibleRowCount: document.querySelectorAll("[data-check-row='true']").length,
|
||||
visibleAlertSamples: numberValue(checksPanel?.getAttribute("data-visible-check-alert-samples")),
|
||||
expectsAlertRows,
|
||||
matchesLatestRun: false,
|
||||
matchesRunDetail: false,
|
||||
belowWorkspace: Boolean(workspaceRect && checksRect && checksRect.top >= workspaceRect.bottom - 2),
|
||||
fullWidth: Boolean(workspaceRect && checksRect && checksRect.width >= workspaceRect.width - 2),
|
||||
};
|
||||
const runListRowFor = (runId) => Array.from(document.querySelectorAll(".run-list .run-row"))
|
||||
.find((row) => row.getAttribute("data-run-id") === runId) || null;
|
||||
const selectedRunRow = runListRowFor(latestRunCounts.runId) || document.querySelector(".run-list .run-row.selected");
|
||||
const selectedRunTags = {
|
||||
error: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]),
|
||||
warning: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]),
|
||||
errorVisible: Boolean(selectedRunRow?.querySelector("[data-run-error-tag='true']")),
|
||||
warningVisible: Boolean(selectedRunRow?.querySelector("[data-run-warning-tag='true']")),
|
||||
rowSelected: Boolean(selectedRunRow?.classList.contains("selected")),
|
||||
matchesRunDetail: false,
|
||||
};
|
||||
selectedRunTags.matchesRunDetail = selectedRunTags.error === latestDetailSummary.errorTypeCount
|
||||
&& selectedRunTags.warning === latestDetailSummary.warningTypeCount
|
||||
&& selectedRunTags.errorVisible === (latestDetailSummary.errorTypeCount > 0)
|
||||
&& selectedRunTags.warningVisible === (latestDetailSummary.warningTypeCount > 0)
|
||||
&& selectedRunTags.rowSelected === true;
|
||||
checkScope.matchesLatestRun = checkScope.present === true
|
||||
&& checkScope.scope === "run"
|
||||
&& checkScope.runId === latestRunCounts.runId
|
||||
&& checkScope.errorTypeCount === latestRunCounts.error
|
||||
&& checkScope.warningTypeCount === latestRunCounts.warning
|
||||
&& checkScope.alertTypeCount === latestRunCounts.total;
|
||||
checkScope.matchesRunDetail = checkScope.present === true
|
||||
&& checkScope.typeCount === latestDetailSummary.typeCount
|
||||
&& checkScope.errorTypeCount === latestDetailSummary.errorTypeCount
|
||||
&& checkScope.warningTypeCount === latestDetailSummary.warningTypeCount
|
||||
&& checkScope.alertTypeCount === latestDetailSummary.alertTypeCount
|
||||
&& checkScope.errorSamples === latestDetailSummary.errorSamples
|
||||
&& checkScope.warningSamples === latestDetailSummary.warningSamples
|
||||
&& checkScope.alertSamples === latestDetailSummary.alertSamples
|
||||
&& checkScope.visibleRowCount === latestDetailSummary.alertTypeCount
|
||||
&& checkScope.visibleAlertSamples === latestDetailSummary.alertSamples;
|
||||
const overviewCounts = overviewPayload?.severityCounts && typeof overviewPayload.severityCounts === "object" && !Array.isArray(overviewPayload.severityCounts)
|
||||
? overviewPayload.severityCounts
|
||||
: {};
|
||||
const overviewSamples = {
|
||||
error: errorSampleCount(overviewCounts),
|
||||
warning: warningSampleCount(overviewCounts),
|
||||
total: errorSampleCount(overviewCounts) + warningSampleCount(overviewCounts),
|
||||
allSamples: allSampleCount(overviewCounts),
|
||||
};
|
||||
chartTiming.ok = chartTiming.title.includes("运行耗时") && chartTiming.legendHasDuration === true && Number.isFinite(chartTiming.latestMinutes);
|
||||
chartTiming.matchesLatestRun = chartTiming.ok === true
|
||||
&& Math.abs(Number(chartTiming.latestMinutes) - Number(latestRunCounts.durationMinutes)) < 0.02;
|
||||
const trendPanel = document.querySelector(".trend-panel");
|
||||
const trendLegend = document.querySelector(".trend-panel .trend-legend");
|
||||
const trendPanelRect = trendPanel?.getBoundingClientRect();
|
||||
const trendLegendRect = trendLegend?.getBoundingClientRect();
|
||||
const trendPanelCompact = {
|
||||
present: Boolean(trendPanelRect && trendLegendRect),
|
||||
bottomSlackPx: trendPanelRect && trendLegendRect ? Math.round(trendPanelRect.bottom - trendLegendRect.bottom) : null,
|
||||
ok: Boolean(trendPanelRect && trendLegendRect && trendPanelRect.bottom - trendLegendRect.bottom <= 28),
|
||||
};
|
||||
const firstCheckRow = document.querySelector("[data-check-row='true']");
|
||||
let checkDialog = { opened: false, title: "", width: null, height: null, large: false };
|
||||
if (firstCheckRow instanceof HTMLElement) {
|
||||
firstCheckRow.click();
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 80));
|
||||
const dialog = document.querySelector("[data-check-dialog='true'] .check-dialog");
|
||||
const rect = dialog?.getBoundingClientRect();
|
||||
checkDialog = {
|
||||
opened: Boolean(dialog),
|
||||
title: String(dialog?.querySelector("#check-dialog-title")?.textContent || "").trim(),
|
||||
width: rect ? Math.round(rect.width) : null,
|
||||
height: rect ? Math.round(rect.height) : null,
|
||||
large: Boolean(rect && rect.width >= Math.min(900, window.innerWidth * 0.7) && rect.height >= Math.min(460, window.innerHeight * 0.5)),
|
||||
};
|
||||
const close = dialog?.querySelector("button[aria-label='关闭监测项详情']");
|
||||
if (close instanceof HTMLElement) close.click();
|
||||
}
|
||||
const datasetSentinelId = root?.getAttribute("data-sentinel-id") || "";
|
||||
const datasetSentinelId = String(dataset.sentinelId || "");
|
||||
const finalPath = new URL(window.location.href).pathname.replace(/\/+$/u, "") || "/";
|
||||
const expectedPath = expectedRoutePrefix.replace(/\/+$/u, "") || "/";
|
||||
const routePrefixMatches = expectedPath === "/" ? finalPath === "/" : finalPath === expectedPath || finalPath.startsWith(expectedPath + "/");
|
||||
@@ -804,16 +610,15 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
expectedSentinelId,
|
||||
expectedRoutePrefix,
|
||||
datasetSentinelId,
|
||||
overviewSentinelId: overviewPayload?.sentinelId || null,
|
||||
runsSentinelId: runsPayload?.sentinelId || null,
|
||||
overviewSentinelId: overviewPayload.sentinelId || null,
|
||||
runsSentinelId: runsPayload.sentinelId || null,
|
||||
finalPath,
|
||||
routePrefixMatches,
|
||||
datasetMatches: expectedSentinelId ? datasetSentinelId === expectedSentinelId : true,
|
||||
overviewMatches: expectedSentinelId ? overviewPayload?.sentinelId === expectedSentinelId : true,
|
||||
runsPayloadMatches: expectedSentinelId ? runsPayload?.sentinelId === expectedSentinelId : true,
|
||||
overviewMatches: expectedSentinelId ? overviewPayload.sentinelId === expectedSentinelId : true,
|
||||
runsPayloadMatches: expectedSentinelId ? runsPayload.sentinelId === expectedSentinelId : true,
|
||||
runRowsMatch: expectedSentinelId ? runs.every((run) => (run?.sentinelId || expectedSentinelId) === expectedSentinelId) : true,
|
||||
};
|
||||
const statusText = text(".status-strip");
|
||||
const doc = document.documentElement;
|
||||
const body = document.body;
|
||||
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||
@@ -832,7 +637,6 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
if (overflow.length < 5) {
|
||||
overflow.push({
|
||||
tag: element.tagName.toLowerCase(),
|
||||
className: String(element.className || "").slice(0, 40),
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
width: Math.round(rect.width),
|
||||
@@ -846,55 +650,28 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
return {
|
||||
shell: Boolean(root && shell),
|
||||
ready: root?.getAttribute("data-monitor-ready") === "true",
|
||||
dataset: root ? {
|
||||
node: root.getAttribute("data-node"),
|
||||
lane: root.getAttribute("data-lane"),
|
||||
sentinelId: datasetSentinelId,
|
||||
basePath: root.getAttribute("data-base-path"),
|
||||
contractVersion: root.getAttribute("data-contract-version"),
|
||||
} : {},
|
||||
dataset,
|
||||
contract: {
|
||||
htmlShell: Boolean(root && shell),
|
||||
appReady: root?.getAttribute("data-monitor-ready") === "true",
|
||||
apiOverview: overviewResult.ok === true && overviewPayload.ok !== false,
|
||||
apiRuns: runsResult.ok === true && Array.isArray(runs),
|
||||
runCount: runs.length,
|
||||
latestRunId: latestRunCounts.runId,
|
||||
latestFindingTypeCount: latestRunCounts.typeCount,
|
||||
},
|
||||
sentinelBoundary,
|
||||
title: document.title,
|
||||
finalUrl: window.location.href,
|
||||
statusText: text(".topbar .pill"),
|
||||
subtitle: text(".subtitle"),
|
||||
summaryText: text(".status-strip"),
|
||||
runRows: document.querySelectorAll(".run-list .run-row").length,
|
||||
runRows: runs.length,
|
||||
checkRows: document.querySelectorAll("[data-check-row='true']").length,
|
||||
badCardTitleCount: badCardTitles.length,
|
||||
badCardBodyCount: badCardBodies.length,
|
||||
trendCurve: Boolean(trend),
|
||||
trendDotCount: document.querySelectorAll(".trend-dot-hit").length,
|
||||
trendTooltip: trendTooltipSummary,
|
||||
trendPanelText: text("#trend-heading"),
|
||||
chartTiming,
|
||||
latestRunCounts,
|
||||
latestDetailSummary,
|
||||
timingVisibility,
|
||||
checkScope,
|
||||
selectedRunTags,
|
||||
trendPanelCompact,
|
||||
checkDialog,
|
||||
overviewSamples,
|
||||
panelHeights,
|
||||
scopeLabels: {
|
||||
latestPointLegend: legendTexts.some((item) => item.includes("最新运行耗时")),
|
||||
historicalSamples: legendTexts.some((item) => item.includes("历史样本累计")) || statusText.includes("历史错误样本"),
|
||||
},
|
||||
timelineItems: document.querySelectorAll(".timeline-list .timeline-item").length,
|
||||
timelineVisible: Boolean(timeline),
|
||||
errorVisible: visible(error),
|
||||
errorText: visible(error) ? text("#monitor-web-error").slice(0, 500) : "",
|
||||
scrollModel: {
|
||||
workspace: Boolean(workspace),
|
||||
paneCount: panes.length,
|
||||
independentScroll: panes.length >= 3 && panes.every((pane) => {
|
||||
const style = window.getComputedStyle(pane);
|
||||
return style.overflowY === "auto" || style.overflowY === "scroll";
|
||||
}),
|
||||
stickyHeader: stickyHeaderSummary(detailPane, detailHeader),
|
||||
stickyChecksHeader: stickyHeaderSummary(checksPanel, checksHeader),
|
||||
api: {
|
||||
overview: { ok: overviewResult.ok, httpStatus: overviewResult.httpStatus },
|
||||
runs: { ok: runsResult.ok, httpStatus: runsResult.httpStatus },
|
||||
},
|
||||
errorVisible: Boolean(error && !error.hidden),
|
||||
errorText: error && !error.hidden ? String(error.textContent || "").replace(/\s+/g, " ").trim().slice(0, 500) : "",
|
||||
layout: {
|
||||
viewport,
|
||||
documentSize,
|
||||
@@ -903,193 +680,23 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
|
||||
overflow: overflow.slice(0, 2),
|
||||
},
|
||||
};
|
||||
|
||||
function tooltipSummary(element) {
|
||||
const body = String(element?.textContent || "").replace(/\s+/g, " ").trim();
|
||||
return {
|
||||
visible: Boolean(element && body.length > 0),
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
function stickyHeaderSummary(pane, header) {
|
||||
if (!(pane instanceof HTMLElement) || !(header instanceof HTMLElement)) {
|
||||
return { present: false, coversScroll: false, backgroundOpaque: false, detailScrollTop: null };
|
||||
}
|
||||
const rect = header.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(header);
|
||||
const sampleX = Math.round(rect.left + Math.min(32, Math.max(2, rect.width / 2)));
|
||||
const sampleY = Math.round(rect.top + Math.min(12, Math.max(2, rect.height / 2)));
|
||||
const topElement = document.elementFromPoint(sampleX, sampleY);
|
||||
return {
|
||||
present: true,
|
||||
detailScrollTop: pane.scrollTop,
|
||||
headerTop: Math.round(rect.top),
|
||||
headerBottom: Math.round(rect.bottom),
|
||||
zIndex: style.zIndex,
|
||||
backgroundColor: style.backgroundColor,
|
||||
coversScroll: Boolean(topElement && header.contains(topElement)),
|
||||
backgroundOpaque: backgroundIsOpaque(style.backgroundColor),
|
||||
topElementClass: String(topElement?.className || "").slice(0, 80),
|
||||
};
|
||||
}
|
||||
|
||||
function backgroundIsOpaque(value) {
|
||||
const rgba = /rgba?\(([^)]+)\)/u.exec(value);
|
||||
if (rgba === null) return value.length > 0 && value !== "transparent";
|
||||
const parts = rgba[1].split(",").map((part) => part.trim());
|
||||
if (parts.length < 4) return true;
|
||||
return Number(parts[3]) >= 0.99;
|
||||
}
|
||||
}, { expectedRoutePrefix, expectedSentinelId });
|
||||
|
||||
const runFilterProbe = await page.evaluate(async ({ expectedRoutePrefix }) => {
|
||||
const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
||||
const root = document.querySelector("#monitor-web-root");
|
||||
const apiUrl = (path) => {
|
||||
const basePath = root?.getAttribute("data-base-path") || expectedRoutePrefix || "";
|
||||
const prefix = basePath.replace(/\/+$/u, "");
|
||||
return (prefix + (path.startsWith("/") ? path : "/" + path)) || path;
|
||||
};
|
||||
const rowSeverity = (row) => {
|
||||
const raw = String(row?.maxSeverity || row?.checkLevel || row?.severity || row?.level || "").toLowerCase();
|
||||
if (["red", "critical", "error", "blocked", "failed"].includes(raw)) return "red";
|
||||
if (["warning", "warn", "amber"].includes(raw)) return "warning";
|
||||
if (["info", "notice"].includes(raw)) return "info";
|
||||
return "healthy";
|
||||
};
|
||||
const sampleCount = (row) => Number.isFinite(Number(row?.count)) ? Number(row.count) : 1;
|
||||
const summarizeRows = (rows) => {
|
||||
const errorRows = rows.filter((row) => rowSeverity(row) === "red");
|
||||
const warningRows = rows.filter((row) => rowSeverity(row) === "warning");
|
||||
const sum = (items) => items.reduce((total, row) => total + sampleCount(row), 0);
|
||||
return {
|
||||
typeCount: rows.length,
|
||||
errorTypeCount: errorRows.length,
|
||||
warningTypeCount: warningRows.length,
|
||||
alertTypeCount: errorRows.length + warningRows.length,
|
||||
errorSamples: sum(errorRows),
|
||||
warningSamples: sum(warningRows),
|
||||
alertSamples: sum(errorRows) + sum(warningRows),
|
||||
};
|
||||
};
|
||||
const runListRowFor = (runId) => Array.from(document.querySelectorAll(".run-list .run-row"))
|
||||
.find((row) => row.getAttribute("data-run-id") === runId) || null;
|
||||
const panelCounts = () => {
|
||||
const panel = document.querySelector("[data-monitor-checks='true']");
|
||||
const panelRunId = panel?.getAttribute("data-check-run-id") || null;
|
||||
const selectedRunRow = runListRowFor(panelRunId) || document.querySelector(".run-list .run-row.selected");
|
||||
return {
|
||||
present: Boolean(panel),
|
||||
runId: panelRunId,
|
||||
typeCount: numberValue(panel?.getAttribute("data-check-type-count")),
|
||||
errorTypeCount: numberValue(panel?.getAttribute("data-check-error-type-count")),
|
||||
warningTypeCount: numberValue(panel?.getAttribute("data-check-warning-type-count")),
|
||||
alertTypeCount: numberValue(panel?.getAttribute("data-check-alert-type-count")),
|
||||
errorSamples: numberValue(panel?.getAttribute("data-check-error-samples")),
|
||||
warningSamples: numberValue(panel?.getAttribute("data-check-warning-samples")),
|
||||
alertSamples: numberValue(panel?.getAttribute("data-check-alert-samples")),
|
||||
visibleRowCount: document.querySelectorAll("[data-check-row='true']").length,
|
||||
visibleAlertSamples: numberValue(panel?.getAttribute("data-visible-check-alert-samples")),
|
||||
selectedRunErrorTag: numberValue(String(selectedRunRow?.querySelector("[data-run-error-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]),
|
||||
selectedRunWarningTag: numberValue(String(selectedRunRow?.querySelector("[data-run-warning-tag='true']")?.textContent || "").match(/(\d+)/u)?.[1]),
|
||||
selectedRunRowSelected: Boolean(selectedRunRow?.classList.contains("selected")),
|
||||
};
|
||||
};
|
||||
const waitForRun = async (runId) => {
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
const current = panelCounts();
|
||||
if (current.runId === runId && current.present === true) return true;
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 100));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const select = document.querySelector("select[aria-label='选择运行记录']");
|
||||
if (!(select instanceof HTMLSelectElement)) {
|
||||
return { ok: false, requestedRunId: null, reason: "run-select-missing" };
|
||||
}
|
||||
const options = Array.from(select.options).filter((option) => option.value);
|
||||
const requestedOption = options[Math.min(2, Math.max(0, options.length - 1))] || options[0] || null;
|
||||
const requestedRunId = requestedOption?.value || null;
|
||||
const fallbackOption = requestedOption || null;
|
||||
const targetRunId = fallbackOption?.value || requestedRunId;
|
||||
if (!targetRunId) return { ok: false, requestedRunId, reason: "run-option-missing" };
|
||||
select.value = targetRunId;
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
const panelReady = await waitForRun(targetRunId);
|
||||
const detailPayload = await fetch(apiUrl("/api/runs/" + encodeURIComponent(targetRunId)), { cache: "no-store" }).then((item) => item.json()).catch(() => null);
|
||||
const detailRows = Array.isArray(detailPayload?.findings) ? detailPayload.findings : [];
|
||||
const expected = summarizeRows(detailRows);
|
||||
const observed = panelCounts();
|
||||
const matchesRunDetail = observed.runId === targetRunId
|
||||
&& observed.typeCount === expected.typeCount
|
||||
&& observed.errorTypeCount === expected.errorTypeCount
|
||||
&& observed.warningTypeCount === expected.warningTypeCount
|
||||
&& observed.alertTypeCount === expected.alertTypeCount
|
||||
&& observed.errorSamples === expected.errorSamples
|
||||
&& observed.warningSamples === expected.warningSamples
|
||||
&& observed.alertSamples === expected.alertSamples
|
||||
&& observed.visibleRowCount === expected.alertTypeCount
|
||||
&& observed.visibleAlertSamples === expected.alertSamples
|
||||
&& observed.selectedRunErrorTag === expected.errorTypeCount
|
||||
&& observed.selectedRunWarningTag === expected.warningTypeCount
|
||||
&& observed.selectedRunRowSelected === true;
|
||||
return {
|
||||
ok: panelReady === true && matchesRunDetail === true,
|
||||
requestedRunId,
|
||||
requestedOptionPresent: Boolean(requestedOption),
|
||||
targetRunId,
|
||||
panelReady,
|
||||
observed,
|
||||
expected,
|
||||
matchesRunDetail,
|
||||
};
|
||||
}, { expectedRoutePrefix });
|
||||
dom.runFilterProbe = runFilterProbe;
|
||||
|
||||
const consoleErrors = consoleMessages.filter((item) => item.type === "error");
|
||||
const ok = !navigationError
|
||||
&& httpStatus !== null
|
||||
&& httpStatus >= 200
|
||||
&& httpStatus < 300
|
||||
&& dom.shell === true
|
||||
&& dom.ready === true
|
||||
&& (captureScreenshot || dom.ready === true)
|
||||
&& dom.contract?.apiOverview === true
|
||||
&& dom.contract?.apiRuns === true
|
||||
&& dom.sentinelBoundary?.datasetMatches === true
|
||||
&& dom.sentinelBoundary?.overviewMatches === true
|
||||
&& dom.sentinelBoundary?.runsPayloadMatches === true
|
||||
&& dom.sentinelBoundary?.runRowsMatch === true
|
||||
&& dom.sentinelBoundary?.routePrefixMatches === true
|
||||
&& dom.errorVisible !== true
|
||||
&& dom.trendCurve === true
|
||||
&& dom.chartTiming?.ok === true
|
||||
&& dom.chartTiming?.matchesLatestRun === true
|
||||
&& dom.checkScope?.matchesLatestRun === true
|
||||
&& dom.checkScope?.matchesRunDetail === true
|
||||
&& dom.selectedRunTags?.matchesRunDetail === true
|
||||
&& dom.runFilterProbe?.ok === true
|
||||
&& dom.runFilterProbe?.requestedOptionPresent === true
|
||||
&& dom.checkScope?.belowWorkspace === true
|
||||
&& 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.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
|
||||
&& dom.badCardBodyCount === 0
|
||||
&& dom.timelineVisible === true
|
||||
&& dom.scrollModel?.independentScroll === true
|
||||
&& dom.scrollModel?.stickyHeader?.present === true
|
||||
&& dom.scrollModel?.stickyHeader?.backgroundOpaque === true
|
||||
&& dom.scrollModel?.stickyChecksHeader?.present === true
|
||||
&& dom.scrollModel?.stickyChecksHeader?.backgroundOpaque === true
|
||||
&& dom.panelHeights?.workspaceOk === true
|
||||
&& dom.panelHeights?.checksOk === true
|
||||
&& dom.layout?.horizontalOverflow !== true
|
||||
&& pageErrors.length === 0;
|
||||
|
||||
@@ -1155,7 +762,12 @@ function compactDashboardArtifact(artifact: Record<string, unknown>): Record<str
|
||||
|
||||
function dashboardDegradedReason(result: CommandResult, transport: Record<string, unknown>, page: Record<string, unknown> | null, screenshotOk: boolean): string {
|
||||
if (result.timedOut) return "sentinel-dashboard-command-timeout";
|
||||
if (transport.ok !== true) return "sentinel-dashboard-transport-failed";
|
||||
if (transport.ok !== true) {
|
||||
const pollFailure = record(transport.pollFailure);
|
||||
if (pollFailure.phase === "timeout") return "sentinel-dashboard-transport-timeout";
|
||||
if (Object.keys(pollFailure).length > 0) return "sentinel-dashboard-transport-failed";
|
||||
return "sentinel-dashboard-transport-failed";
|
||||
}
|
||||
if (page === null) return "sentinel-dashboard-browser-output-missing";
|
||||
if (page.ok !== true) return "sentinel-dashboard-render-failed";
|
||||
if (!screenshotOk) return "sentinel-dashboard-screenshot-missing";
|
||||
@@ -1438,11 +1050,11 @@ function probeSentinelPublicDashboard(state: SentinelCicdState, timeoutSeconds:
|
||||
"const [rootUrl,cssUrl,jsUrl,vueUrl,rootCode,rootRc,cssCode,cssRc,jsCode,jsRc,vueCode,vueRc,rootPath,cssPath,jsPath,vuePath]=process.argv.slice(2);",
|
||||
"function read(path){try{return fs.readFileSync(path,'utf8')}catch{return ''}}",
|
||||
"const root=read(rootPath); const css=read(cssPath); const js=read(jsPath); const vue=read(vuePath);",
|
||||
"const rootOk=Number(rootRc)===0&&Number(rootCode)>=200&&Number(rootCode)<300&&root.includes('id=\"monitor-web-root\"')&&root.includes('/monitor-web/assets/monitor-web.js')&&root.includes('monitor-web-bootstrap');",
|
||||
"const cssOk=Number(cssRc)===0&&Number(cssCode)>=200&&Number(cssCode)<300&&css.includes('monitor-shell')&&css.includes('workspace-grid')&&css.includes('trend-stage')&&css.length>1000;",
|
||||
"const jsOk=Number(jsRc)===0&&Number(jsCode)>=200&&Number(jsCode)<300&&js.includes('createApp')&&js.includes('/api/overview')&&js.includes('data-monitor-trend-curve')&&js.length>1000;",
|
||||
"const vueOk=Number(vueRc)===0&&Number(vueCode)>=200&&Number(vueCode)<300&&vue.includes('createApp')&&vue.length>80000;",
|
||||
"console.log(JSON.stringify({ok:rootOk&&cssOk&&jsOk&&vueOk,root:{url:rootUrl,httpStatus:Number(rootCode),bytes:Buffer.byteLength(root),shell:root.includes('id=\"monitor-web-root\"'),contract:root.includes('draft-2026-06-27-p11-monitor-web-observability-dashboard')},css:{url:cssUrl,httpStatus:Number(cssCode),bytes:Buffer.byteLength(css),workspaceGrid:css.includes('workspace-grid'),trendStage:css.includes('trend-stage')},js:{url:jsUrl,httpStatus:Number(jsCode),bytes:Buffer.byteLength(js),vueApp:js.includes('createApp'),apiClient:js.includes('/api/overview'),trend:js.includes('data-monitor-trend-curve')},vue:{url:vueUrl,httpStatus:Number(vueCode),bytes:Buffer.byteLength(vue),runtime:vue.includes('createApp')},valuesRedacted:true}));",
|
||||
"const rootOk=Number(rootRc)===0&&Number(rootCode)>=200&&Number(rootCode)<300&&root.includes('id=\"monitor-web-root\"')&&root.includes('/monitor-web/assets/monitor-web.js');",
|
||||
"const cssOk=Number(cssRc)===0&&Number(cssCode)>=200&&Number(cssCode)<300&&css.length>1000;",
|
||||
"const jsOk=Number(jsRc)===0&&Number(jsCode)>=200&&Number(jsCode)<300&&js.length>1000;",
|
||||
"const vueOk=Number(vueRc)===0&&Number(vueCode)>=200&&Number(vueCode)<300&&vue.length>80000;",
|
||||
"console.log(JSON.stringify({ok:rootOk&&cssOk&&jsOk&&vueOk,root:{url:rootUrl,httpStatus:Number(rootCode),bytes:Buffer.byteLength(root),shell:root.includes('id=\"monitor-web-root\"'),contract:root.includes('draft-2026-06-27-p11-monitor-web-observability-dashboard')},css:{url:cssUrl,httpStatus:Number(cssCode),bytes:Buffer.byteLength(css)},js:{url:jsUrl,httpStatus:Number(jsCode),bytes:Buffer.byteLength(js)},vue:{url:vueUrl,httpStatus:Number(vueCode),bytes:Buffer.byteLength(vue)},valuesRedacted:true}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
const result = runCommand(["bash", "-lc", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 30) * 1000 });
|
||||
@@ -1529,120 +1141,51 @@ function renderDashboardResult(result: Record<string, unknown>): string {
|
||||
const page = record(result.page);
|
||||
const dom = record(page.dom);
|
||||
const dataset = record(dom.dataset);
|
||||
const contract = record(dom.contract);
|
||||
const sentinelBoundary = record(dom.sentinelBoundary);
|
||||
const api = record(dom.api);
|
||||
const apiOverview = record(api.overview);
|
||||
const apiRuns = record(api.runs);
|
||||
const layout = record(dom.layout);
|
||||
const chartTiming = record(dom.chartTiming);
|
||||
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);
|
||||
const checkDialog = record(dom.checkDialog);
|
||||
const runFilterProbe = record(dom.runFilterProbe);
|
||||
const runFilterObserved = record(runFilterProbe.observed);
|
||||
const runFilterExpected = record(runFilterProbe.expected);
|
||||
const overviewSamples = record(dom.overviewSamples);
|
||||
const panelHeights = record(dom.panelHeights);
|
||||
const workspaceHeight = record(panelHeights.workspace);
|
||||
const checksHeight = record(panelHeights.checks);
|
||||
const screenshot = record(result.screenshot);
|
||||
const remote = record(result.remote);
|
||||
const transport = record(result.transport);
|
||||
const pollFailure = record(transport.pollFailure);
|
||||
const degradedReason = result.degradedReason ?? null;
|
||||
return [
|
||||
String(result.command),
|
||||
"",
|
||||
table(["NODE", "LANE", "SENTINEL", "STATUS", "URL"], [[result.node, result.lane, result.sentinelId, result.ok === true ? "pass" : "blocked", result.publicUrl]]),
|
||||
"",
|
||||
table(["HTTP", "SHELL", "RUN_ROWS", "CHECK_ROWS", "TABS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[
|
||||
table(["HTTP", "SHELL", "READY", "API_OVERVIEW", "API_RUNS", "RUN_ROWS", "ERRORS", "CONSOLE_ERR", "REQ_FAIL"], [[
|
||||
page.httpStatus ?? "-",
|
||||
dom.shell,
|
||||
dom.ready,
|
||||
`${apiOverview.ok ?? "-"}/${apiOverview.httpStatus ?? "-"}`,
|
||||
`${apiRuns.ok ?? "-"}/${apiRuns.httpStatus ?? "-"}`,
|
||||
dom.runRows,
|
||||
dom.checkRows,
|
||||
dom.detailTabs,
|
||||
page.pageErrorCount,
|
||||
page.consoleErrorCount,
|
||||
page.requestFailureCount,
|
||||
]]),
|
||||
"",
|
||||
table(["TITLE", "STATUS_TEXT", "CONTRACT", "BASE_PATH"], [[dom.title, dom.statusText, dataset.contractVersion, dataset.basePath ?? "-"]]),
|
||||
"",
|
||||
table(["TREND_LATEST_MIN", "TREND_MAX_MIN", "TREND_DURATION", "MATCH_LATEST", "BAD_TITLE", "BAD_BODY"], [[
|
||||
chartTiming.latestMinutes ?? "-",
|
||||
chartTiming.maxMinutes ?? "-",
|
||||
chartTiming.ok ?? "-",
|
||||
chartTiming.matchesLatestRun ?? "-",
|
||||
dom.badCardTitleCount ?? "-",
|
||||
dom.badCardBodyCount ?? "-",
|
||||
table(["CONTRACT", "BASE_PATH", "DATASET_SENTINEL", "OVERVIEW_SENTINEL", "RUNS_SENTINEL", "ROUTE_MATCH"], [[
|
||||
dataset.contractVersion ?? "-",
|
||||
dataset.basePath ?? "-",
|
||||
sentinelBoundary.datasetSentinelId ?? "-",
|
||||
sentinelBoundary.overviewSentinelId ?? "-",
|
||||
sentinelBoundary.runsSentinelId ?? "-",
|
||||
sentinelBoundary.routePrefixMatches ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["LATEST_RUN", "TYPE_COUNT", "ERR_TYPES", "ALERT_TYPES", "TOTAL_TYPES", "SAMPLE_TOTAL", "HIST_ERR", "HIST_ALERT"], [[
|
||||
table(["HTML_SHELL", "APP_READY", "API_OVERVIEW_OK", "API_RUNS_OK", "LATEST_RUN", "TYPE_COUNT"], [[
|
||||
contract.htmlShell ?? "-",
|
||||
contract.appReady ?? "-",
|
||||
contract.apiOverview ?? "-",
|
||||
contract.apiRuns ?? "-",
|
||||
latestRunCounts.runId ?? "-",
|
||||
latestRunCounts.typeCount ?? "-",
|
||||
latestRunCounts.error ?? "-",
|
||||
latestRunCounts.warning ?? "-",
|
||||
latestRunCounts.total ?? "-",
|
||||
latestRunCounts.alertSamples ?? "-",
|
||||
overviewSamples.error ?? "-",
|
||||
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 ?? "-",
|
||||
`${checkScope.typeCount ?? "-"}/${latestDetailSummary.typeCount ?? "-"}`,
|
||||
`${checkScope.errorTypeCount ?? "-"}/${latestDetailSummary.errorTypeCount ?? "-"}`,
|
||||
`${checkScope.alertTypeCount ?? "-"}/${latestDetailSummary.alertTypeCount ?? "-"}`,
|
||||
checkScope.errorSamples ?? "-",
|
||||
checkScope.alertSamples ?? "-",
|
||||
checkScope.matchesLatestRun ?? "-",
|
||||
checkScope.matchesRunDetail ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["CHECK_VISIBLE_ROWS", "EXPECT_ALERT_ROWS", "CHECK_VISIBLE_ALERT", "RUN_TAG_ERR", "RUN_TAG_ALERT", "RUN_TAG_MATCH", "BELOW_WORKSPACE", "FULL_WIDTH"], [[
|
||||
checkScope.visibleRowCount ?? "-",
|
||||
checkScope.expectsAlertRows ?? "-",
|
||||
checkScope.visibleAlertSamples ?? "-",
|
||||
selectedRunTags.error ?? "-",
|
||||
selectedRunTags.warning ?? "-",
|
||||
selectedRunTags.matchesRunDetail ?? "-",
|
||||
checkScope.belowWorkspace ?? "-",
|
||||
checkScope.fullWidth ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["TREND_PANEL_SLACK", "TREND_PANEL_COMPACT", "DETAIL_DIALOG", "DIALOG_LARGE"], [[
|
||||
trendPanelCompact.bottomSlackPx ?? "-",
|
||||
trendPanelCompact.ok ?? "-",
|
||||
checkDialog.opened ?? "-",
|
||||
checkDialog.large ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["WORKSPACE_H", "WORKSPACE_RATIO", "WORKSPACE_80", "CHECKS_H", "CHECKS_RATIO", "CHECKS_80", "PANES_80"], [[
|
||||
`${workspaceHeight.heightPx ?? "-"}/${workspaceHeight.targetPx ?? "-"}`,
|
||||
workspaceHeight.ratio ?? "-",
|
||||
panelHeights.workspaceOk ?? "-",
|
||||
`${checksHeight.heightPx ?? "-"}/${checksHeight.targetPx ?? "-"}`,
|
||||
checksHeight.ratio ?? "-",
|
||||
panelHeights.checksOk ?? "-",
|
||||
panelHeights.workspacePaneBounded ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["FILTER_RUN", "FILTER_OPTION", "FILTER_TYPES", "FILTER_ERR_TYPES", "FILTER_ALERT_TYPES", "FILTER_SAMPLE_ERR", "FILTER_SAMPLE_ALERT", "FILTER_MATCH_DETAIL"], [[
|
||||
runFilterProbe.targetRunId ?? "-",
|
||||
runFilterProbe.requestedOptionPresent ?? "-",
|
||||
`${runFilterObserved.typeCount ?? "-"}/${runFilterExpected.typeCount ?? "-"}`,
|
||||
`${runFilterObserved.errorTypeCount ?? "-"}/${runFilterExpected.errorTypeCount ?? "-"}`,
|
||||
`${runFilterObserved.alertTypeCount ?? "-"}/${runFilterExpected.alertTypeCount ?? "-"}`,
|
||||
runFilterObserved.errorSamples ?? "-",
|
||||
runFilterObserved.alertSamples ?? "-",
|
||||
runFilterProbe.matchesRunDetail ?? "-",
|
||||
]]),
|
||||
"",
|
||||
table(["VIEWPORT", "DOC", "H_OVERFLOW", "OVERFLOW_COUNT"], [[
|
||||
@@ -1656,14 +1199,14 @@ function renderDashboardResult(result: Record<string, unknown>): string {
|
||||
? "SCREENSHOT\n-"
|
||||
: table(["LOCAL_PATH", "BYTES", "SHA256", "VERIFIED"], [[screenshot.localPath, screenshot.bytes, short(screenshot.sha256), screenshot.verified]]),
|
||||
"",
|
||||
degradedReason === null ? "BLOCKER\n-" : table(["CODE", "REMOTE_EXIT", "TRANSPORT"], [[degradedReason, remote.exitCode, transport.ok]]),
|
||||
degradedReason === null ? "BLOCKER\n-" : table(["CODE", "REMOTE_EXIT", "TRANSPORT", "POLL_PHASE"], [[degradedReason, remote.exitCode, transport.ok, pollFailure.phase ?? "-"]]),
|
||||
"",
|
||||
"NEXT",
|
||||
` screenshot: bun scripts/cli.ts web-probe sentinel dashboard screenshot --node ${result.node} --lane ${result.lane} --sentinel ${result.sentinelId}`,
|
||||
` validate: bun scripts/cli.ts web-probe sentinel validate --node ${result.node} --lane ${result.lane} --sentinel ${result.sentinelId}`,
|
||||
"",
|
||||
"DISCLOSURE",
|
||||
" dashboard verify uses the YAML publicExposure URL and remote browser execution; it does not start a sentinel run or inspect provider payloads.",
|
||||
" dashboard verify uses YAML publicExposure, remote browser execution and API/data-contract checks; it does not assert UI copy or CSS class names.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO
|
||||
if (cleanup.result !== null) commandResults.push(cleanup.result);
|
||||
const manifestExitCode = manifest?.exitCode ?? null;
|
||||
const ok = pollFailure === null && manifest !== null && manifest.exitCode === 0 && downloadFailure === null;
|
||||
const exitCode = ok ? 0 : manifestExitCode ?? (pollFailure !== null ? 1 : submit.exitCode ?? 1);
|
||||
const transport = {
|
||||
ok,
|
||||
command: "web-probe remote-artifact",
|
||||
@@ -168,7 +169,7 @@ export function runWebProbeRemoteArtifactJob(options: WebProbeRemoteArtifactJobO
|
||||
return {
|
||||
result: syntheticCommandResult(
|
||||
[transPath(), options.route, "web-probe-remote-artifact"],
|
||||
ok ? 0 : manifestExitCode ?? submit.exitCode ?? 1,
|
||||
exitCode,
|
||||
JSON.stringify(transport, null, 2),
|
||||
commandResults,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user