fix: add monitor run detail deep links
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p12-cadence-scheduler-monitor-web.
|
// 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.
|
// 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 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;
|
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({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
|
const initialDeepLink = readMonitorDeepLink();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
const overview = ref(null);
|
const overview = ref(null);
|
||||||
@@ -36,6 +37,9 @@ createApp({
|
|||||||
const refreshSeconds = ref(30);
|
const refreshSeconds = ref(30);
|
||||||
const lastLoadedAt = ref("");
|
const lastLoadedAt = ref("");
|
||||||
const hoveredTrendDot = ref(null);
|
const hoveredTrendDot = ref(null);
|
||||||
|
const deepLinkRunId = ref(initialDeepLink.runId);
|
||||||
|
const deepLinkFocus = ref(initialDeepLink.focus);
|
||||||
|
let pendingInitialFocus = initialDeepLink.focus;
|
||||||
let lastAutoRefreshAt = 0;
|
let lastAutoRefreshAt = 0;
|
||||||
|
|
||||||
const sentinels = computed(() => {
|
const sentinels = computed(() => {
|
||||||
@@ -54,7 +58,7 @@ createApp({
|
|||||||
return [run.id, run.scenarioId, run.status, run.observerId].some((item) => String(item || "").toLowerCase().includes(needle));
|
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 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 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))));
|
const trendDurationMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendDurationMinutes(run))));
|
||||||
@@ -201,9 +205,12 @@ createApp({
|
|||||||
await refreshRunCheckSummaries(runs.value);
|
await refreshRunCheckSummaries(runs.value);
|
||||||
lastLoadedAt.value = new Date().toISOString();
|
lastLoadedAt.value = new Date().toISOString();
|
||||||
lastAutoRefreshAt = Date.now();
|
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 keepSelected = runs.value.find((run) => run.id === selectedRunId.value);
|
||||||
const nextRun = keepSelected || runs.value[0] || latestRun.value;
|
const nextRun = deepLinkedRun || keepSelected || runs.value[0] || latestRun.value;
|
||||||
if (nextRun?.id) await selectRun(nextRun, true);
|
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) {
|
} catch (cause) {
|
||||||
const message = String(cause?.message || cause);
|
const message = String(cause?.message || cause);
|
||||||
if (!options.silent || runs.value.length === 0) error.value = message;
|
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;
|
const runId = typeof run === "string" ? run : run?.id;
|
||||||
if (!runId) return;
|
if (!runId) return;
|
||||||
selectedRunId.value = runId;
|
selectedRunId.value = runId;
|
||||||
checkRunId.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;
|
if (!silent) selectedDetail.value = null;
|
||||||
try {
|
try {
|
||||||
selectedDetail.value = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`);
|
selectedDetail.value = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`);
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
selectedDetail.value = { ok: false, error: String(cause?.message || cause), runId };
|
selectedDetail.value = { ok: false, error: String(cause?.message || cause), runId };
|
||||||
}
|
}
|
||||||
|
await focusDeepLinkedPanel(nextFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshNow() {
|
function refreshNow() {
|
||||||
@@ -236,6 +248,17 @@ createApp({
|
|||||||
if (run) void selectRun(run);
|
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) {
|
async function refreshRunCheckSummaries(rows) {
|
||||||
const targets = Array.isArray(rows) ? rows.slice(0, 48) : [];
|
const targets = Array.isArray(rows) ? rows.slice(0, 48) : [];
|
||||||
const next = { ...runCheckSummaries.value };
|
const next = { ...runCheckSummaries.value };
|
||||||
@@ -413,6 +436,8 @@ createApp({
|
|||||||
refreshSeconds,
|
refreshSeconds,
|
||||||
lastLoadedAt,
|
lastLoadedAt,
|
||||||
hoveredTrendDot,
|
hoveredTrendDot,
|
||||||
|
deepLinkRunId,
|
||||||
|
deepLinkFocus,
|
||||||
sentinels,
|
sentinels,
|
||||||
currentStatus,
|
currentStatus,
|
||||||
latestRun,
|
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) {
|
async function fetchJson(path) {
|
||||||
const response = await fetch(apiUrl(path), { cache: "no-store" });
|
const response = await fetch(apiUrl(path), { cache: "no-store" });
|
||||||
if (!response.ok) throw new Error(`${path} HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`${path} HTTP ${response.status}`);
|
||||||
|
|||||||
@@ -415,11 +415,12 @@ export function runSentinelDashboard(state: SentinelCicdState, options: Extract<
|
|||||||
|
|
||||||
export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): Record<string, unknown> {
|
export function probeSentinelDashboardBrowser(state: SentinelCicdState, options: Extract<WebProbeSentinelOptions, { kind: "dashboard" }>): Record<string, unknown> {
|
||||||
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
|
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
|
||||||
|
const dashboardUrl = sentinelDashboardUrl(publicBaseUrl, options.runId);
|
||||||
const [widthRaw, heightRaw] = options.viewport.split("x");
|
const [widthRaw, heightRaw] = options.viewport.split("x");
|
||||||
const screenshotName = options.action === "screenshot" ? dashboardScreenshotName(options, state) : "";
|
const screenshotName = options.action === "screenshot" ? dashboardScreenshotName(options, state) : "";
|
||||||
const script = [
|
const script = [
|
||||||
"set -eu",
|
"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_SCREENSHOT="$UNIDESK_WEB_PROBE_ARTIFACT_REMOTE_DIR"/${shellQuote(screenshotName)}`,
|
||||||
`export UNIDESK_SENTINEL_DASHBOARD_CAPTURE=${shellQuote(options.action === "screenshot" ? "1" : "0")}`,
|
`export UNIDESK_SENTINEL_DASHBOARD_CAPTURE=${shellQuote(options.action === "screenshot" ? "1" : "0")}`,
|
||||||
`export UNIDESK_SENTINEL_DASHBOARD_WIDTH=${shellQuote(widthRaw ?? "1440")}`,
|
`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 {
|
function sentinelDashboardBrowserModule(): string {
|
||||||
return String.raw`import { pathToFileURL } from "node:url";
|
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 wait = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||||
const rowForRun = () => document.querySelector('.run-list .run-row[data-run-id="' + CSS.escape(runId) + '"]');
|
const rowForRun = () => document.querySelector('.run-list .run-row[data-run-id="' + CSS.escape(runId) + '"]');
|
||||||
const row = rowForRun();
|
const row = rowForRun();
|
||||||
if (!(row instanceof HTMLElement)) return { requestedRunId: runId, ok: false, reason: "run-row-missing" };
|
if (row instanceof HTMLElement) row.click();
|
||||||
row.click();
|
for (let index = 0; index < 80; index += 1) {
|
||||||
for (let index = 0; index < 40; index += 1) {
|
|
||||||
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
|
const memoryChart = document.querySelector("[data-run-memory-chart='true']");
|
||||||
const selected = rowForRun();
|
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
|
if (memoryChart?.getAttribute("data-memory-run-id") === runId
|
||||||
&& selected instanceof HTMLElement
|
&& (!(selected instanceof HTMLElement) || selected.classList.contains("selected"))
|
||||||
&& selected.classList.contains("selected")) {
|
&& axesOk
|
||||||
return { requestedRunId: runId, ok: true, reason: "selected" };
|
&& 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" };
|
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) }));
|
}, 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-check-dialog",
|
||||||
"data-memory-axis-x",
|
"data-memory-axis-x",
|
||||||
"data-memory-axis-y",
|
"data-memory-axis-y",
|
||||||
|
"readMonitorDeepLink",
|
||||||
|
"data-monitor-deep-link-focus",
|
||||||
"rootCause",
|
"rootCause",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user