diff --git a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js index a6d3e8e5..d3c3d0ee 100644 --- a/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js +++ b/scripts/assets/web-probe-sentinel-monitor-web/monitor-web.js @@ -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}`); diff --git a/scripts/src/hwlab-node-web-sentinel-p5.ts b/scripts/src/hwlab-node-web-sentinel-p5.ts index 61823134..71f769dc 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5.ts @@ -415,11 +415,12 @@ export function runSentinelDashboard(state: SentinelCicdState, options: Extract< export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: Extract): Record { 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) })); diff --git a/scripts/verify-web-probe-sentinel-monitor-web.ts b/scripts/verify-web-probe-sentinel-monitor-web.ts index 46abe861..aef86a64 100644 --- a/scripts/verify-web-probe-sentinel-monitor-web.ts +++ b/scripts/verify-web-probe-sentinel-monitor-web.ts @@ -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", ], },