// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p12-cadence-scheduler-monitor-web.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
// Responsibility: Vue monitor-web runtime for sentinel trend, timeline, detail and finding observability.
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;
const memoryChartFrame = Object.freeze({
  left: 88,
  right: 696,
  top: 20,
  bottom: 132,
  yLabelX: 78,
  xLabelY: 154,
  xTitleY: 172,
});

createApp({
  setup() {
    const initialDeepLink = readMonitorDeepLink();
    const loading = ref(true);
    const error = ref("");
    const manualTriggering = ref(false);
    const manualTriggerResult = ref(null);
    const manualTriggerError = ref("");
    const overview = ref(null);
    const runs = ref([]);
    const findings = ref([]);
    const runCheckSummaries = ref({});
    const selectedRunId = ref("");
    const selectedDetail = ref(null);
    const runFilter = ref("");
    const severityFilter = ref("");
    const checkScope = ref("run");
    const checkRunId = ref("");
    const checkTimeWindow = ref("24h");
    const checkSeverityFilter = ref("alert");
    const findingFilter = ref("");
    const activeCheckItem = ref(null);
    const detailCopyState = ref("");
    const autoRefresh = ref(true);
    const refreshSeconds = ref(30);
    const lastLoadedAt = ref("");
    const hoveredTrendDot = ref(null);
    const deepLinkRunId = ref(initialDeepLink.runId);
    const deepLinkFocus = ref(initialDeepLink.focus);
    let pendingInitialErrorId = initialDeepLink.errorId;
    let pendingInitialFocus = initialDeepLink.focus;
    let lastAutoRefreshAt = 0;

    const sentinels = computed(() => {
      const rows = Array.isArray(bootstrap.sentinels) ? bootstrap.sentinels : [];
      return rows.length > 0 ? rows : [{ id: bootstrap.sentinelId, enabled: true }];
    });

    const currentStatus = computed(() => String(overview.value?.status || "idle"));
    const latestRun = computed(() => overview.value?.latestRun || null);
    const severityTotals = computed(() => overview.value?.severityCounts || {});
    const filteredRuns = computed(() => {
      const needle = runFilter.value.trim().toLowerCase();
      return runs.value.filter((run) => {
        if (severityFilter.value && severityClass(run) !== severityFilter.value) return false;
        if (!needle) return true;
        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) || (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))));
    const trendErrorMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendErrorCount(run))));
    const trendWarningMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendWarningCount(run))));
    const trendMax = computed(() => Math.max(1, trendDurationMax.value, trendErrorMax.value, trendWarningMax.value));
    const trendPolylines = computed(() => ({
      duration: trendPolyline((run) => trendDurationMinutes(run)),
      red: trendPolyline((run) => trendErrorCount(run)),
      warning: trendPolyline((run) => trendWarningCount(run)),
    }));
    const trendDots = computed(() => trendRows.value.map((run, index) => {
      const red = trendErrorCount(run);
      const warning = trendWarningCount(run);
      const total = trendTotalCount(run);
      const duration = trendDurationMinutes(run);
      const x = trendX(index, trendRows.value.length);
      const durationY = trendY(duration);
      const redY = trendY(red);
      const warningY = trendY(warning);
      const rawTime = run.updatedAt || run.createdAt || "";
      const durationText = runDurationText(run);
      const configTimingText = runTimingConfigText(run);
      const tooltipY = Math.min(durationY, redY, warningY);
      return {
        id: run.id || String(index),
        runId: run.id || "",
        x,
        durationY,
        redY,
        warningY,
        duration,
        tooltipLeft: `${clamp((x / 720) * 100, 16, 84)}%`,
        tooltipTop: `${clamp(((tooltipY + 18) / 142) * 100, 24, 76)}%`,
        red,
        warning,
        total,
        status: run.status || "-",
        severity: severityClass(run),
        rawTime,
        timeLabel: formatDate(rawTime),
        absoluteTime: formatAbsoluteDate(rawTime),
        durationText,
        configTimingText,
        reportSha: shortHash(run.reportJsonSha256 || run.report_json_sha256 || run.reportSha256 || ""),
        title: `${shortId(run.id)} ${formatAbsoluteDate(rawTime)} 错误 ${red} 告警 ${warning} 合计 ${total} 运行 ${durationText} ${configTimingText}`,
      };
    }));
    const timelineRuns = computed(() => runs.value.slice(0, 16));
    const historicalCheckFindings = computed(() => {
      const rows = findings.value.filter((item) => item.checkRegistered !== false || item.rootCause || item.nextAction || ["red", "warning", "info"].includes(severityClass(item)));
      return rows.slice(0, 200);
    });
    const runDetailCheckFindings = computed(() => {
      const rows = selectedDetail.value?.findings;
      return Array.isArray(rows) ? rows : [];
    });
    const detailMemory = computed(() => selectedDetail.value?.memory && typeof selectedDetail.value.memory === "object" ? selectedDetail.value.memory : {});
    const detailRequestRate = computed(() => selectedDetail.value?.requestRate && typeof selectedDetail.value.requestRate === "object" ? selectedDetail.value.requestRate : {});
    const detailRequestRateSeries = computed(() => {
      const rate = detailRequestRate.value;
      const rows = [
        ...tagRequestRateSeries(Array.isArray(rate.totalSeries) ? rate.totalSeries : [], "total"),
        ...tagRequestRateSeries(Array.isArray(rate.pageSeries) ? rate.pageSeries : [], "page"),
        ...tagRequestRateSeries(Array.isArray(rate.apiPathSeries) ? rate.apiPathSeries : [], "apiPath"),
      ];
      return rows.map((series) => ({
        ...series,
        points: Array.isArray(series.points) ? series.points.filter((point) => Number.isFinite(Number(point?.requestPerMinute))) : [],
      })).filter((series) => series.points.length > 0);
    });
    const detailMemorySeries = computed(() => {
      const rows = Array.isArray(detailMemory.value.pageSeries) ? detailMemory.value.pageSeries : [];
      return rows.map((series) => ({
        ...series,
        points: Array.isArray(series.points) ? series.points.filter((point) => Number.isFinite(Number(point?.memoryMb))) : [],
      })).filter((series) => series.points.length > 0);
    });
    const detailMemoryMax = computed(() => Math.max(1, ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => number(point.memoryMb)))));
    const detailRequestRateMax = computed(() => Math.max(
      1,
      number(detailRequestRate.value.totalPeakPerMinute),
      number(detailRequestRate.value.pagePeakPerMinute),
      number(detailRequestRate.value.apiPathPeakPerMinute),
      ...detailRequestRateSeries.value.flatMap((series) => series.points.map((point) => number(point.requestPerMinute))),
    ));
    const detailTimelineStartMs = computed(() => {
      const values = [
        ...detailMemorySeries.value.flatMap((series) => series.points.map(pointTimeMs)),
        ...detailRequestRateSeries.value.flatMap((series) => series.points.map(pointTimeMs)),
      ].filter((value) => value !== null);
      return values.length === 0 ? null : Math.min(...values);
    });
    const detailTimelineDurationMax = computed(() => {
      const startMs = detailTimelineStartMs.value;
      const values = [
        ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => pointElapsedMinutes(point, startMs))),
        ...detailRequestRateSeries.value.flatMap((series) => series.points.map((point) => pointElapsedMinutes(point, startMs))),
      ].filter((value) => Number.isFinite(value));
      return Math.max(1, ...values);
    });
    const detailMemoryDurationMax = computed(() => detailTimelineDurationMax.value);
    const detailRequestRateYAxisTicks = computed(() => memoryTickValues(detailRequestRateMax.value).map((value) => ({
      key: `request-rate-y-${value}`,
      value,
      y: requestRateYValue(value),
      label: formatRpm(value),
    })));
    const detailRequestRatePeak = computed(() => {
      const peak = optionalNumber(detailRequestRate.value.totalPeakPerMinute);
      if (peak !== null) return peak;
      return detailRequestRateSeries.value.length > 0 ? detailRequestRateMax.value : null;
    });
    const detailMemoryYAxisTicks = computed(() => memoryTickValues(detailMemoryMax.value).map((value) => ({
      key: `memory-y-${value}`,
      value,
      y: memoryYValue(value),
      label: formatMb(value),
    })));
    const detailMemoryXAxisTicks = computed(() => memoryTickValues(detailMemoryDurationMax.value).map((value) => ({
      key: `memory-x-${value}`,
      value,
      x: memoryXValue(value),
      label: formatMinuteTick(value),
    })));
    const scopedCheckFindings = computed(() => {
      if (checkScope.value === "history") return historicalCheckFindings.value;
      return runDetailCheckFindings.value;
    });
    const scopedCheckSummary = computed(() => summarizeCheckRows(scopedCheckFindings.value));
    const visibleCheckFindings = computed(() => {
      const needle = findingFilter.value.trim().toLowerCase();
      const rows = scopedCheckFindings.value.filter((item) => checkMatchesLevel(item, checkSeverityFilter.value));
      const filtered = needle.length === 0 ? rows : rows.filter((item) => findingSearchText(item).includes(needle));
      return filtered.slice(0, 64);
    });
    const visibleCheckSummary = computed(() => summarizeCheckRows(visibleCheckFindings.value));
    const checkScopeRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || selectedRun.value || latestRun.value);
    const checkScopeText = computed(() => {
      if (checkScope.value === "history") return `历史聚合 · ${timeWindowLabel(checkTimeWindow.value)}`;
      return `运行记录 · ${checkScopeRun.value?.id || "未选择"}`;
    });
    const cadence = computed(() => {
      const apiCadence = overview.value?.cadence || {};
      const intervalMs = Number(overview.value?.scheduler?.intervalMs || 0);
      const latestAge = Number(apiCadence.latestRunAgeSeconds ?? overview.value?.freshness?.latestRunAgeSeconds ?? -1);
      const heartbeatAge = Number(apiCadence.schedulerHeartbeatAgeSeconds ?? overview.value?.freshness?.schedulerHeartbeatAgeSeconds ?? -1);
      const intervalSeconds = Number(apiCadence.expectedCadenceSeconds || 0) || (intervalMs > 0 ? Math.round(intervalMs / 1000) : 0);
      const status = String(apiCadence.status || "");
      const stale = status === "warning" || status === "blocker";
      const cronJob = apiCadence.cronJob || {};
      const observability = overview.value?.observability || {};
      return {
        intervalSeconds,
        latestAge,
        heartbeatAge,
        status,
        stale,
        blocker: status === "blocker",
        cronJob,
        observability,
        label: intervalSeconds > 0 ? `${formatDuration(intervalSeconds)} 间隔` : "未配置",
        alert: stale
          ? `最近运行 ${formatDuration(latestAge)} 前；状态 ${status || "warning"}，阈值来自 YAML。`
          : "运行新鲜度在 YAML 窗口内",
      };
    });
    const healthChecks = computed(() => {
      const checks = overview.value?.health?.checks || {};
      return Object.entries(checks).map(([key, value]) => ({
        key,
        ok: value?.ok !== false,
        text: `${key} ${value?.ok === false ? "异常" : "ok"}`,
      }));
    });
    const manualTriggerState = computed(() => {
      if (manualTriggering.value) return "triggering";
      if (manualTriggerError.value) return "failed";
      return String(manualTriggerResult.value?.status || "idle");
    });
    const manualTriggerJobName = computed(() => String(manualTriggerResult.value?.jobName || ""));
    const manualTriggerStatusText = computed(() => {
      if (manualTriggering.value) return "触发中";
      if (manualTriggerError.value) return manualTriggerError.value;
      const result = manualTriggerResult.value || {};
      if (result.status === "triggered") return `已触发 ${shortId(result.jobName)}`;
      if (result.status === "already-active") return `运行中 ${shortId(result.jobName)}`;
      return "";
    });

    async function loadAll(options = {}) {
      if (!options.silent) loading.value = true;
      error.value = "";
      setReady(false);
      try {
        const overviewPayload = await fetchJson("/api/overview");
        const [runsResult, findingsResult] = await Promise.allSettled([
          fetchJson("/api/runs?limit=30&sort=updated"),
          fetchJson(findingsApiPath(checkTimeWindow.value)),
        ]);
        overview.value = overviewPayload;
        if (runsResult.status === "fulfilled") {
          const runsPayload = runsResult.value;
          runs.value = Array.isArray(runsPayload.runs) ? runsPayload.runs : Array.isArray(runsPayload.items) ? runsPayload.items : [];
        } else if (runs.value.length === 0) {
          throw runsResult.reason;
        }
        if (findingsResult.status === "fulfilled") {
          const findingsPayload = findingsResult.value;
          findings.value = Array.isArray(findingsPayload.findings) ? findingsPayload.findings : Array.isArray(findingsPayload.groups) ? findingsPayload.groups : [];
        } else {
          console.warn("monitor-web findings refresh failed", findingsResult.reason);
        }
        void refreshRunCheckSummaries(runs.value);
        lastLoadedAt.value = new Date().toISOString();
        lastAutoRefreshAt = Date.now();
        if (pendingInitialErrorId) {
          const errorId = pendingInitialErrorId;
          pendingInitialErrorId = "";
          await openErrorById(errorId, { initial: true });
          pendingInitialFocus = "";
          return;
        }
        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 = 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;
        else console.warn("monitor-web silent refresh failed", message);
      } finally {
        loading.value = false;
        setReady(true);
      }
    }

    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, options.errorId || "");
      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() {
      void loadAll();
    }

    async function triggerQuickVerify() {
      if (manualTriggering.value) return;
      manualTriggering.value = true;
      manualTriggerError.value = "";
      manualTriggerResult.value = null;
      try {
        const payload = await postJson("/api/quick-verify/trigger", {
          source: "monitor-web",
          reason: "manual-web-ui",
          valuesRedacted: true,
        });
        manualTriggerResult.value = payload;
        autoRefresh.value = true;
        await loadAll({ silent: true });
      } catch (cause) {
        manualTriggerError.value = String(cause?.message || cause).slice(0, 160);
      } finally {
        manualTriggering.value = false;
      }
    }

    function selectCheckRun() {
      const run = runs.value.find((item) => item.id === checkRunId.value);
      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 };
      const results = await Promise.allSettled(targets.map(async (run) => {
        const runId = run?.id || run?.runId;
        if (!runId) return null;
        const payload = await fetchJson(`/api/runs/${encodeURIComponent(runId)}`);
        const detailRows = Array.isArray(payload.findings) ? payload.findings : [];
        return [runId, summarizeCheckRows(detailRows)];
      }));
      const failures = results.filter((item) => item.status === "rejected");
      if (failures.length > 0) console.warn("monitor-web run check summaries partial", failures.length);
      for (const result of results) {
        if (result.status !== "fulfilled" || result.value === null) continue;
        const [runId, summary] = result.value;
        next[runId] = summary;
      }
      runCheckSummaries.value = next;
    }

    function trendSummary(run) {
      const runId = run?.id || run?.runId || "";
      return runId ? runCheckSummaries.value[runId] || emptyCheckSummary() : emptyCheckSummary();
    }

    function runCheckErrorCount(run) {
      return trendSummary(run).errorTypeCount;
    }

    function runCheckWarningCount(run) {
      return trendSummary(run).warningTypeCount;
    }

    function runCheckAlertCount(run) {
      return trendSummary(run).alertTypeCount;
    }

    function trendErrorCount(run) {
      return runCheckErrorCount(run);
    }

    function trendWarningCount(run) {
      return runCheckWarningCount(run);
    }

    function trendTotalCount(run) {
      return runCheckAlertCount(run);
    }

    function trendDurationMinutes(run) {
      return number(run?.runDurationMinutes ?? run?.durationMinutes ?? run?.timing?.durationMinutes);
    }

    async function openErrorById(errorId, options = {}) {
      if (!errorId) return;
      const payload = await fetchJson(`/api/errors/${encodeURIComponent(errorId)}`);
      const runId = payload?.run?.id || payload?.detail?.identity?.runId || "";
      if (runId) await selectRun(runId, true, { focus: "detail", errorId });
      const rows = Array.isArray(selectedDetail.value?.findings) ? selectedDetail.value.findings : [];
      const row = rows.find((item) => checkErrorId(item) === errorId) || payload.finding || {};
      activeCheckItem.value = { ...row, ...(payload.finding || {}), errorId, detail: payload.detail || row.detail || null };
      deepLinkFocus.value = "detail";
      if (runId) syncMonitorDeepLink(runId, "detail", errorId);
      if (options.initial === true) await nextTick();
    }

    async function openCheckDetail(item) {
      detailCopyState.value = "";
      const errorId = checkErrorId(item);
      const runId = item?.latestRunId || item?.runId || item?.run?.id || selectedRunId.value || "";
      if (runId) syncMonitorDeepLink(runId, "detail", errorId);
      if (!errorId) {
        activeCheckItem.value = item || null;
        return;
      }
      try {
        const payload = await fetchJson(`/api/errors/${encodeURIComponent(errorId)}`);
        activeCheckItem.value = { ...item, ...(payload.finding || {}), errorId, detail: payload.detail || item.detail || null };
      } catch (cause) {
        activeCheckItem.value = { ...item, errorId, detail: { ok: false, error: String(cause?.message || cause), identity: { errorId }, valuesRedacted: true } };
      }
    }

    function closeCheckDetail() {
      activeCheckItem.value = null;
      detailCopyState.value = "";
      if (selectedRunId.value) syncMonitorDeepLink(selectedRunId.value, deepLinkFocus.value === "detail" ? "" : deepLinkFocus.value, "");
    }

    async function copyErrorId(item) {
      await copyText(checkErrorId(item));
      detailCopyState.value = "error-id";
    }

    async function copyDetailLink(item) {
      await copyText(checkDeepLink(item));
      detailCopyState.value = "link";
    }

    async function refreshHistoricalFindings() {
      try {
        const findingsPayload = await fetchJson(findingsApiPath(checkTimeWindow.value));
        findings.value = Array.isArray(findingsPayload.findings) ? findingsPayload.findings : Array.isArray(findingsPayload.groups) ? findingsPayload.groups : [];
      } catch (cause) {
        console.warn("monitor-web historical findings refresh failed", cause);
      }
    }

    function findingsApiPath(windowValue) {
      if (windowValue === "all") return "/api/findings?limit=200";
      return `/api/findings?limit=200&window=${encodeURIComponent(windowValue || "24h")}`;
    }

    function currentHref(item) {
      if (!item || item.id === bootstrap.sentinelId) return bootstrap.basePath || "/";
      if (item.monitorRoot === true) return "/";
      return `/sentinels/${encodeURIComponent(item.id)}/`;
    }

    function trendPolyline(accessor) {
      if (trendRows.value.length < 2) return "";
      return trendRows.value.map((run, index) => `${trendX(index, trendRows.value.length)},${trendY(accessor(run))}`).join(" ");
    }

    function trendX(index, total) {
      if (total <= 1) return 24;
      return Math.round(24 + index * (672 / (total - 1)));
    }

    function trendY(value) {
      return Math.round(126 - (Number(value || 0) / trendMax.value) * 102);
    }

    function memoryPolyline(series) {
      const points = Array.isArray(series?.points) ? series.points : [];
      if (points.length < 2) return "";
      return points.map((point) => `${memoryX(point)},${memoryY(point)}`).join(" ");
    }

    function requestRatePolyline(series) {
      const points = Array.isArray(series?.points) ? series.points : [];
      if (points.length < 2) return "";
      return points.map((point) => `${memoryX(point)},${requestRateY(point)}`).join(" ");
    }

    function memoryX(point) {
      return memoryXValue(pointElapsedMinutes(point, detailTimelineStartMs.value));
    }

    function memoryY(point) {
      const memory = number(point?.memoryMb);
      return memoryYValue(memory);
    }

    function requestRateY(point) {
      return requestRateYValue(number(point?.requestPerMinute));
    }

    function memoryXValue(elapsedMinutes) {
      const ratio = clamp(number(elapsedMinutes) / detailMemoryDurationMax.value, 0, 1);
      return Math.round(memoryChartFrame.left + ratio * (memoryChartFrame.right - memoryChartFrame.left));
    }

    function memoryYValue(memoryMb) {
      const ratio = clamp(number(memoryMb) / detailMemoryMax.value, 0, 1);
      return Math.round(memoryChartFrame.bottom - ratio * (memoryChartFrame.bottom - memoryChartFrame.top));
    }

    function requestRateYValue(requestPerMinute) {
      const ratio = clamp(number(requestPerMinute) / detailRequestRateMax.value, 0, 1);
      return Math.round(memoryChartFrame.bottom - ratio * (memoryChartFrame.bottom - memoryChartFrame.top));
    }

    function memoryLineClass(index) {
      return `memory-line-${(index % 6) + 1}`;
    }

    function requestRateLineClass(index) {
      return `request-rate-line-${(index % 6) + 1}`;
    }

    function memorySeriesLabel(series) {
      const sampleCount = Number.isFinite(Number(series?.sampleCount)) ? Number(series.sampleCount) : Array.isArray(series?.points) ? series.points.length : 0;
      const latest = Array.isArray(series?.points) && series.points.length > 0 ? series.points[series.points.length - 1] : null;
      return `${series?.label || series?.pageRole || series?.pageId || "page"} · 最新 ${formatMb(latest?.memoryMb)} · 样本 ${sampleCount}`;
    }

    function requestRateSeriesLabel(series) {
      const sampleCount = Number.isFinite(Number(series?.bucketCount)) ? Number(series.bucketCount) : Array.isArray(series?.points) ? series.points.length : 0;
      const latest = Array.isArray(series?.points) && series.points.length > 0 ? series.points[series.points.length - 1] : null;
      return `${series?.label || series?.path || series?.key || "request"} · 最新 ${formatRpm(latest?.requestPerMinute)} · 峰值 ${formatRpm(series?.peakRequestPerMinute)} · 桶 ${sampleCount}`;
    }

    function showTrendTooltip(dot) {
      hoveredTrendDot.value = dot;
    }

    function hideTrendTooltip() {
      hoveredTrendDot.value = null;
    }

    onMounted(() => {
      void loadAll();
      window.setInterval(() => {
        if (!autoRefresh.value) return;
        if (Date.now() - lastAutoRefreshAt >= Math.max(5, Number(refreshSeconds.value || 30)) * 1000) void loadAll({ silent: true });
      }, 1000);
    });

    watch(checkTimeWindow, () => {
      if (checkScope.value === "history") void refreshHistoricalFindings();
    });

    watch(checkScope, () => {
      if (checkScope.value === "history") void refreshHistoricalFindings();
    });

    return {
      bootstrap,
      loading,
      error,
      manualTriggering,
      manualTriggerResult,
      manualTriggerError,
      overview,
      runs,
      findings,
      runCheckSummaries,
      selectedRunId,
      selectedDetail,
      runFilter,
      severityFilter,
      checkScope,
      checkRunId,
      checkTimeWindow,
      checkSeverityFilter,
      findingFilter,
      activeCheckItem,
      detailCopyState,
      autoRefresh,
      refreshSeconds,
      lastLoadedAt,
      hoveredTrendDot,
      deepLinkRunId,
      deepLinkFocus,
      sentinels,
      currentStatus,
      latestRun,
      severityTotals,
      filteredRuns,
      selectedRun,
      trendRows,
      latestTrendRun,
      trendDurationMax,
      trendErrorMax,
      trendWarningMax,
      trendPolylines,
      trendDots,
      timelineRuns,
      historicalCheckFindings,
      runDetailCheckFindings,
      detailRequestRate,
      detailRequestRateSeries,
      detailRequestRateMax,
      detailRequestRatePeak,
      detailRequestRateYAxisTicks,
      detailMemory,
      detailMemorySeries,
      detailMemoryMax,
      detailMemoryDurationMax,
      detailMemoryYAxisTicks,
      detailMemoryXAxisTicks,
      memoryFrame: memoryChartFrame,
      scopedCheckFindings,
      scopedCheckSummary,
      visibleCheckFindings,
      visibleCheckSummary,
      checkScopeRun,
      checkScopeText,
      cadence,
      healthChecks,
      manualTriggerState,
      manualTriggerJobName,
      manualTriggerStatusText,
      loadAll,
      selectRun,
      selectCheckRun,
      openCheckDetail,
      closeCheckDetail,
      copyErrorId,
      copyDetailLink,
      refreshNow,
      triggerQuickVerify,
      currentHref,
      showTrendTooltip,
      hideTrendTooltip,
      redCount,
      warningCount,
      findingCount,
      findingSampleCount,
      alertSampleCount,
      runCheckErrorCount,
      runCheckWarningCount,
      runCheckAlertCount,
      trendTotalCount,
      trendErrorCount,
      trendWarningCount,
      trendDurationMinutes,
      requestRatePolyline,
      requestRateLineClass,
      requestRateSeriesLabel,
      memoryPolyline,
      memoryLineClass,
      memorySeriesLabel,
      runDurationText,
      runTimingConfigText,
      severityClass,
      formatDate,
      formatAbsoluteDate,
      formatDuration,
      formatMinutes,
      formatMb,
      formatRpm,
      shortId,
      rootCauseText,
      findingTitle,
      findingCode,
      checkRowKey,
      checkRunText,
      checkTimeText,
      checkActionText,
      checkDetailRows,
      checkEvidenceRows,
      checkErrorId,
      checkDeepLink,
      checkTraceRows,
      checkOtelRows,
      levelLabel,
      findingGroupCountLabel,
      timeWindowLabel,
      detailSummaryText,
      detailSummaryRows,
      commandSummary,
      statusLabel,
    };
  },
  template: `
    <div
      class="monitor-shell"
      data-monitor-shell="true"
      :data-monitor-status="currentStatus"
      :data-manual-trigger-state="manualTriggerState"
      :data-manual-trigger-job="manualTriggerJobName"
    >
      <header class="topbar">
        <div class="title-block">
          <div class="mark" aria-hidden="true"></div>
          <div>
            <h1>HWLAB Web哨兵</h1>
            <p class="subtitle">
              <span>{{ bootstrap.node }} / {{ bootstrap.lane }}</span>
              <span class="mono">{{ bootstrap.sentinelId }}</span>
              <span>Vue monitor-web</span>
            </p>
          </div>
        </div>
        <div class="toolbar" aria-label="观察面板控制">
          <span class="pill" :class="currentStatus">{{ statusLabel(currentStatus) }}</span>
          <select v-model.number="refreshSeconds" aria-label="刷新间隔">
            <option :value="5">5s</option>
            <option :value="10">10s</option>
            <option :value="30">30s</option>
          </select>
          <button class="toolbar-button" type="button" @click="autoRefresh = !autoRefresh">{{ autoRefresh ? "自动" : "手动" }}</button>
          <button class="toolbar-button" type="button" @click="refreshNow">刷新</button>
          <button
            class="toolbar-button trigger-button"
            data-monitor-manual-trigger="true"
            type="button"
            :disabled="manualTriggering"
            :data-trigger-state="manualTriggerState"
            :data-trigger-job="manualTriggerJobName"
            @click="triggerQuickVerify"
          >
            {{ manualTriggering ? "触发中" : "触发巡检" }}
          </button>
          <span
            v-if="manualTriggerStatusText"
            class="trigger-status"
            data-monitor-manual-trigger-status="true"
            :data-trigger-state="manualTriggerState"
            :data-trigger-job="manualTriggerJobName"
          >
            {{ manualTriggerStatusText }}
          </span>
        </div>
      </header>

      <section class="entry-strip" aria-label="哨兵入口">
        <div class="entry-copy">
          <strong>哨兵入口</strong>
          <span>{{ sentinels.length }} 个实例，当前入口固定在第一屏</span>
        </div>
        <div class="entry-links">
          <a
            v-for="item in sentinels"
            :key="item.id"
            class="entry-link"
            :class="{ current: item.id === bootstrap.sentinelId, disabled: item.enabled === false }"
            :href="currentHref(item)"
          >
            <span>{{ item.id }}</span>
            <small>{{ item.id === bootstrap.sentinelId ? "当前" : item.enabled === false ? "停用" : "查看" }}</small>
          </a>
        </div>
      </section>

      <section class="trend-stage" aria-label="运行趋势与时间线">
        <section class="trend-panel" aria-labelledby="trend-heading">
          <div class="panel-header">
            <div>
              <h2 id="trend-heading">运行趋势曲线</h2>
              <p>按运行更新时间展示最近 {{ trendRows.length }} 次巡检的耗时、独立错误类型和独立告警类型</p>
            </div>
            <span class="pill" :class="cadence.stale ? 'warning' : 'healthy'">{{ cadence.stale ? "非阻塞报警" : "新鲜" }}</span>
          </div>
          <div class="trend-chart-wrap">
            <svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="运行耗时、独立错误和独立告警趋势" data-monitor-trend-curve="true">
              <line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
              <line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
              <line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
              <polyline v-if="trendPolylines.duration" class="trend-duration" :points="trendPolylines.duration"></polyline>
              <polyline v-if="trendPolylines.red" class="trend-red" :points="trendPolylines.red" data-monitor-trend-error-curve="true"></polyline>
              <polyline v-if="trendPolylines.warning" class="trend-warning" :points="trendPolylines.warning" data-monitor-trend-warning-curve="true"></polyline>
              <g
                v-for="dot in trendDots"
                :key="dot.id"
                class="trend-dot-hit"
                tabindex="0"
                role="button"
                :aria-label="dot.title"
                @mouseenter="showTrendTooltip(dot)"
                @focusin="showTrendTooltip(dot)"
                @mouseleave="hideTrendTooltip"
                @focusout="hideTrendTooltip"
              >
                <circle class="trend-hit-area" :cx="dot.x" :cy="dot.durationY" r="12"></circle>
                <circle class="trend-dot-red" :cx="dot.x" :cy="dot.redY" r="3"></circle>
                <circle class="trend-dot-warning" :cx="dot.x" :cy="dot.warningY" r="3"></circle>
                <circle class="trend-dot-duration" :cx="dot.x" :cy="dot.durationY" r="4">
                  <title>{{ dot.title }}</title>
                </circle>
              </g>
            </svg>
            <div
              v-if="hoveredTrendDot"
              class="trend-tooltip"
              data-monitor-trend-tooltip="true"
              :style="{ left: hoveredTrendDot.tooltipLeft, top: hoveredTrendDot.tooltipTop }"
            >
              <strong>{{ shortId(hoveredTrendDot.runId) }}</strong>
              <span>{{ hoveredTrendDot.absoluteTime }}</span>
              <span>状态 {{ hoveredTrendDot.status }}</span>
              <span>运行 {{ hoveredTrendDot.durationText }}</span>
              <span v-if="hoveredTrendDot.configTimingText !== '-'">配置 {{ hoveredTrendDot.configTimingText }}</span>
              <span>错误 {{ hoveredTrendDot.red }} / 告警 {{ hoveredTrendDot.warning }} / 合计 {{ hoveredTrendDot.total }}</span>
              <span v-if="hoveredTrendDot.reportSha">report {{ hoveredTrendDot.reportSha }}</span>
            </div>
            <div v-if="trendRows.length === 0" class="trend-empty">暂无运行数据</div>
          </div>
          <div class="trend-legend">
            <span class="legend-item"><span class="legend-swatch duration"></span>最新运行耗时 {{ runDurationText(latestTrendRun) }}</span>
            <span class="legend-item"><span class="legend-swatch red"></span>独立错误 {{ trendErrorCount(latestTrendRun) }} / 最高 {{ trendErrorMax }}</span>
            <span class="legend-item"><span class="legend-swatch warning"></span>独立告警 {{ trendWarningCount(latestTrendRun) }} / 最高 {{ trendWarningMax }}</span>
            <span class="legend-item">最近最高耗时 {{ formatMinutes(trendDurationMax) }}</span>
            <span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }}</span>
            <span class="legend-item">{{ cadence.alert }}</span>
          </div>
        </section>

        <section class="timeline-panel" aria-labelledby="timeline-heading" data-monitor-timeline="true">
          <div class="panel-header">
            <div>
              <h2 id="timeline-heading">运行时间线</h2>
              <p>位于哨兵入口下方，方便首屏确认最近运行</p>
            </div>
            <span class="tag">{{ cadence.label }}</span>
          </div>
          <div class="timeline-list">
            <button
              v-for="run in timelineRuns"
              :key="run.id"
              class="timeline-item"
              :class="severityClass(run)"
              type="button"
              @click="selectRun(run)"
            >
              <span class="muted">{{ formatDate(run.updatedAt || run.createdAt) }}</span>
              <span class="severity-line"><span class="timeline-marker"></span><strong>{{ run.scenarioId || shortId(run.id) }}</strong></span>
              <span class="run-alert-tags">
                <span v-if="runCheckErrorCount(run) > 0" class="tag red">错误 {{ runCheckErrorCount(run) }}</span>
                <span v-if="runCheckWarningCount(run) > 0" class="tag warning">告警 {{ runCheckWarningCount(run) }}</span>
                <span v-if="runCheckAlertCount(run) === 0" class="tag healthy">正常</span>
                <span class="tag">运行 {{ runDurationText(run) }}</span>
              </span>
            </button>
            <div v-if="timelineRuns.length === 0" class="empty">暂无时间线记录</div>
          </div>
        </section>
      </section>

      <section class="status-strip" aria-label="状态指标">
        <div class="metric">
          <span>最近运行</span>
          <strong>{{ latestRun ? formatDate(latestRun.updatedAt || latestRun.createdAt) : "-" }}</strong>
        </div>
        <div class="metric" :class="{ warning: cadence.stale }">
          <span>调度新鲜度</span>
          <strong>{{ cadence.latestAge >= 0 ? formatDuration(cadence.latestAge) : "-" }}</strong>
        </div>
        <div class="metric" :class="{ warning: cadence.cronJob?.status !== 'ok' }">
          <span>CronJob</span>
          <strong>{{ cadence.cronJob?.status || "-" }}</strong>
        </div>
        <div class="metric" :class="{ warning: cadence.observability?.coverage === 'instrumentation-gap' }">
          <span>OTel</span>
          <strong>{{ cadence.observability?.coverage || "-" }}</strong>
        </div>
        <div class="metric">
          <span>历史错误样本</span>
          <strong>{{ redCount({ severityCounts: severityTotals }) }}</strong>
        </div>
        <div class="metric">
          <span>历史告警样本</span>
          <strong>{{ warningCount({ severityCounts: severityTotals }) }}</strong>
        </div>
        <div class="metric">
          <span>最后刷新</span>
          <strong>{{ lastLoadedAt ? formatDate(lastLoadedAt) : "-" }}</strong>
        </div>
      </section>

      <section v-if="error" class="banner error" id="monitor-web-error">{{ error }}</section>

      <section class="workspace-grid" aria-label="哨兵工作区" data-monitor-independent-scroll="true">
        <aside class="pane pane-runs" aria-labelledby="runs-heading">
          <div class="pane-header">
            <div>
              <h2 id="runs-heading">运行记录</h2>
              <p class="muted">{{ filteredRuns.length }} / {{ runs.length }}</p>
            </div>
            <span v-if="loading" class="tag">加载中</span>
          </div>
          <div class="filter-row">
            <input v-model="runFilter" type="search" placeholder="搜索 run / scenario" aria-label="搜索运行">
            <select v-model="severityFilter" aria-label="严重级别筛选">
              <option value="">全部</option>
              <option value="red">错误</option>
              <option value="warning">告警</option>
              <option value="info">信息</option>
              <option value="healthy">正常</option>
            </select>
          </div>
          <div class="run-list">
            <button
              v-for="run in filteredRuns"
              :key="run.id"
              class="run-row"
              :class="[severityClass(run), { selected: run.id === selectedRunId }]"
              :data-run-id="run.id"
              :data-run-error-count="runCheckErrorCount(run)"
              :data-run-warning-count="runCheckWarningCount(run)"
              :data-run-alert-count="runCheckAlertCount(run)"
              type="button"
              @click="selectRun(run)"
            >
              <span class="row-line">
                <span class="severity-line"><span class="severity-dot"></span><strong>{{ run.scenarioId || shortId(run.id) }}</strong></span>
                <span v-if="runCheckErrorCount(run) > 0" class="tag red" data-run-error-tag="true">错误 {{ runCheckErrorCount(run) }}</span>
                <span v-if="runCheckWarningCount(run) > 0" class="tag warning" data-run-warning-tag="true">告警 {{ runCheckWarningCount(run) }}</span>
                <span v-if="runCheckAlertCount(run) === 0" class="tag healthy">正常</span>
              </span>
              <span class="row-line muted">
                <span>{{ run.status || "-" }}</span>
                <span>运行 {{ runDurationText(run) }}</span>
                <span>{{ formatDate(run.updatedAt || run.createdAt) }}</span>
              </span>
            </button>
            <div v-if="filteredRuns.length === 0" class="empty">没有匹配的运行记录</div>
          </div>
        </aside>

        <main class="pane pane-detail" aria-labelledby="detail-heading">
          <div class="pane-header">
            <div>
              <h2 id="detail-heading">运行详情</h2>
              <p class="muted">{{ selectedRun ? selectedRun.id : "未选择" }}</p>
            </div>
            <span v-if="selectedRun" class="tag" :class="severityClass(selectedRun)">{{ severityClass(selectedRun) }}</span>
          </div>
          <div v-if="selectedRun" class="detail-stack">
            <section class="detail-card">
              <h3>摘要</h3>
              <div class="detail-grid">
                <div class="metric"><span>状态</span><strong>{{ selectedRun.status || "-" }}</strong></div>
                <div class="metric"><span>运行分钟</span><strong>{{ runDurationText(selectedRun) }}</strong></div>
                <div class="metric"><span>运行配置</span><strong>{{ runTimingConfigText(selectedRun) }}</strong></div>
                <div class="metric"><span>监测项类型</span><strong>{{ findingCount(selectedRun) }}</strong></div>
                <div class="metric"><span>错误/告警样本</span><strong>{{ alertSampleCount(selectedRun) }}</strong></div>
                <div class="metric"><span>全部样本</span><strong>{{ findingSampleCount(selectedRun) }}</strong></div>
                <div class="metric"><span>Observer</span><strong>{{ selectedRun.observerId || "-" }}</strong></div>
                <div class="metric"><span>更新时间</span><strong>{{ formatDate(selectedRun.updatedAt || selectedRun.createdAt) }}</strong></div>
              </div>
            </section>
            <section class="detail-card">
              <h3>健康检查</h3>
              <div class="check-grid">
                <span v-for="check in healthChecks" :key="check.key" class="check-chip" :class="check.ok ? 'ok' : 'bad'">{{ check.text }}</span>
                <span v-if="healthChecks.length === 0" class="check-chip">无检查数据</span>
              </div>
            </section>
            <section class="detail-card">
              <h3>报告摘要</h3>
              <div class="summary-list">
                <div v-for="row in detailSummaryRows(selectedDetail)" :key="row.key" class="summary-row">
                  <span>{{ row.label }}</span>
                  <strong>{{ row.value }}</strong>
                </div>
              </div>
            </section>
            <section
              class="detail-card detail-card-wide memory-card"
              data-run-memory-chart="true"
              :data-memory-run-id="selectedRunId"
              :data-memory-page-count="detailMemorySeries.length"
              :data-memory-sample-count="detailMemory.sampleCount || 0"
              :data-memory-source="detailMemory.source || ''"
              :data-request-rate-series-count="detailRequestRateSeries.length"
              :data-request-rate-source="detailRequestRate.source || ''"
            >
              <div class="detail-card-heading">
                <h3>请求频率 / 页面内存曲线</h3>
                <span class="tag">请求峰值 {{ formatRpm(detailRequestRatePeak) }} · 内存峰值 {{ formatMb(detailMemory.maxMemoryMb || detailMemoryMax) }}</span>
              </div>
              <div v-if="detailRequestRateSeries.length > 0" class="request-rate-chart-wrap" data-run-request-rate-chart="true">
                <svg class="request-rate-chart" viewBox="0 0 720 178" role="img" aria-label="运行详情 API 请求频率曲线，纵轴为每分钟请求数，横轴与内存曲线对齐">
                  <g data-request-rate-axis-y="true">
                    <g v-for="tick in detailRequestRateYAxisTicks" :key="tick.key" data-request-rate-axis-y-tick="true">
                      <line class="memory-grid-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="tick.y" :y2="tick.y"></line>
                      <text class="memory-axis-label memory-axis-label-y" :x="memoryFrame.yLabelX" :y="tick.y + 4">{{ tick.label }}</text>
                    </g>
                    <line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.left" :y1="memoryFrame.top" :y2="memoryFrame.bottom"></line>
                    <text class="memory-axis-title memory-axis-title-y" x="18" y="78" transform="rotate(-90 18 78)">请求 rpm</text>
                  </g>
                  <g data-request-rate-axis-x="true">
                    <line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="memoryFrame.bottom" :y2="memoryFrame.bottom"></line>
                    <g v-for="tick in detailMemoryXAxisTicks" :key="'request-' + tick.key" data-request-rate-axis-x-tick="true">
                      <line class="memory-tick-line" :x1="tick.x" :x2="tick.x" :y1="memoryFrame.top" :y2="memoryFrame.bottom + 6"></line>
                      <text class="memory-axis-label memory-axis-label-x" :x="tick.x" :y="memoryFrame.xLabelY">{{ tick.label }}</text>
                    </g>
                    <text class="memory-axis-title memory-axis-title-x" :x="(memoryFrame.left + memoryFrame.right) / 2" :y="memoryFrame.xTitleY">运行分钟</text>
                  </g>
                  <polyline
                    v-for="(series, index) in detailRequestRateSeries"
                    :key="series.key || series.label || index"
                    class="request-rate-line"
                    :class="requestRateLineClass(index)"
                    :points="requestRatePolyline(series)"
                  ></polyline>
                </svg>
                <div class="memory-legend request-rate-legend">
                  <span
                    v-for="(series, index) in detailRequestRateSeries"
                    :key="(series.key || series.label || index) + '-request-rate-legend'"
                    class="legend-item"
                  >
                    <span class="legend-swatch request-rate" :class="requestRateLineClass(index)"></span>{{ requestRateSeriesLabel(series) }}
                  </span>
                </div>
              </div>
              <div v-else class="empty request-rate-empty">暂无 API 请求频率样本</div>
              <div v-if="detailMemorySeries.length > 0" class="memory-chart-wrap">
                <svg class="memory-chart" viewBox="0 0 720 178" role="img" aria-label="运行详情页面内存曲线，纵轴为 MB，横轴为运行分钟">
                  <g data-memory-axis-y="true">
                    <g v-for="tick in detailMemoryYAxisTicks" :key="tick.key" data-memory-axis-y-tick="true">
                      <line class="memory-grid-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="tick.y" :y2="tick.y"></line>
                      <text class="memory-axis-label memory-axis-label-y" :x="memoryFrame.yLabelX" :y="tick.y + 4">{{ tick.label }}</text>
                    </g>
                    <line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.left" :y1="memoryFrame.top" :y2="memoryFrame.bottom"></line>
                    <text class="memory-axis-title memory-axis-title-y" x="18" y="78" transform="rotate(-90 18 78)">内存 MB</text>
                  </g>
                  <g data-memory-axis-x="true">
                    <line class="memory-axis-line" :x1="memoryFrame.left" :x2="memoryFrame.right" :y1="memoryFrame.bottom" :y2="memoryFrame.bottom"></line>
                    <g v-for="tick in detailMemoryXAxisTicks" :key="tick.key" data-memory-axis-x-tick="true">
                      <line class="memory-tick-line" :x1="tick.x" :x2="tick.x" :y1="memoryFrame.bottom" :y2="memoryFrame.bottom + 6"></line>
                      <text class="memory-axis-label memory-axis-label-x" :x="tick.x" :y="memoryFrame.xLabelY">{{ tick.label }}</text>
                    </g>
                    <text class="memory-axis-title memory-axis-title-x" :x="(memoryFrame.left + memoryFrame.right) / 2" :y="memoryFrame.xTitleY">运行分钟</text>
                  </g>
                  <polyline
                    v-for="(series, index) in detailMemorySeries"
                    :key="series.key || series.label || index"
                    class="memory-line"
                    :class="memoryLineClass(index)"
                    :points="memoryPolyline(series)"
                  ></polyline>
                </svg>
                <div class="memory-legend">
                  <span
                    v-for="(series, index) in detailMemorySeries"
                    :key="(series.key || series.label || index) + '-legend'"
                    class="legend-item"
                  >
                    <span class="legend-swatch memory" :class="memoryLineClass(index)"></span>{{ memorySeriesLabel(series) }}
                  </span>
                </div>
              </div>
              <div v-else class="empty">暂无页面级内存样本</div>
            </section>
            <section class="detail-card">
              <h3>复现命令</h3>
              <pre>{{ commandSummary(selectedDetail) }}</pre>
            </section>
          </div>
          <div v-else class="empty">选择一条运行记录查看详情</div>
        </main>
      </section>

      <section
        class="pane pane-findings checks-panel"
        aria-labelledby="findings-heading"
        data-monitor-checks="true"
        :data-check-scope="checkScope"
        :data-check-run-id="checkScopeRun ? checkScopeRun.id : ''"
        :data-check-type-count="scopedCheckSummary.typeCount"
        :data-check-error-type-count="scopedCheckSummary.errorTypeCount"
        :data-check-warning-type-count="scopedCheckSummary.warningTypeCount"
        :data-check-alert-type-count="scopedCheckSummary.alertTypeCount"
        :data-check-error-samples="scopedCheckSummary.errorSamples"
        :data-check-warning-samples="scopedCheckSummary.warningSamples"
        :data-check-alert-samples="scopedCheckSummary.alertSamples"
        :data-visible-check-count="visibleCheckFindings.length"
        :data-visible-check-alert-samples="visibleCheckSummary.alertSamples"
      >
          <div class="pane-header">
            <div>
              <h2 id="findings-heading">监测项</h2>
              <p class="muted">{{ checkScopeText }}</p>
            </div>
            <span class="tag">错误/告警样本 {{ scopedCheckSummary.alertSamples }}</span>
          </div>
          <div class="filter-row">
            <select v-model="checkScope" aria-label="监测项作用域">
              <option value="run">按运行记录</option>
              <option value="history">按时间聚合</option>
            </select>
            <select v-model="checkRunId" :disabled="checkScope === 'history'" aria-label="选择运行记录" @change="selectCheckRun">
              <option v-for="run in runs" :key="run.id" :value="run.id">{{ shortId(run.id) }} · {{ formatDate(run.updatedAt || run.createdAt) }}</option>
            </select>
            <select v-model="checkTimeWindow" :disabled="checkScope !== 'history'" aria-label="监测项时间窗口">
              <option value="1h">最近 1 小时</option>
              <option value="24h">最近 24 小时</option>
              <option value="7d">最近 7 天</option>
              <option value="all">全部历史</option>
            </select>
            <select v-model="checkSeverityFilter" aria-label="监测项等级筛选">
              <option value="alert">错误+告警</option>
              <option value="red">错误</option>
              <option value="warning">告警</option>
              <option value="info">信息</option>
              <option value="all">全部</option>
            </select>
            <input v-model="findingFilter" type="search" placeholder="搜索编号 / 标题 / 处理建议" aria-label="搜索监测项">
          </div>
          <div class="check-summary" aria-label="监测项摘要">
            <div class="metric"><span>当前作用域</span><strong>{{ checkScope === "history" ? timeWindowLabel(checkTimeWindow) : shortId(checkScopeRun && checkScopeRun.id) }}</strong></div>
            <div class="metric"><span>监测项类型</span><strong>{{ scopedCheckSummary.typeCount }}</strong></div>
            <div class="metric"><span>错误/告警类型</span><strong>{{ scopedCheckSummary.alertTypeCount }}</strong></div>
            <div class="metric"><span>错误样本</span><strong>{{ scopedCheckSummary.errorSamples }}</strong></div>
            <div class="metric"><span>告警样本</span><strong>{{ scopedCheckSummary.warningSamples }}</strong></div>
            <div class="metric"><span>错误/告警样本</span><strong>{{ scopedCheckSummary.alertSamples }}</strong></div>
          </div>
          <div class="finding-list check-table-wrap">
            <table v-if="visibleCheckFindings.length > 0" class="check-table" aria-label="监测项列表">
              <thead>
                <tr>
                  <th scope="col">编号</th>
                  <th scope="col">等级</th>
                  <th scope="col">标题</th>
                  <th scope="col">样本</th>
                  <th scope="col">运行记录</th>
                  <th scope="col">时间</th>
                  <th scope="col">处理建议</th>
                  <th scope="col">详情</th>
                </tr>
              </thead>
              <tbody>
                <tr
                  v-for="(item, index) in visibleCheckFindings"
                  :key="checkRowKey(item, index)"
                  class="check-row"
                  :class="severityClass(item)"
                  data-check-row="true"
                  tabindex="0"
                  role="button"
                  :aria-label="'查看监测项详情 ' + findingCode(item) + ' ' + findingTitle(item)"
                  :data-check-code="findingCode(item)"
                  :data-error-id="checkErrorId(item)"
                  :data-check-deep-link="checkDeepLink(item)"
                  @click="openCheckDetail(item)"
                  @keydown.enter.prevent="openCheckDetail(item)"
                  @keydown.space.prevent="openCheckDetail(item)"
                >
                  <td><span class="check-code">{{ findingCode(item) }}</span></td>
                  <td><span class="tag" :class="severityClass(item)">{{ levelLabel(item) }}</span></td>
                  <td class="check-title-cell"><strong>{{ findingTitle(item) }}</strong><span>{{ rootCauseText(item) }}</span></td>
                  <td>{{ findingGroupCountLabel(item) }}</td>
                  <td class="mono">{{ checkRunText(item) }}</td>
                  <td>{{ checkTimeText(item) }}</td>
                  <td>{{ checkActionText(item) }}</td>
                  <td><span class="detail-link">查看</span></td>
                </tr>
              </tbody>
            </table>
            <div v-if="visibleCheckFindings.length === 0" class="empty">暂无匹配监测项</div>
          </div>
      </section>

      <div
        v-if="activeCheckItem"
        class="check-dialog-backdrop"
        data-check-dialog="true"
        :data-error-id="checkErrorId(activeCheckItem)"
        @click.self="closeCheckDetail"
        @keydown.esc="closeCheckDetail"
      >
        <section class="check-dialog" role="dialog" aria-modal="true" aria-labelledby="check-dialog-title" tabindex="-1">
          <header class="check-dialog-header">
            <div>
              <span class="check-code">{{ findingCode(activeCheckItem) }}</span>
              <h2 id="check-dialog-title">{{ findingTitle(activeCheckItem) }}</h2>
              <p class="muted">
                <span>{{ checkScopeText }}</span>
                <span v-if="checkErrorId(activeCheckItem)" class="error-id-pill" data-error-id="true">{{ checkErrorId(activeCheckItem) }}</span>
              </p>
            </div>
            <div class="check-dialog-actions">
              <button v-if="checkErrorId(activeCheckItem)" class="dialog-close copy-button" type="button" data-copy-error-id="true" @click="copyErrorId(activeCheckItem)">复制ID</button>
              <button v-if="checkErrorId(activeCheckItem)" class="dialog-close copy-button" type="button" data-copy-detail-link="true" @click="copyDetailLink(activeCheckItem)">复制链接</button>
              <span class="tag" :class="severityClass(activeCheckItem)">{{ levelLabel(activeCheckItem) }} · {{ findingGroupCountLabel(activeCheckItem) }}</span>
              <button class="dialog-close" type="button" aria-label="关闭监测项详情" @click="closeCheckDetail">关闭</button>
            </div>
          </header>
          <div class="check-dialog-body">
            <section class="detail-card">
              <h3>基本信息</h3>
              <div class="summary-list">
                <div v-for="row in checkDetailRows(activeCheckItem)" :key="row.key" class="summary-row">
                  <span>{{ row.label }}</span>
                  <strong>{{ row.value }}</strong>
                </div>
              </div>
            </section>
            <section class="detail-card">
              <h3>详情</h3>
              <p>{{ rootCauseText(activeCheckItem) }}</p>
              <p class="muted">处理建议: {{ checkActionText(activeCheckItem) }}</p>
              <p v-if="detailCopyState" class="copy-status">{{ detailCopyState === "link" ? "已复制深链" : "已复制错误ID" }}</p>
            </section>
            <section class="detail-card detail-card-wide">
              <h3>哨兵证据</h3>
              <div class="summary-list">
                <div v-for="row in checkEvidenceRows(activeCheckItem)" :key="row.key" class="summary-row">
                  <span>{{ row.label }}</span>
                  <strong>{{ row.value }}</strong>
                </div>
              </div>
            </section>
            <section class="detail-card">
              <h3>Trace 引用</h3>
              <div class="summary-list" data-sentinel-evidence-summary="true">
                <div v-for="row in checkTraceRows(activeCheckItem)" :key="row.key" class="summary-row">
                  <span>{{ row.label }}</span>
                  <strong>{{ row.value }}</strong>
                </div>
              </div>
            </section>
            <section class="detail-card">
              <h3>OTel 追溯</h3>
              <div class="summary-list" data-otel-trace-summary="true">
                <div v-for="row in checkOtelRows(activeCheckItem)" :key="row.key" class="summary-row">
                  <span>{{ row.label }}</span>
                  <strong>{{ row.value }}</strong>
                </div>
              </div>
            </section>
          </div>
        </section>
      </div>
    </div>
  `,
}).mount("#monitor-web-root");

