feat: add web sentinel dashboard frontend shell
This commit is contained in:
@@ -1245,12 +1245,14 @@ function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbe
|
||||
const metrics = callSentinelService(state, "GET", "/metrics", null, options.timeoutSeconds);
|
||||
const report = callSentinelService(state, "GET", "/api/report?view=summary", null, options.timeoutSeconds);
|
||||
const publicExposure = probeSentinelPublicExposure(state, options.timeoutSeconds);
|
||||
const publicDashboard = probeSentinelPublicDashboard(state, options.timeoutSeconds);
|
||||
const ok = health.ok
|
||||
&& record(health.bodyJson).ok === true
|
||||
&& metrics.ok
|
||||
&& metricNames(record(metrics).bodyTextPreview).includes("web_probe_sentinel_health")
|
||||
&& report.ok
|
||||
&& publicExposure.ok === true
|
||||
&& publicDashboard.ok === true
|
||||
&& (quickVerify === null || quickVerify.ok === true);
|
||||
const result = {
|
||||
ok,
|
||||
@@ -1262,8 +1264,9 @@ function runSentinelValidate(state: SentinelCicdState, options: Extract<WebProbe
|
||||
metrics,
|
||||
report,
|
||||
publicExposure,
|
||||
publicDashboard,
|
||||
quickVerify,
|
||||
blocker: ok ? null : validationBlocker(health, metrics, report, publicExposure, quickVerify),
|
||||
blocker: ok ? null : validationBlocker(health, metrics, report, publicExposure, publicDashboard, quickVerify),
|
||||
next: sentinelP5Next(state),
|
||||
valuesRedacted: true,
|
||||
};
|
||||
@@ -1618,6 +1621,38 @@ function probeSentinelPublicExposure(state: SentinelCicdState, timeoutSeconds: n
|
||||
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true };
|
||||
}
|
||||
|
||||
function probeSentinelPublicDashboard(state: SentinelCicdState, timeoutSeconds: number): Record<string, unknown> {
|
||||
const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl").replace(/\/$/u, "");
|
||||
const rootUrl = `${publicBaseUrl}/`;
|
||||
const cssUrl = `${publicBaseUrl}/dashboard/assets/dashboard.css`;
|
||||
const jsUrl = `${publicBaseUrl}/dashboard/assets/dashboard.js`;
|
||||
const script = [
|
||||
"set +e",
|
||||
`root_url=${shellQuote(rootUrl)}`,
|
||||
`css_url=${shellQuote(cssUrl)}`,
|
||||
`js_url=${shellQuote(jsUrl)}`,
|
||||
"root_body=$(mktemp)",
|
||||
"css_body=$(mktemp)",
|
||||
"js_body=$(mktemp)",
|
||||
"root_code=$(curl -sS -L --connect-timeout 8 --max-time 20 -o \"$root_body\" --write-out '%{http_code}' \"$root_url\" 2>/tmp/web-probe-sentinel-dashboard-root.err); root_rc=$?",
|
||||
"css_code=$(curl -sS -L --connect-timeout 8 --max-time 20 -o \"$css_body\" --write-out '%{http_code}' \"$css_url\" 2>/tmp/web-probe-sentinel-dashboard-css.err); css_rc=$?",
|
||||
"js_code=$(curl -sS -L --connect-timeout 8 --max-time 20 -o \"$js_body\" --write-out '%{http_code}' \"$js_url\" 2>/tmp/web-probe-sentinel-dashboard-js.err); js_rc=$?",
|
||||
"node - \"$root_url\" \"$css_url\" \"$js_url\" \"$root_code\" \"$root_rc\" \"$css_code\" \"$css_rc\" \"$js_code\" \"$js_rc\" \"$root_body\" \"$css_body\" \"$js_body\" <<'NODE'",
|
||||
"const fs=require('node:fs');",
|
||||
"const [rootUrl,cssUrl,jsUrl,rootCode,rootRc,cssCode,cssRc,jsCode,jsRc,rootPath,cssPath,jsPath]=process.argv.slice(2);",
|
||||
"function read(path){try{return fs.readFileSync(path,'utf8')}catch{return ''}}",
|
||||
"const root=read(rootPath); const css=read(cssPath); const js=read(jsPath);",
|
||||
"const rootOk=Number(rootRc)===0&&Number(rootCode)>=200&&Number(rootCode)<300&&root.includes('id=\"sentinel-dashboard\"')&&root.includes('/dashboard/assets/dashboard.js');",
|
||||
"const cssOk=Number(cssRc)===0&&Number(cssCode)>=200&&Number(cssCode)<300&&css.includes('sentinel-shell')&&css.length>1000;",
|
||||
"const jsOk=Number(jsRc)===0&&Number(jsCode)>=200&&Number(jsCode)<300&&js.includes('createAutoRefresh')&&js.includes('/api/overview')&&js.length>1000;",
|
||||
"console.log(JSON.stringify({ok:rootOk&&cssOk&&jsOk,root:{url:rootUrl,httpStatus:Number(rootCode),bytes:Buffer.byteLength(root),shell:root.includes('id=\"sentinel-dashboard\"')},css:{url:cssUrl,httpStatus:Number(cssCode),bytes:Buffer.byteLength(css),shell:css.includes('sentinel-shell')},js:{url:jsUrl,httpStatus:Number(jsCode),bytes:Buffer.byteLength(js),apiClient:js.includes('/api/overview'),autoRefresh:js.includes('createAutoRefresh')},valuesRedacted:true}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
const result = runCommand(["bash", "-lc", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 30) * 1000 });
|
||||
const parsed = parseJsonObject(result.stdout);
|
||||
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true };
|
||||
}
|
||||
|
||||
function applySentinelPublicExposure(state: SentinelCicdState, timeoutSeconds: number): Record<string, unknown> {
|
||||
const material = readSentinelFrpcMaterial(state);
|
||||
if (!material.ok) return { ok: false, hostname: stringAt(state.publicExposure, "hostname"), material, valuesRedacted: true };
|
||||
@@ -2109,12 +2144,13 @@ function metricNames(textValue: unknown): string[] {
|
||||
return textValue.split(/\r?\n/u).map((line) => /^([A-Za-z_:][A-Za-z0-9_:]*)/u.exec(line)?.[1]).filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
|
||||
function validationBlocker(health: Record<string, unknown>, metrics: Record<string, unknown>, report: Record<string, unknown>, publicExposure: Record<string, unknown>, quickVerify: Record<string, unknown> | null): Record<string, unknown> {
|
||||
function validationBlocker(health: Record<string, unknown>, metrics: Record<string, unknown>, report: Record<string, unknown>, publicExposure: Record<string, unknown>, publicDashboard: Record<string, unknown>, quickVerify: Record<string, unknown> | null): Record<string, unknown> {
|
||||
const blockers = [];
|
||||
if (!health.ok || record(health.bodyJson).ok !== true) blockers.push("health");
|
||||
if (!metrics.ok || !metricNames(metrics.bodyTextPreview).includes("web_probe_sentinel_health")) blockers.push("metrics");
|
||||
if (!report.ok) blockers.push("recent-report");
|
||||
if (publicExposure.ok !== true) blockers.push("public-exposure");
|
||||
if (publicDashboard.ok !== true) blockers.push("public-dashboard");
|
||||
if (quickVerify !== null && quickVerify.ok !== true) blockers.push("quick-verify");
|
||||
return { code: "sentinel-validation-failed", blockers, valuesRedacted: true };
|
||||
}
|
||||
@@ -2232,6 +2268,7 @@ function renderValidateResult(result: Record<string, unknown>): string {
|
||||
const metrics = record(result.metrics);
|
||||
const report = record(result.report);
|
||||
const publicExposure = record(result.publicExposure);
|
||||
const publicDashboard = record(result.publicDashboard);
|
||||
const quickVerify = record(result.quickVerify);
|
||||
const blocker = record(result.blocker);
|
||||
const next = record(result.next);
|
||||
@@ -2246,6 +2283,7 @@ function renderValidateResult(result: Record<string, unknown>): string {
|
||||
["metrics", metrics.ok && metricNames(metrics.bodyTextPreview).includes("web_probe_sentinel_health"), `bytes=${metrics.bodyBytes ?? "-"} metric=web_probe_sentinel_health`],
|
||||
["recent-report", report.ok, `${record(record(report.bodyJson).run).id ?? "-"} ${short(record(record(report.bodyJson).run).report_json_sha256)}`],
|
||||
["public-exposure", publicExposure.ok, `${record(publicExposure.dns).expectedA ?? "-"} http=${record(publicExposure.https).httpStatus ?? "-"}`],
|
||||
["public-dashboard", publicDashboard.ok, `${record(publicDashboard.root).url ?? "-"} root=${record(publicDashboard.root).httpStatus ?? "-"} css=${record(publicDashboard.css).httpStatus ?? "-"} js=${record(publicDashboard.js).httpStatus ?? "-"}`],
|
||||
["quick-verify", Object.keys(quickVerify).length === 0 ? "skipped" : quickVerify.ok, `${quickVerify.runId ?? "-"} ${short(quickVerify.reportJsonSha256)}`],
|
||||
]),
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user