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}`);