feat(web-sentinel): add dashboard manual trigger

This commit is contained in:
Codex
2026-07-01 11:42:24 +00:00
parent a071d1e33e
commit 4830410e68
9 changed files with 464 additions and 7 deletions
+58 -2
View File
@@ -537,6 +537,7 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
`export UNIDESK_SENTINEL_DASHBOARD_URL=${shellQuote(dashboardUrl)}`,
`export UNIDESK_SENTINEL_DASHBOARD_SCREENSHOT="$UNIDESK_WEB_PROBE_ARTIFACT_REMOTE_DIR"/${shellQuote(screenshotName)}`,
`export UNIDESK_SENTINEL_DASHBOARD_CAPTURE=${shellQuote(options.action === "screenshot" ? "1" : "0")}`,
`export UNIDESK_SENTINEL_DASHBOARD_TRIGGER=${shellQuote(options.action === "trigger" ? "1" : "0")}`,
`export UNIDESK_SENTINEL_DASHBOARD_WIDTH=${shellQuote(widthRaw ?? "1440")}`,
`export UNIDESK_SENTINEL_DASHBOARD_HEIGHT=${shellQuote(heightRaw ?? "900")}`,
`export UNIDESK_SENTINEL_DASHBOARD_TIMEOUT_MS=${shellQuote(String(options.timeoutMs))}`,
@@ -633,6 +634,7 @@ const { chromium } = await import(playwrightModuleSpecifier);
const url = process.env.UNIDESK_SENTINEL_DASHBOARD_URL;
const screenshotPath = process.env.UNIDESK_SENTINEL_DASHBOARD_SCREENSHOT || "";
const captureScreenshot = process.env.UNIDESK_SENTINEL_DASHBOARD_CAPTURE === "1";
const triggerManual = process.env.UNIDESK_SENTINEL_DASHBOARD_TRIGGER === "1";
const width = Number(process.env.UNIDESK_SENTINEL_DASHBOARD_WIDTH || 1440);
const height = Number(process.env.UNIDESK_SENTINEL_DASHBOARD_HEIGHT || 900);
const timeout = Number(process.env.UNIDESK_SENTINEL_DASHBOARD_TIMEOUT_MS || 30000);
@@ -709,7 +711,32 @@ if (requestedRunId) {
await page.waitForTimeout(150);
}
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection }) => {
let manualTrigger = { requested: triggerManual, ok: !triggerManual, status: triggerManual ? "not-attempted" : "not-requested", jobName: null, statusText: "" };
if (triggerManual) {
manualTrigger = await page.evaluate(async () => {
const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
const root = document.querySelector("[data-monitor-shell='true']");
const button = document.querySelector("[data-monitor-manual-trigger='true']");
if (!(button instanceof HTMLButtonElement)) return { requested: true, ok: false, status: "button-missing", jobName: null, statusText: "" };
if (button.disabled) return { requested: true, ok: false, status: "button-disabled", jobName: button.getAttribute("data-trigger-job") || null, statusText: "" };
button.click();
for (let index = 0; index < 160; index += 1) {
const state = root?.getAttribute("data-manual-trigger-state") || button.getAttribute("data-trigger-state") || "";
const jobName = root?.getAttribute("data-manual-trigger-job") || button.getAttribute("data-trigger-job") || null;
const statusText = String(document.querySelector("[data-monitor-manual-trigger-status='true']")?.textContent || "").replace(/\s+/g, " ").trim();
if (state === "triggered" || state === "already-active" || state === "failed") {
return { requested: true, ok: state === "triggered" || state === "already-active", status: state, jobName, statusText };
}
await wait(250);
}
const finalState = root?.getAttribute("data-manual-trigger-state") || button.getAttribute("data-trigger-state") || "timeout";
const finalJob = root?.getAttribute("data-manual-trigger-job") || button.getAttribute("data-trigger-job") || null;
return { requested: true, ok: false, status: finalState || "timeout", jobName: finalJob, statusText: "timeout" };
}).catch((error) => ({ requested: true, ok: false, status: "trigger-error", jobName: null, statusText: String(error?.message || error).slice(0, 300) }));
await page.waitForTimeout(300);
}
const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, manualTrigger }) => {
const numberValue = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
const objectOrNull = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
const text = (selector) => String(document.querySelector(selector)?.textContent || "").replace(/\s+/g, " ").trim();
@@ -720,6 +747,8 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
const trendErrorCurve = document.querySelector("[data-monitor-trend-error-curve='true']");
const trendWarningCurve = document.querySelector("[data-monitor-trend-warning-curve='true']");
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
const manualTriggerButton = document.querySelector("[data-monitor-manual-trigger='true']");
const manualTriggerStatus = document.querySelector("[data-monitor-manual-trigger-status='true']");
const chartTiming = {
hasDurationCurve: Boolean(trend),
hasErrorCurve: Boolean(trendErrorCurve),
@@ -884,6 +913,16 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
requestMatched: !requestedRunId || targetRunId === requestedRunId,
},
requestedRunSelection,
manualTriggerUi: {
requested: manualTrigger.requested,
ok: manualTrigger.ok,
status: manualTrigger.status,
jobName: manualTrigger.jobName,
buttonPresent: Boolean(manualTriggerButton),
buttonDisabled: manualTriggerButton instanceof HTMLButtonElement ? manualTriggerButton.disabled : null,
buttonState: manualTriggerButton?.getAttribute("data-trigger-state") || null,
statusText: String(manualTriggerStatus?.textContent || "").replace(/\s+/g, " ").trim(),
},
chartTiming,
memorySummary,
api: {
@@ -901,7 +940,7 @@ const dom = await page.evaluate(async ({ expectedRoutePrefix, expectedSentinelId
overflow: overflow.slice(0, 2),
},
};
}, { expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection });
}, { expectedRoutePrefix, expectedSentinelId, requestedRunId, requestedRunSelection, manualTrigger });
if (captureScreenshot && screenshotPath) {
if (requestedRunId && dom.memorySummary?.present === true && dom.memorySummary?.matchesTargetRun === true) {
@@ -932,6 +971,7 @@ const ok = navigationOk
&& dom.sentinelBoundary?.routePrefixMatches === true
&& dom.errorVisible !== true
&& dom.requestedRunSelection?.ok === true
&& manualTrigger.ok === true
&& dom.chartTiming?.ok === true
&& dom.memorySummary?.contractOk === true
&& dom.layout?.horizontalOverflow !== true
@@ -946,6 +986,7 @@ console.log("__WEB_PROBE_SENTINEL_DASHBOARD_JSON__" + JSON.stringify({
executablePath: executablePath || null,
viewport: { width, height },
screenshotPath: captureScreenshot ? screenshotPath : null,
manualTrigger,
dom,
consoleCount: consoleMessages.length,
consoleErrorCount: consoleErrors.length,
@@ -1006,6 +1047,8 @@ function dashboardDegradedReason(result: CommandResult, transport: Record<string
return "sentinel-dashboard-transport-failed";
}
if (page === null) return "sentinel-dashboard-browser-output-missing";
const manualTrigger = record(page.manualTrigger);
if (manualTrigger.requested === true && manualTrigger.ok !== true) return "sentinel-dashboard-manual-trigger-failed";
if (page.ok !== true) return "sentinel-dashboard-render-failed";
if (!screenshotOk) return "sentinel-dashboard-screenshot-missing";
return "sentinel-dashboard-unknown";
@@ -1415,6 +1458,8 @@ function renderDashboardResult(result: Record<string, unknown>): string {
const chartTiming = record(dom.chartTiming);
const memorySummary = record(dom.memorySummary);
const requestedRunSelection = record(dom.requestedRunSelection);
const manualTrigger = record(page.manualTrigger);
const manualTriggerUi = record(dom.manualTriggerUi);
const screenshot = record(result.screenshot);
const remote = record(result.remote);
const transport = record(result.transport);
@@ -1464,6 +1509,16 @@ function renderDashboardResult(result: Record<string, unknown>): string {
chartTiming.hasWarningCurve ?? "-",
]]),
"",
manualTrigger.requested === true
? table(["TRIGGER", "STATUS", "JOB", "UI_STATE", "UI_TEXT"], [[
manualTrigger.ok ?? "-",
manualTrigger.status ?? "-",
manualTrigger.jobName ?? "-",
manualTriggerUi.buttonState ?? "-",
manualTriggerUi.statusText ?? "-",
]])
: "TRIGGER\n-",
"",
table(["MEMORY_CHART", "MEMORY_RUN", "MEMORY_MATCH", "MEMORY_PAGES", "MEMORY_SAMPLES", "API_PAGES", "API_SAMPLES", "CONTRACT", "STATUS", "MEMORY_SOURCE"], [[
memorySummary.present ?? "-",
memorySummary.runId ?? "-",
@@ -1491,6 +1546,7 @@ function renderDashboardResult(result: Record<string, unknown>): string {
degradedReason === null ? "BLOCKER\n-" : table(["CODE", "REMOTE_EXIT", "TRANSPORT", "POLL_PHASE"], [[degradedReason, remote.exitCode, transport.ok, pollFailure.phase ?? "-"]]),
"",
"NEXT",
` trigger: bun scripts/cli.ts web-probe sentinel dashboard trigger --node ${result.node} --lane ${result.lane} --sentinel ${result.sentinelId}`,
` 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}`,
"",