fix: add monitor run detail deep links

This commit is contained in:
Codex
2026-07-01 02:57:19 +00:00
parent fc733ca3b6
commit 0fdd05a90a
3 changed files with 88 additions and 13 deletions
@@ -1,6 +1,6 @@
// 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, watch } from "./vendor/vue.esm-browser.prod.js";
import { createApp, computed, nextTick, onMounted, ref, watch } from "./vendor/vue.esm-browser.prod.js";
const bootstrap = readBootstrap();
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;
@@ -16,6 +16,7 @@ const memoryChartFrame = Object.freeze({
createApp({
setup() {
const initialDeepLink = readMonitorDeepLink();
const loading = ref(true);
const error = ref("");
const overview = ref(null);
@@ -36,6 +37,9 @@ createApp({
const refreshSeconds = ref(30);
const lastLoadedAt = ref("");
const hoveredTrendDot = ref(null);
const deepLinkRunId = ref(initialDeepLink.runId);
const deepLinkFocus = ref(initialDeepLink.focus);
let pendingInitialFocus = initialDeepLink.focus;
let lastAutoRefreshAt = 0;
const sentinels = computed(() => {
@@ -54,7 +58,7 @@ createApp({
return [run.id, run.scenarioId, run.status, run.observerId].some((item) => String(item || "").toLowerCase().includes(needle));
});
});
const selectedRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || latestRun.value);
const selectedRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || (selectedDetail.value?.run?.id === selectedRunId.value ? selectedDetail.value.run : null) || latestRun.value);
const trendRows = computed(() => runs.value.slice().sort((a, b) => Date.parse(a.updatedAt || a.createdAt || "") - Date.parse(b.updatedAt || b.createdAt || "")).slice(-48));
const latestTrendRun = computed(() => trendRows.value.length > 0 ? trendRows.value[trendRows.value.length - 1] : latestRun.value);
const trendDurationMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendDurationMinutes(run))));
@@ -201,9 +205,12 @@ createApp({
await refreshRunCheckSummaries(runs.value);
lastLoadedAt.value = new Date().toISOString();
lastAutoRefreshAt = Date.now();
const deepLinkedRun = deepLinkRunId.value ? runs.value.find((run) => run.id === deepLinkRunId.value) : null;
const keepSelected = runs.value.find((run) => run.id === selectedRunId.value);
const nextRun = keepSelected || runs.value[0] || latestRun.value;
if (nextRun?.id) await selectRun(nextRun, true);
const nextRun = deepLinkedRun || keepSelected || runs.value[0] || latestRun.value;
const nextRunRef = deepLinkRunId.value && !deepLinkedRun && !keepSelected ? deepLinkRunId.value : nextRun;
if ((typeof nextRunRef === "string" && nextRunRef) || nextRunRef?.id) await selectRun(nextRunRef, true, { focus: pendingInitialFocus });
pendingInitialFocus = "";
} catch (cause) {
const message = String(cause?.message || cause);
if (!options.silent || runs.value.length === 0) error.value = message;
@@ -214,17 +221,22 @@ createApp({
}
}
async function selectRun(run, silent = false) {
async function selectRun(run, silent = false, options = {}) {
const runId = typeof run === "string" ? run : run?.id;
if (!runId) return;
selectedRunId.value = runId;
checkRunId.value = runId;
deepLinkRunId.value = runId;
const nextFocus = normalizeDeepLinkFocus(options.focus || deepLinkFocus.value || "");
deepLinkFocus.value = nextFocus;
syncMonitorDeepLink(runId, nextFocus);
if (!silent) selectedDetail.value = null;
try {
selectedDetail.value = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`);
} catch (cause) {
selectedDetail.value = { ok: false, error: String(cause?.message || cause), runId };
}
await focusDeepLinkedPanel(nextFocus);
}
function refreshNow() {
@@ -236,6 +248,17 @@ createApp({
if (run) void selectRun(run);
}
async function focusDeepLinkedPanel(focus) {
if (!isMemoryFocus(focus)) return;
await nextTick();
const card = document.querySelector("[data-run-memory-chart='true']");
if (card instanceof HTMLElement) {
card.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
card.setAttribute("data-deep-link-focused", "memory");
document.querySelector("#monitor-web-root")?.setAttribute("data-monitor-deep-link-focus", "memory");
}
}
async function refreshRunCheckSummaries(rows) {
const targets = Array.isArray(rows) ? rows.slice(0, 48) : [];
const next = { ...runCheckSummaries.value };
@@ -413,6 +436,8 @@ createApp({
refreshSeconds,
lastLoadedAt,
hoveredTrendDot,
deepLinkRunId,
deepLinkFocus,
sentinels,
currentStatus,
latestRun,
@@ -972,6 +997,33 @@ function readBootstrap() {
};
}
function readMonitorDeepLink() {
const params = new URLSearchParams(window.location.search || "");
const runId = String(params.get("run") || params.get("runId") || "").trim();
const focus = normalizeDeepLinkFocus(params.get("focus") || params.get("panel") || window.location.hash.replace(/^#/u, ""));
return { runId, focus };
}
function syncMonitorDeepLink(runId, focus) {
if (!window.history?.replaceState) return;
const nextFocus = normalizeDeepLinkFocus(focus);
const url = new URL(window.location.href);
url.searchParams.set("run", runId);
if (nextFocus) url.searchParams.set("focus", nextFocus);
else url.searchParams.delete("focus");
window.history.replaceState(null, "", url.toString());
}
function normalizeDeepLinkFocus(value) {
const text = String(value || "").trim().toLowerCase();
if (["memory", "memory-chart", "run-memory", "page-memory"].includes(text)) return "memory";
return "";
}
function isMemoryFocus(value) {
return normalizeDeepLinkFocus(value) === "memory";
}
async function fetchJson(path) {
const response = await fetch(apiUrl(path), { cache: "no-store" });
if (!response.ok) throw new Error(`${path} HTTP ${response.status}`);
+29 -8
View File
@@ -415,11 +415,12 @@ export function runSentinelDashboard(state: SentinelCicdState, options: Extract<
export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): Record<string, unknown> {
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
const dashboardUrl = sentinelDashboardUrl(publicBaseUrl, options.runId);
const [widthRaw, heightRaw] = options.viewport.split("x");
const screenshotName = options.action === "screenshot" ? dashboardScreenshotName(options, state) : "";
const script = [
"set -eu",
`export UNIDESK_SENTINEL_DASHBOARD_URL=${shellQuote(`${publicBaseUrl}/`)}`,
`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_WIDTH=${shellQuote(widthRaw ?? "1440")}`,
@@ -499,6 +500,15 @@ export function probeSentinelDashboardBrowser(state: SentinelCicdState, options:
};
}
function sentinelDashboardUrl(publicBaseUrl: string, runId: string | null): string {
const url = new URL(`${publicBaseUrl.replace(/\/$/u, "")}/`);
if (runId !== null && runId.length > 0) {
url.searchParams.set("run", runId);
url.searchParams.set("focus", "memory");
}
return url.toString();
}
function sentinelDashboardBrowserModule(): string {
return String.raw`import { pathToFileURL } from "node:url";
@@ -574,17 +584,28 @@ if (requestedRunId) {
const wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
const rowForRun = () => document.querySelector('.run-list .run-row[data-run-id="' + CSS.escape(runId) + '"]');
const row = rowForRun();
if (!(row instanceof HTMLElement)) return { requestedRunId: runId, ok: false, reason: "run-row-missing" };
row.click();
for (let index = 0; index < 40; index += 1) {
if (row instanceof HTMLElement) row.click();
for (let index = 0; index < 80; index += 1) {
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
const selected = rowForRun();
const pageCount = Number(memoryChart?.getAttribute("data-memory-page-count") || 0);
const curveCount = document.querySelectorAll(".memory-chart .memory-line").length;
const axesOk = Boolean(memoryChart?.querySelector("[data-memory-axis-x='true']"))
&& Boolean(memoryChart?.querySelector("[data-memory-axis-y='true']"))
&& document.querySelectorAll(".memory-chart [data-memory-axis-x-tick='true']").length >= 3
&& document.querySelectorAll(".memory-chart [data-memory-axis-y-tick='true']").length >= 3;
const pageLinesOk = pageCount > 0 && curveCount === pageCount;
if (memoryChart?.getAttribute("data-memory-run-id") === runId
&& selected instanceof HTMLElement
&& selected.classList.contains("selected")) {
return { requestedRunId: runId, ok: true, reason: "selected" };
&& (!(selected instanceof HTMLElement) || selected.classList.contains("selected"))
&& axesOk
&& pageLinesOk) {
if (memoryChart instanceof HTMLElement) {
memoryChart.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
await wait(250);
}
return { requestedRunId: runId, ok: true, reason: "selected-memory-ready" };
}
await wait(100);
await wait(150);
}
return { requestedRunId: runId, ok: false, reason: "detail-switch-timeout" };
}, requestedRunId).catch((error) => ({ requestedRunId, ok: false, reason: "selection-error", error: String(error?.message || error).slice(0, 300) }));
@@ -14,6 +14,8 @@ const checks: Array<{ readonly path: string; readonly contains: readonly string[
"data-check-dialog",
"data-memory-axis-x",
"data-memory-axis-y",
"readMonitorDeepLink",
"data-monitor-deep-link-focus",
"rootCause",
],
},