feat: add web sentinel dashboard frontend shell
This commit is contained in:
@@ -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, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user