114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
// 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.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p10-monitor-web-aggregation.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
|
|
// Responsibility: Static dashboard shell and asset serving for the web-probe sentinel frontend.
|
|
import { readFileSync } from "node:fs";
|
|
import { rootPath } from "./config";
|
|
|
|
interface DashboardShellConfig {
|
|
readonly node: string;
|
|
readonly lane: string;
|
|
readonly sentinelId: string;
|
|
readonly plan: { readonly ok: boolean; readonly sentinels?: readonly Record<string, unknown>[] };
|
|
readonly publicExposure: Record<string, unknown>;
|
|
}
|
|
|
|
const DASHBOARD_ASSET_ROOT = "scripts/assets/web-probe-sentinel-dashboard";
|
|
const MONITOR_WEB_ASSET_ROOT = "scripts/assets/web-probe-sentinel-monitor-web";
|
|
const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-27-p11-monitor-web-observability-dashboard";
|
|
|
|
export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig): string {
|
|
const publicOrigin = stringOrNull(config.publicExposure.publicBaseUrl) ?? "";
|
|
const basePath = publicBasePath(publicOrigin);
|
|
const sentinels = sentinelRegistryRows(config);
|
|
const bootstrap = {
|
|
node: config.node,
|
|
lane: config.lane,
|
|
sentinelId: config.sentinelId,
|
|
publicOrigin,
|
|
basePath,
|
|
configReady: config.plan.ok,
|
|
contractVersion: DASHBOARD_CONTRACT_VERSION,
|
|
sentinels,
|
|
valuesRedacted: true,
|
|
};
|
|
return `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>HWLAB Web哨兵 monitor-web</title>
|
|
<link rel="stylesheet" href="${escapeAttr(basePath)}/monitor-web/assets/monitor-web.css">
|
|
</head>
|
|
<body>
|
|
<main
|
|
id="monitor-web-root"
|
|
data-node="${escapeAttr(config.node)}"
|
|
data-lane="${escapeAttr(config.lane)}"
|
|
data-sentinel-id="${escapeAttr(config.sentinelId)}"
|
|
data-base-path="${escapeAttr(basePath)}"
|
|
data-public-origin="${escapeAttr(publicOrigin)}"
|
|
data-config-ready="${config.plan.ok ? "true" : "false"}"
|
|
data-contract-version="${DASHBOARD_CONTRACT_VERSION}"
|
|
data-monitor-ready="false"
|
|
>
|
|
加载 monitor-web...
|
|
</main>
|
|
<script id="monitor-web-bootstrap" type="application/json">${jsonForScript(bootstrap)}</script>
|
|
<script type="module" src="${escapeAttr(basePath)}/monitor-web/assets/monitor-web.js"></script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function sentinelRegistryRows(config: DashboardShellConfig): Array<{ readonly id: string; readonly enabled: boolean }> {
|
|
return Array.isArray(config.plan.sentinels) ? config.plan.sentinels.map((item) => ({
|
|
id: stringOrNull(item.id) ?? "",
|
|
enabled: item.enabled !== false,
|
|
})).filter((item) => item.id.length > 0) : [];
|
|
}
|
|
|
|
function publicBasePath(publicBaseUrl: string): string {
|
|
try {
|
|
const path = new URL(publicBaseUrl).pathname.replace(/\/+$/u, "");
|
|
return path === "/" ? "" : path;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
export function webProbeSentinelDashboardAssetResponse(pathname: string): Response | null {
|
|
if (pathname === "/dashboard/assets/dashboard.css") return textAsset(`${DASHBOARD_ASSET_ROOT}/dashboard.css`, "text/css; charset=utf-8");
|
|
if (pathname === "/dashboard/assets/dashboard.js") return textAsset(`${DASHBOARD_ASSET_ROOT}/dashboard.js`, "application/javascript; charset=utf-8");
|
|
if (pathname === "/monitor-web/assets/monitor-web.css") return textAsset(`${MONITOR_WEB_ASSET_ROOT}/monitor-web.css`, "text/css; charset=utf-8");
|
|
if (pathname === "/monitor-web/assets/monitor-web.js") return textAsset(`${MONITOR_WEB_ASSET_ROOT}/monitor-web.js`, "application/javascript; charset=utf-8");
|
|
if (pathname === "/monitor-web/assets/vendor/vue.esm-browser.prod.js") return textAsset(`${MONITOR_WEB_ASSET_ROOT}/vendor/vue.esm-browser.prod.js`, "application/javascript; charset=utf-8");
|
|
if (pathname === "/monitor-web/assets/vendor/VUE-LICENSE") return textAsset(`${MONITOR_WEB_ASSET_ROOT}/vendor/VUE-LICENSE`, "text/plain; charset=utf-8");
|
|
return null;
|
|
}
|
|
|
|
function textAsset(path: string, contentType: string): Response {
|
|
return new Response(readFileSync(rootPath(path), "utf8"), {
|
|
headers: {
|
|
"cache-control": "no-store",
|
|
"content-type": contentType,
|
|
},
|
|
});
|
|
}
|
|
|
|
function stringOrNull(value: unknown): string | null {
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """);
|
|
}
|
|
|
|
function escapeAttr(value: string): string {
|
|
return escapeHtml(value).replace(/'/gu, "'");
|
|
}
|
|
|
|
function jsonForScript(value: unknown): string {
|
|
return JSON.stringify(value).replace(/</gu, "\\u003c");
|
|
}
|