Trace: move verification badge into header

This commit is contained in:
danny 2025-12-28 06:32:59 +00:00
parent 8b1cb0724a
commit bac86571a8

View file

@ -81,7 +81,7 @@ markdown.renderer.rules.fence = (tokens, idx, options, env, self) => {
return self.renderToken(tokens, idx, options);
};
function renderMarkdownPage({ title, html, topLinksHtml }) {
function renderMarkdownPage({ title, headerHtml, html, topLinksHtml }) {
return [
"<!doctype html>",
"<html><head>",
@ -99,6 +99,7 @@ function renderMarkdownPage({ title, html, topLinksHtml }) {
"blockquote{border-left:4px solid #ddd;margin:16px 0;padding:2px 14px;color:#444}",
"table{border-collapse:collapse;width:100%;margin:14px 0}",
"th,td{border:1px solid #ddd;padding:8px;vertical-align:top}",
".page-header{margin:0 0 18px 0}",
".topbar{display:flex;gap:14px;flex-wrap:wrap;margin:0 0 18px 0;font-size:14px}",
".topbar code{padding:2px 6px}",
".badge{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid transparent;font-weight:700;letter-spacing:.02em}",
@ -106,8 +107,16 @@ function renderMarkdownPage({ title, html, topLinksHtml }) {
".badge.ok{background:#e6f4ea;border-color:#b7dfc1;color:#137333}",
".badge.warn{background:#fff7e0;border-color:#ffe0a3;color:#8b5a00}",
".badge.fail{background:#fce8e6;border-color:#f6b8b6;color:#a50e0e}",
".trace-header{display:flex;flex-direction:column;gap:10px;margin:0 0 18px 0;padding:14px 14px;border:1px solid #eee;border-radius:14px;background:#fff}",
".trace-header-title{display:flex;align-items:center;gap:12px;flex-wrap:wrap}",
".trace-header-title h1{margin:0;font-size:24px}",
".trace-meta{display:flex;flex-wrap:wrap;gap:10px;font-size:14px;color:#333}",
".trace-meta code{padding:2px 6px}",
".trace-checks{margin:0;padding-left:18px;color:#333;font-size:14px}",
".trace-checks li{margin:2px 0}",
"</style>",
"</head><body>",
headerHtml ? `<header class="page-header">${headerHtml}</header>` : "",
topLinksHtml ? `<div class="topbar">${topLinksHtml}</div>` : "",
`<main class="markdown-body">${html}</main>`,
"<script type='module'>",
@ -131,6 +140,45 @@ function renderVerificationBadgeHtml(verification) {
return `<span class="badge ${cls}">${safeLabel}${safeDetail ? ` <small>${safeDetail}</small>` : ""}</span>`;
}
function renderTraceHeaderHtml({ verification, job }) {
const badge = renderVerificationBadgeHtml(verification);
const createdAt = job?.createdAt ? String(job.createdAt) : "";
const traceId = String(job?.id || "");
const style = String(job?.style || "");
const checks = verification?.checks || {};
const outputOk = checks.outputOk === true;
const sourceOk = checks.sourceOk;
const warningsPresent = checks.warningsPresent === true;
const outputLabel = outputOk ? "PASS" : "FAIL";
const sourceLabel = sourceOk === true ? "PASS" : sourceOk === false ? "FAIL" : "UNKNOWN";
const safeCreatedAt = createdAt ? escapeHtml(createdAt) : "";
const safeTraceId = traceId ? escapeHtml(traceId) : "";
const safeStyle = style ? escapeHtml(style) : "";
const metaParts = [
safeCreatedAt ? `<span>Generated (UTC): <code>${safeCreatedAt}</code></span>` : "",
safeTraceId ? `<span>Trace ID: <code>${safeTraceId}</code></span>` : "",
safeStyle ? `<span>Style: <code>${safeStyle}</code></span>` : "",
].filter(Boolean);
return [
`<div class="trace-header">`,
` <div class="trace-header-title">${badge}<h1>IF.TTT trace</h1></div>`,
metaParts.length ? ` <div class="trace-meta">${metaParts.join(" · ")}</div>` : "",
` <ul class="trace-checks">`,
` <li>Output hash check: <strong>${escapeHtml(outputLabel)}</strong></li>`,
` <li>Source hash check: <strong>${escapeHtml(sourceLabel)}</strong></li>`,
` <li>Quality warnings: <strong>${warningsPresent ? "present" : "none recorded"}</strong></li>`,
` </ul>`,
`</div>`,
]
.filter(Boolean)
.join("");
}
async function computeVerificationStatus({ job, projectRoot, outputsDir, uploadsDir }) {
const expectedOutput = String(job?.outputSha256 || "").trim();
const expectedSource = String(job?.sourceSha256 || "").trim();
@ -207,10 +255,6 @@ function renderTraceMarkdown({ shareId, job, publicBaseUrl, staticPublicBaseUrl
const bases = staticMirrorBaseUrls(primaryBase, publicBaseUrl);
const mirrorBase = bases.find((b) => b !== primaryBase) || "";
const verificationStatus = String(job?._verification?.status || "").toUpperCase() || "UNKNOWN";
const verificationDetail = String(job?._verification?.detail || "").trim();
const verificationChecks = job?._verification?.checks || {};
const createdAt = job?.createdAt ? String(job.createdAt) : "";
const status = job?.status ? String(job.status) : "";
const warningsPresent = Boolean(job?.warnings && String(job.warnings).trim());
@ -235,15 +279,7 @@ function renderTraceMarkdown({ shareId, job, publicBaseUrl, staticPublicBaseUrl
const lastResortDownloadUrl = lastResortBase ? `${lastResortBase}/r/${encodeURIComponent(shareId)}/download` : "";
const lines = [
"# IF.TTT trace (public evidence view)",
"",
"## Trace verification status",
"",
`**${verificationStatus}**${verificationDetail ? `${verificationDetail}` : ""}`,
"",
"- Output hash check: " + (verificationChecks.outputOk ? "**PASS**" : "**FAIL**"),
"- Source hash check: " + (verificationChecks.sourceOk === true ? "**PASS**" : verificationChecks.sourceOk === false ? "**FAIL**" : "**UNKNOWN**"),
"- Quality warnings: " + (warningsPresent ? "**present**" : "none recorded"),
"## IF.TTT trace (public evidence view)",
"",
"IF.TTT (Traceable, Transparent, Trustworthy) is InfraFabrics chain-of-custody protocol: it binds the **source fingerprint** to the **generated output fingerprint**, so a skeptical reader can verify what was produced and from which input, without needing access to internal systems.",
"This page is intentionally scoped to **one dossier only** (no index, no directory listing).",
@ -1018,9 +1054,8 @@ function main() {
const md = renderTraceMarkdown({ shareId, job: jobForRender, publicBaseUrl, staticPublicBaseUrl });
const html = markdown.render(md);
const badge = renderVerificationBadgeHtml(verification);
const headerHtml = renderTraceHeaderHtml({ verification, job: jobForRender });
const topLinks = [
badge,
`<a href="/r/${encodeURIComponent(shareId)}">Back to dossier</a>`,
`<a href="/r/${encodeURIComponent(shareId)}/download">Download Markdown</a>`,
job.sourcePath ? `<a href="/r/${encodeURIComponent(shareId)}/source">Download source</a>` : "",
@ -1032,7 +1067,7 @@ function main() {
res
.status(200)
.type("text/html; charset=utf-8")
.send(renderMarkdownPage({ title: "IF.TTT trace", html, topLinksHtml: topLinks }));
.send(renderMarkdownPage({ title: "IF.TTT trace", headerHtml, html, topLinksHtml: topLinks }));
});
app.get("/r/:shareId/source", (req, res) => {