feat(web-probe): add multi-sentinel registry

This commit is contained in:
Codex
2026-06-26 12:42:04 +00:00
parent 7b3df965cc
commit 4e0f1cba21
25 changed files with 1038 additions and 140 deletions
@@ -1,4 +1,5 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: Browser-side API client, formatting, auto refresh, and base dashboard rendering.
// Desktop view redesign (issue #1025): 三栏 master-detail、低噪声、渐进披露、排查直觉化。
// 保持纯 vanilla JS + 原生 CSS,无框架、无构建步骤,所有渲染通过 innerHTML 拼接。
@@ -15,6 +16,7 @@
*/
const root = document.getElementById("sentinel-dashboard");
const basePath = normalizeBasePath(root?.dataset?.basePath || "");
const refs = {
statusPill: document.getElementById("status-pill"),
loadingBanner: document.getElementById("loading-banner"),
@@ -178,21 +180,31 @@ loadDashboard({ silent: false }).catch((error) => renderError(error));
function createDashboardApi() {
return {
/** @returns {Promise<SentinelOverview>} */
overview: () => getJson("/api/overview"),
overview: () => getJson(apiPath("/api/overview")),
/** @returns {Promise<SentinelRunsResponse>} */
runs: (filters) => getJson(`/api/runs?${runsQuery(filters)}`),
runs: (filters) => getJson(apiPath(`/api/runs?${runsQuery(filters)}`)),
/** @returns {Promise<SentinelFindingsResponse>} */
findings: (filters) => getJson(`/api/findings?${findingsQuery(filters)}`),
findings: (filters) => getJson(apiPath(`/api/findings?${findingsQuery(filters)}`)),
/** @returns {Promise<SentinelRunDetail>} */
runDetail: (runId) => getJson(`/api/runs/${encodeURIComponent(runId)}`),
runDetail: (runId) => getJson(apiPath(`/api/runs/${encodeURIComponent(runId)}`)),
runViews: (runId, view = null) => {
const query = new URLSearchParams({ maxBytes: "24000" });
if (view) query.set("view", view);
return getJson(`/api/runs/${encodeURIComponent(runId)}/views?${query.toString()}`);
return getJson(apiPath(`/api/runs/${encodeURIComponent(runId)}/views?${query.toString()}`));
},
};
}
function apiPath(path) {
return `${basePath}${path}`;
}
function normalizeBasePath(value) {
const text = String(value || "").replace(/\/+$/u, "");
if (!text || text === "/") return "";
return text.startsWith("/") ? text : `/${text}`;
}
async function getJson(path) {
const response = await fetch(path, { headers: { accept: "application/json" } });
const body = await response.json().catch(() => ({}));