Merge pull request #1357 from pikasTech/fix/1356-monitor-screenshot-deeplink
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.
|
||||
// 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}`);
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user