feat: add web sentinel dashboard frontend shell

This commit is contained in:
Codex
2026-06-26 02:57:41 +00:00
parent 2f1911ee35
commit 05b998c326
5 changed files with 1023 additions and 22 deletions
+6 -20
View File
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { Database } from "bun:sqlite";
import { rootPath } from "./config";
import { renderWebProbeSentinelDashboardHtml, webProbeSentinelDashboardAssetResponse } from "./hwlab-node-web-sentinel-dashboard-assets";
import { webProbeSentinelConfigPlan, type WebProbeSentinelConfigPlan } from "./hwlab-node-web-sentinel-config";
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
@@ -249,7 +250,7 @@ export function createWebProbeSentinelService(options: WebProbeSentinelServiceOp
return renderMetrics(config, db, this.health(), this.maintenance());
},
dashboardHtml() {
return renderDashboard(config, this.status());
return renderWebProbeSentinelDashboardHtml(config);
},
async fetch(request: Request) {
return sentinelFetch(service, request);
@@ -312,6 +313,10 @@ async function sentinelFetch(service: WebProbeSentinelService, request: Request)
if (request.method === "GET" && url.pathname === "/metrics") {
return new Response(service.metrics(), { headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" } });
}
if (request.method === "GET" && url.pathname.startsWith("/dashboard/assets/")) {
const asset = webProbeSentinelDashboardAssetResponse(url.pathname);
if (asset !== null) return asset;
}
if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
return new Response(service.dashboardHtml(), { headers: { "content-type": "text/html; charset=utf-8" } });
}
@@ -479,21 +484,6 @@ function renderMetrics(config: WebProbeSentinelServiceConfig, db: Database, heal
return `${lines.join("\n")}\n`;
}
function renderDashboard(config: WebProbeSentinelServiceConfig, status: Record<string, unknown>): string {
const runs = Array.isArray(status.latestRuns) ? status.latestRuns.map(record) : [];
const rows = runs.map((run) => `<tr><td>${escapeHtml(stringOrNull(run.id) ?? "-")}</td><td>${escapeHtml(stringOrNull(run.scenario_id) ?? "-")}</td><td>${escapeHtml(stringOrNull(run.status) ?? "-")}</td><td>${escapeHtml(stringOrNull(run.report_json_sha256) ?? "-")}</td><td>${escapeHtml(String(run.finding_count ?? 0))}</td></tr>`).join("");
return `<!doctype html>
<html><head><meta charset="utf-8"><title>HWLAB Web Probe Sentinel</title>
<style>body{font-family:system-ui,sans-serif;margin:24px;color:#18202a}table{border-collapse:collapse;width:100%}td,th{border:1px solid #d7dde5;padding:6px;text-align:left}code{background:#f2f4f7;padding:2px 4px}</style></head>
<body>
<h1>HWLAB Web Probe Sentinel</h1>
<p><code>${escapeHtml(config.node)}</code> / <code>${escapeHtml(config.lane)}</code> configReady=${config.plan.ok ? "true" : "false"}</p>
<p>Dashboard is redacted: prompt text, assistant body, cookies, tokens, API keys, provider payload, and stdout/stderr are not displayed.</p>
<h2>Latest Runs</h2>
<table><thead><tr><th>Run</th><th>Scenario</th><th>Status</th><th>Report SHA</th><th>Findings</th></tr></thead><tbody>${rows}</tbody></table>
</body></html>`;
}
function readConfigRefTarget(ref: string): unknown {
const [file, path] = ref.split("#");
if (file === undefined || path === undefined) throw new Error(`invalid configRef: ${ref}`);
@@ -1270,7 +1260,3 @@ function nowIso(): string {
function metricLabel(value: string): string {
return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"').replace(/\n/gu, "\\n");
}
function escapeHtml(value: string): string {
return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;");
}