function readBootstrap() {
  const root = document.querySelector("#monitor-web-root");
  const script = document.querySelector("#monitor-web-bootstrap");
  let parsed = {};
  try {
    parsed = script?.textContent ? JSON.parse(script.textContent) : {};
  } catch {
    parsed = {};
  }
  return {
    node: root?.getAttribute("data-node") || parsed.node || "",
    lane: root?.getAttribute("data-lane") || parsed.lane || "",
    sentinelId: root?.getAttribute("data-sentinel-id") || parsed.sentinelId || "",
    basePath: root?.getAttribute("data-base-path") || parsed.basePath || "",
    publicOrigin: root?.getAttribute("data-public-origin") || parsed.publicOrigin || "",
    contractVersion: root?.getAttribute("data-contract-version") || parsed.contractVersion || "",
    sentinels: Array.isArray(parsed.sentinels) ? parsed.sentinels : [],
  };
}

function readMonitorDeepLink() {
  const params = new URLSearchParams(window.location.search || "");
  const runId = String(params.get("run") || params.get("runId") || "").trim();
  const errorId = String(params.get("error") || params.get("errorId") || "").trim();
  const focus = normalizeDeepLinkFocus(params.get("focus") || params.get("panel") || window.location.hash.replace(/^#/u, ""));
  return { runId, focus, errorId };
}

function syncMonitorDeepLink(runId, focus, errorId = "") {
  if (!window.history?.replaceState) return;
  const nextFocus = normalizeDeepLinkFocus(focus);
  const url = new URL(window.location.href);
  if (runId) url.searchParams.set("run", runId);
  else url.searchParams.delete("run");
  if (nextFocus) url.searchParams.set("focus", nextFocus);
  else url.searchParams.delete("focus");
  const nextErrorId = String(errorId || "").trim();
  if (nextErrorId) {
    url.searchParams.set("error", nextErrorId);
    url.searchParams.set("panel", "detail");
  } else {
    url.searchParams.delete("error");
    url.searchParams.delete("panel");
  }
  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";
  if (["detail", "error", "finding"].includes(text)) return "detail";
  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}`);
  return await response.json();
}

async function postJson(path, body) {
  const response = await fetch(apiUrl(path), {
    method: "POST",
    cache: "no-store",
    headers: { "content-type": "application/json", accept: "application/json" },
    body: JSON.stringify(body || {}),
  });
  const payload = await response.json().catch(() => ({}));
  if (!response.ok || payload.ok === false) {
    const reason = payload.reason || payload.error || `${path} HTTP ${response.status}`;
    throw new Error(String(reason));
  }
  return payload;
}

function apiUrl(path) {
  const prefix = bootstrap.basePath || "";
  const suffix = path.startsWith("/") ? path : `/${path}`;
  return `${prefix}${suffix}`;
}

async function copyText(value) {
  const text = String(value || "");
  if (!text) return;
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }
  const input = document.createElement("textarea");
  input.value = text;
  input.setAttribute("readonly", "true");
  input.style.position = "fixed";
  input.style.left = "-1000px";
  document.body.appendChild(input);
  input.select();
  document.execCommand("copy");
  input.remove();
}

function setReady(value) {
  document.querySelector("#monitor-web-root")?.setAttribute("data-monitor-ready", value ? "true" : "false");
}

function redCount(item) {
  const counts = item?.severityCounts || item || {};
  return number(counts.red) + number(counts.critical) + number(counts.error);
}

function warningCount(item) {
  const counts = item?.severityCounts || item || {};
  return number(counts.warning) + number(counts.warn) + number(counts.amber);
}

function findingCount(item) {
  if (Number.isFinite(Number(item?.findingTypeCount))) return Number(item.findingTypeCount);
  if (Number.isFinite(Number(item?.findingCount))) return Number(item.findingCount);
  if (Number.isFinite(Number(item?.finding_count))) return Number(item.finding_count);
  if (Number.isFinite(Number(item?.count))) return Number(item.count);
  const counts = item?.severityCounts;
  if (counts && typeof counts === "object") return Object.keys(counts).length;
  return Object.values(item || {}).reduce((sum, value) => sum + number(value), 0);
}

function findingSampleCount(item) {
  if (Number.isFinite(Number(item?.findingSampleCount))) return Number(item.findingSampleCount);
  const counts = item?.severityCounts;
  if (counts && typeof counts === "object") return Object.values(counts).reduce((sum, value) => sum + number(value), 0);
  return 0;
}

function alertSampleCount(item) {
  if (Number.isFinite(Number(item?.findingAlertSampleCount))) return Number(item.findingAlertSampleCount);
  return redCount(item) + warningCount(item);
}

function summarizeCheckRows(rows) {
  const items = Array.isArray(rows) ? rows : [];
  const errorItems = items.filter((item) => severityBucket(item) === "red");
  const warningItems = items.filter((item) => severityBucket(item) === "warning");
  const sampleCount = (entry) => Number.isFinite(Number(entry?.count)) ? Number(entry.count) : 1;
  const sumSamples = (entries) => entries.reduce((sum, entry) => sum + sampleCount(entry), 0);
  return {
    typeCount: items.length,
    errorTypeCount: errorItems.length,
    warningTypeCount: warningItems.length,
    alertTypeCount: errorItems.length + warningItems.length,
    errorSamples: sumSamples(errorItems),
    warningSamples: sumSamples(warningItems),
    alertSamples: sumSamples(errorItems) + sumSamples(warningItems),
    allSamples: sumSamples(items),
  };
}

function emptyCheckSummary() {
  return {
    typeCount: 0,
    errorTypeCount: 0,
    warningTypeCount: 0,
    alertTypeCount: 0,
    errorSamples: 0,
    warningSamples: 0,
    alertSamples: 0,
    allSamples: 0,
  };
}

function checkMatchesLevel(item, filter) {
  const value = String(filter || "alert");
  const bucket = severityBucket(item);
  if (value === "all") return true;
  if (value === "alert") return bucket === "red" || bucket === "warning";
  return bucket === value;
}

function severityBucket(item) {
  const explicit = String(item?.maxSeverity || item?.checkLevel || item?.severity || item?.level || "").toLowerCase();
  if (["red", "critical", "error", "blocked", "failed"].includes(explicit)) return "red";
  if (["warning", "warn", "amber"].includes(explicit)) return "warning";
  if (["info", "notice"].includes(explicit)) return "info";
  if (redCount(item) > 0) return "red";
  if (warningCount(item) > 0) return "warning";
  return "healthy";
}

function severityClass(item) {
  return severityBucket(item);
}

function formatDate(value) {
  if (!value) return "-";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  const now = Date.now();
  const seconds = Math.max(0, Math.round((now - date.getTime()) / 1000));
  if (seconds < 90) return `${seconds}s前`;
  if (seconds < 7200) return `${Math.round(seconds / 60)}m前`;
  if (seconds < 172800) return `${Math.round(seconds / 3600)}h前`;
  return date.toISOString().slice(5, 16).replace("T", " ");
}

function formatAbsoluteDate(value) {
  if (!value) return "-";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  return `${date.toISOString().slice(0, 19).replace("T", " ")} UTC`;
}

function formatDuration(seconds) {
  const value = Math.max(0, Number(seconds || 0));
  if (value < 90) return `${Math.round(value)}s`;
  if (value < 7200) return `${Math.round(value / 60)}m`;
  if (value < 172800) return `${Math.round(value / 3600)}h`;
  return `${Math.round(value / 86400)}d`;
}

function formatMinutes(value) {
  const numberValue = optionalNumber(value);
  if (numberValue === null) return "-";
  const rounded = Math.round(numberValue * 100) / 100;
  return `${Number.isInteger(rounded) ? String(rounded) : String(rounded).replace(/0+$/u, "").replace(/\.$/u, "")} 分钟`;
}

function formatMinuteTick(value) {
  const numberValue = optionalNumber(value);
  if (numberValue === null) return "-";
  const rounded = Math.round(numberValue * 100) / 100;
  const text = Number.isInteger(rounded) ? String(rounded) : String(rounded).replace(/0+$/u, "").replace(/\.$/u, "");
  return `${text}m`;
}

function formatMb(value) {
  const numberValue = optionalNumber(value);
  if (numberValue === null) return "-";
  const rounded = Math.round(numberValue * 100) / 100;
  const text = rounded >= 100 ? String(Math.round(rounded)) : rounded >= 10 ? rounded.toFixed(1).replace(/\.0$/u, "") : rounded.toFixed(2).replace(/0+$/u, "").replace(/\.$/u, "");
  return `${text} MB`;
}

function formatRpm(value) {
  const numberValue = optionalNumber(value);
  if (numberValue === null) return "-";
  const rounded = Math.round(numberValue * 10) / 10;
  const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/u, "");
  return `${text} rpm`;
}

function memoryTickValues(maxValue) {
  const max = Math.max(1, number(maxValue));
  return [0, max / 2, max];
}

function tagRequestRateSeries(rows, scope) {
  return rows.map((series) => ({
    ...series,
    scope: series?.scope || scope,
  }));
}

function pointTimeMs(point) {
  const raw = point?.ts || point?.startAt;
  if (typeof raw !== "string" || raw.length === 0) return null;
  const parsed = Date.parse(raw);
  return Number.isFinite(parsed) ? parsed : null;
}

function pointElapsedMinutes(point, timelineStartMs) {
  const tsMs = pointTimeMs(point);
  if (tsMs !== null && Number.isFinite(Number(timelineStartMs))) return Math.max(0, (tsMs - Number(timelineStartMs)) / 60000);
  return number(point?.elapsedMinutes);
}

function runDurationText(run) {
  const timing = run?.timing || {};
  return formatMinutes(optionalNumber(run?.runDurationMinutes, run?.durationMinutes, timing.runDurationMinutes, timing.durationMinutes));
}

function runTimingConfigText(run) {
  const timing = run?.timing || {};
  const cadence = optionalNumber(run?.scenarioCadenceMinutes, timing.scenarioCadenceMinutes);
  const maxRun = optionalNumber(run?.scenarioMaxRunMinutes, timing.scenarioMaxRunMinutes);
  const scheduler = optionalNumber(run?.schedulerIntervalMinutes, timing.schedulerIntervalMinutes);
  const parts = [];
  if (cadence !== null) parts.push(`探测周期 ${formatMinutes(cadence)}`);
  if (maxRun !== null) parts.push(`上限 ${formatMinutes(maxRun)}`);
  if (scheduler !== null) parts.push(`服务心跳/扫描 ${formatMinutes(scheduler)}`);
  return parts.length > 0 ? parts.join(" / ") : "-";
}

function runTimingSourceText(run) {
  const timing = run?.timing || {};
  return safeDetailValue(run?.durationSource || timing.durationSource || timing.sourceOfTruth);
}

function timeWindowLabel(value) {
  if (value === "1h") return "最近 1 小时";
  if (value === "24h") return "最近 24 小时";
  if (value === "7d") return "最近 7 天";
  if (value === "all") return "全部历史";
  return String(value || "未配置");
}

function shortId(value) {
  const text = String(value || "");
  return text.length > 18 ? `${text.slice(0, 10)}...${text.slice(-6)}` : text || "-";
}

function shortHash(value) {
  const text = String(value || "");
  if (text.length === 0) return "";
  return text.length > 12 ? text.slice(0, 12) : text;
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function rootCauseText(item) {
  const display = checkDisplay(item);
  if (display.summary) return display.summary;
  const safeSummary = safeUserText(item?.checkSummaryZh || item?.check?.summaryZh);
  if (safeSummary) return safeSummary;
  if (item?.rootCause) return "已记录内部根因，见报告详情。";
  return "尚未记录用户可见问题摘要。";
}

function findingTitle(item) {
  const dynamicTitle = safeDisplayTitle(item?.displayTitleZh, item?.errorTitleZh, item?.timeoutDisplay?.titleZh);
  if (dynamicTitle) return dynamicTitle;
  const display = checkDisplay(item);
  if (display.title) return display.title;
  return safeDisplayTitle(item?.checkTitleZh, item?.check?.titleZh) || "未登记监测项";
}

function findingCode(item) {
  return checkDisplay(item).code;
}

function levelLabel(item) {
  const value = String(item?.checkLevel || item?.severity || item?.level || "").toLowerCase();
  if (["critical", "red"].includes(value)) return "严重";
  if (["error", "blocked", "failed"].includes(value)) return "错误";
  if (["warning", "warn", "amber"].includes(value)) return "告警";
  if (["info", "notice"].includes(value)) return "信息";
  return "未知";
}

function findingGroupCountLabel(item) {
  const runCount = Number.isFinite(Number(item?.runCount)) ? Number(item.runCount) : null;
  const sampleCount = Number.isFinite(Number(item?.count)) ? Number(item.count) : null;
  if (runCount !== null && sampleCount !== null) return `${runCount} 次运行 / ${sampleCount} 样本`;
  if (sampleCount !== null) return `${sampleCount} 样本`;
  if (runCount !== null) return `${runCount} 次运行`;
  return "1 样本";
}

function findingSearchText(item) {
  return [
    item?.checkCode,
    item?.check?.code,
    item?.checkTitleZh,
    item?.check?.titleZh,
    item?.checkLevel,
    item?.severity,
    item?.findingId,
    item?.code,
    item?.summary,
    item?.checkSummaryZh,
    item?.rootCause,
    item?.evidenceSummary,
    item?.nextAction,
    item?.timingStatus,
    item?.timingSourceOfTruth,
    item?.scenarioId,
    item?.latestRunId,
  ].filter((value) => value !== null && value !== undefined).join(" ").toLowerCase();
}

function checkRowKey(item, index) {
  return [
    findingCode(item),
    item?.latestRunId || item?.runId || item?.run?.id || "",
    item?.sampleSeq ?? item?.count ?? index,
  ].join(":");
}

function checkRunText(item) {
  return shortId(item?.latestRunId || item?.runId || item?.run?.id || item?.scenarioId || "");
}

function checkTimeText(item) {
  const value = item?.latestRunUpdatedAt || item?.updatedAt || item?.updated_at || item?.createdAt || item?.created_at || item?.timestamp || "";
  return value ? formatDate(value) : "-";
}

function checkActionText(item) {
  const display = checkDisplay(item);
  return display.action || safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理";
}

function checkErrorId(item) {
  return safeDetailValue(item?.errorId || item?.detail?.identity?.errorId) === "-" ? "" : safeDetailValue(item?.errorId || item?.detail?.identity?.errorId);
}

function checkDeepLink(item) {
  const direct = item?.detail?.links?.canonicalUrl || item?.detail?.links?.canonicalPath || "";
  if (direct) return String(direct);
  const errorId = checkErrorId(item);
  if (!errorId) return window.location.href;
  const url = new URL(window.location.href);
  url.searchParams.set("error", errorId);
  url.searchParams.set("panel", "detail");
  const runId = item?.latestRunId || item?.runId || item?.run?.id || item?.detail?.identity?.runId || "";
  if (runId) url.searchParams.set("run", runId);
  return url.toString();
}

function checkDetailRows(item) {
  if (!item) return [{ key: "empty", label: "状态", value: "未选择监测项" }];
  const identity = item?.detail?.identity || {};
  return [
    { key: "errorId", label: "错误ID", value: checkErrorId(item) || "-" },
    { key: "code", label: "编号", value: findingCode(item) },
    { key: "level", label: "等级", value: levelLabel(item) },
    { key: "samples", label: "样本", value: findingGroupCountLabel(item) },
    { key: "run", label: "运行记录", value: safeDetailValue(identity.runId || checkRunText(item)) },
    { key: "time", label: "时间", value: checkTimeText(item) },
    { key: "scenario", label: "场景", value: safeDetailValue(identity.scenarioId || item?.scenarioId || item?.scenario_id) },
    { key: "observer", label: "观察任务", value: safeDetailValue(item?.observerId || item?.observer_id) },
    { key: "timing", label: "计时来源", value: checkTimingText(item) },
    { key: "report", label: "报告", value: shortHash(item?.reportJsonSha256 || item?.report_json_sha256 || item?.reportSha256 || "") || "-" },
  ].filter((row) => row.value !== "");
}

function checkEvidenceRows(item) {
  if (!item) return [{ key: "empty", label: "状态", value: "未选择监测项" }];
  const evidence = item?.evidence && typeof item.evidence === "object" && !Array.isArray(item.evidence) ? item.evidence : {};
  const detailEvidence = item?.detail?.sentinelEvidence && typeof item.detail.sentinelEvidence === "object" ? item.detail.sentinelEvidence : {};
  const rows = [
    { key: "summary", label: "证据摘要", value: safeDetailValue(detailEvidence.evidenceSummary || item?.evidenceSummary || evidence.summary || rootCauseText(item)) },
    { key: "rootCause", label: "根因", value: safeDetailValue(detailEvidence.rootCause || item?.rootCause) },
    { key: "rootCauseStatus", label: "根因状态", value: safeDetailValue(detailEvidence.rootCauseStatus || item?.rootCauseStatus) },
    { key: "sample", label: "样本序号", value: safeDetailValue(item?.sampleSeq ?? evidence.sampleSeq) },
    { key: "page", label: "页面", value: safeDetailValue(item?.pageRole || evidence.pageRole) },
    { key: "command", label: "命令编号", value: safeDetailValue(item?.commandId || evidence.commandId) },
    { key: "range", label: "采集范围", value: safeDetailValue(item?.sentinelRange || evidence.sentinelRange) },
    { key: "timing", label: "计时状态", value: checkTimingText(item) },
    { key: "blocking", label: "阻塞状态", value: item?.blocking === true ? "阻塞" : "非阻塞" },
  ].filter((row) => row.value !== "" && row.value !== "-");
  return rows.length > 0 ? rows : [{ key: "none", label: "证据摘要", value: "已记录到报告详情。" }];
}

function checkTraceRows(item) {
  const refs = Array.isArray(item?.detail?.traceRefs) ? item.detail.traceRefs : Array.isArray(item?.traceRefs) ? item.traceRefs : [];
  if (refs.length === 0) return [{ key: "none", label: "状态", value: "未发现 trace 引用" }];
  return refs.slice(0, 8).map((ref, index) => ({
    key: `${index}-${ref.traceId || ""}`,
    label: ref.type || "trace",
    value: safeDetailValue(ref.traceId),
  }));
}

function checkOtelRows(item) {
  const otel = item?.detail?.otel || {};
  const summaries = Array.isArray(otel.summaries) ? otel.summaries : [];
  const rows = [{ key: "status", label: "状态", value: safeDetailValue(otel.status) }];
  if (summaries.length === 0) return rows;
  return rows.concat(summaries.slice(0, 6).map((summary, index) => ({
    key: `otel-${index}`,
    label: safeDetailValue(summary.traceId || `trace ${index + 1}`),
    value: [
      summary.status || "-",
      summary.spanCount !== undefined ? `spans=${summary.spanCount}` : "",
      summary.errorSpanCount !== undefined ? `errors=${summary.errorSpanCount}` : "",
      summary.httpStatus !== undefined ? `http=${summary.httpStatus}` : "",
    ].filter(Boolean).join(" "),
  })));
}

function checkTimingText(item) {
  const status = item?.timingStatus || "";
  const source = item?.timingSourceOfTruth || item?.expectedElapsedSource || item?.evidenceKind || "";
  if (!status && !source) return "";
  return [
    status ? `status=${safeDetailValue(status)}` : "",
    source ? `source=${safeDetailValue(source)}` : "",
    item?.timingAlert === true ? "alert=true" : "",
  ].filter(Boolean).join(" ");
}

function safeDetailValue(value) {
  if (value === null || value === undefined || value === "") return "-";
  const text = String(value).replace(/\s+/g, " ").trim();
  return text.length > 0 ? text : "-";
}

function safeDisplayTitle(...values) {
  for (const value of values) {
    const title = safeTitleText(value);
    if (title && title !== "未登记监测项") return title;
  }
  return "";
}

function checkDisplay(item) {
  const rawCode = rawCheckCode(item);
  const rawId = rawFindingId(item);
  const serviceCode = publicCheckCode(item?.checkCode || item?.check?.code);
  const detailEvidence = item?.detail?.sentinelEvidence && typeof item.detail.sentinelEvidence === "object" ? item.detail.sentinelEvidence : {};
  const dynamicTitle = safeDisplayTitle(item?.displayTitleZh, item?.errorTitleZh, item?.timeoutDisplay?.titleZh, detailEvidence.displayTitleZh);
  return {
    code: serviceCode || publicCheckCode(rawId || rawCode) || stableCheckCode(rawCode || rawId),
    title: dynamicTitle || safeDisplayTitle(item?.checkTitleZh, item?.check?.titleZh) || "未登记监测项",
    summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh || detailEvidence.summary || detailEvidence.evidenceSummary) || "已记录监测项详情，见报告原文。",
    action: safeUserText(item?.checkActionZh || item?.check?.actionZh || detailEvidence.nextAction) || "查看详情后处理",
  };
}

function rawCheckCode(item) {
  return String(item?.checkCode || item?.check?.code || rawFindingId(item) || "unknown");
}

function rawFindingId(item) {
  const value = item?.findingId || item?.finding_id || item?.id || item?.kind || item?.code;
  return value === null || value === undefined || value === "" ? "" : String(value);
}

function displayCheckCode(value) {
  const text = String(value || "").replace(/\s+/g, " ").trim();
  if (text.length === 0 || text === "unknown") return "";
  return text;
}

function publicCheckCode(value) {
  const text = displayCheckCode(value);
  return /^(?:CHECK|WBC)-\d+$/u.test(text) ? text : "";
}

function stableCheckCode(value) {
  const text = String(value || "unknown");
  let hash = 0;
  for (let index = 0; index < text.length; index += 1) hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
  return `CHECK-${String(hash % 10000).padStart(4, "0")}`;
}

function safeUserText(value) {
  const text = String(value || "").replace(/\s+/g, " ").trim();
  if (text.length === 0 || internalTextPattern.test(text)) return "";
  return text;
}

function safeTitleText(value) {
  const text = String(value || "").replace(/\s+/g, " ").trim();
  if (text.length === 0 || text === "未登记监测项") return "";
  return text;
}

function detailSummaryText(detail) {
  return detailSummaryRows(detail).map((row) => `${row.label}: ${row.value}`).join("\n");
}

function detailSummaryRows(detail) {
  if (!detail) return [{ key: "loading", label: "状态", value: "加载详情中" }];
  if (detail.ok === false) return [{ key: "error", label: "状态", value: detail.error || "详情不可用" }];
  const run = detail.run || {};
  const summary = detail.summary || {};
  const artifacts = detail.artifacts || {};
  const counts = run.severityCounts || {};
  const rows = [
    { key: "run", label: "运行", value: shortId(run.id || run.runId || detail.runId) },
    { key: "scenario", label: "场景", value: run.scenarioId || run.scenario_id || "-" },
    { key: "status", label: "状态", value: run.status || "-" },
    { key: "duration", label: "运行分钟", value: runDurationText(run) },
    { key: "timing", label: "周期/上限", value: runTimingConfigText(run) },
    { key: "durationSource", label: "计时来源", value: runTimingSourceText(run) },
    { key: "checks", label: "监测项类型", value: String(findingCount(run)) },
    { key: "alertSamples", label: "错误/告警样本", value: String(alertSampleCount(run)) },
    { key: "allSamples", label: "全部样本", value: String(findingSampleCount(run)) },
    { key: "error", label: "错误样本", value: String(redCount({ severityCounts: counts })) },
    { key: "warning", label: "告警样本", value: String(warningCount({ severityCounts: counts })) },
    { key: "observer", label: "Observer", value: run.observerId || run.observer_id || "-" },
    { key: "updated", label: "更新时间", value: formatAbsoluteDate(run.updatedAt || run.updated_at || run.createdAt || run.created_at) },
    { key: "report", label: "报告", value: shortHash(artifacts.reportJsonSha256 || run.reportJsonSha256 || run.report_json_sha256 || "") || "-" },
  ];
  const windowStart = summary.analysisWindow?.start || summary.window?.start;
  const windowEnd = summary.analysisWindow?.end || summary.window?.end;
  if (windowStart || windowEnd) rows.push({ key: "window", label: "窗口", value: `${formatDate(windowStart)} - ${formatDate(windowEnd)}` });
  return rows;
}

function commandSummary(detail) {
  const commands = detail?.commands || {};
  const lines = Object.entries(commands).map(([key, value]) => `${key}: ${value}`);
  return lines.length > 0 ? lines.join("\n") : "暂无命令";
}

function statusLabel(status) {
  const value = String(status || "");
  if (value === "blocked") return "阻塞";
  if (value === "degraded") return "降级";
  if (value === "warning") return "告警";
  if (value === "healthy") return "健康";
  return "空闲";
}

function number(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function optionalNumber(...values) {
  for (const value of values) {
    if (value === null || value === undefined || value === "") continue;
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return null;
}
