= {
pass: 'badge badge-pass',
fail: 'badge badge-fail',
skip: 'badge badge-skip',
indeterminate: 'badge badge-ind',
};
function apiRowStatus(r: ApiRunResult): { key: string; label: string } {
if (!r.executed) return { key: 'skip', label: `skipped (${r.reason ?? '—'})` };
return r.ok ? { key: 'pass', label: 'pass' } : { key: 'fail', label: 'FAIL' };
}
function uiRowStatus(r: UiRunResult): { key: string; label: string } {
if (!r.executed) return { key: 'skip', label: `skipped (${r.reason ?? '—'})` };
if (r.actual === 'indeterminate') return { key: 'indeterminate', label: 'indeterminate' };
return r.ok ? { key: 'pass', label: 'pass' } : { key: 'fail', label: 'FAIL' };
}
function durationCell(ms: number, warnMs: number, slowMs: number): string {
const band = classifyDuration(ms, warnMs, slowMs);
return `${ms} ms `;
}
/** A "no metric here" cell (write-flow rows are not page observations). */
const dashCell = '— ';
/** Human run duration: "850 ms", "42s", "4m 12s", "1h 03m". */
export function formatDurationMs(ms: number): string {
if (ms < 1000) return `${ms} ms`;
const totalSeconds = Math.round(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const totalMinutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (totalMinutes < 60) return `${totalMinutes}m ${String(seconds).padStart(2, '0')}s`;
const hours = Math.floor(totalMinutes / 60);
return `${hours}h ${String(totalMinutes % 60).padStart(2, '0')}m`;
}
function kpiCard(label: string, value: string, sub = ''): string {
return `${value}
${label}
${
sub ? `
${sub}
` : ''
}
`;
}
// ── Executive summary ───────────────────────────────────────────────────────
function coverageCard(axis: string, cov: { executed: number; skipped: number; skipReasons: { reason: string; count: number }[] }): string {
const chips = cov.skipReasons
.map((s) => `${e(s.reason)} × ${s.count} `)
.join(' ');
return `${e(axis)} coverage ${cov.executed} executed · ${cov.skipped} skipped ${
chips ? `
${chips}
` : ''
}
`;
}
function renderExecSummary(model: ReportModel): string {
const api = model.api;
const ui = model.ui;
const charts: string[] = [];
if (api) {
const segments: DonutSegment[] = [
{ label: 'passed', count: api.passed, cssClass: 'seg-pass' },
{ label: 'failed', count: api.failed, cssClass: 'seg-fail' },
{ label: 'skipped', count: api.skipped, cssClass: 'seg-skip' },
];
const rate = api.executed > 0 ? `${Math.round((api.passed / api.executed) * 100)}%` : '—';
charts.push(`API checks ${donutSvg(segments, rate, 'API checks')} `);
}
if (ui) {
const determinate = ui.executed - ui.indeterminate;
const segments: DonutSegment[] = [
{ label: 'passed', count: ui.passed, cssClass: 'seg-pass' },
{ label: 'failed', count: ui.failed, cssClass: 'seg-fail' },
{ label: 'indeterminate', count: ui.indeterminate, cssClass: 'seg-ind' },
{ label: 'skipped', count: ui.skipped, cssClass: 'seg-skip' },
];
const rate = determinate > 0 ? `${Math.round((ui.passed / determinate) * 100)}%` : '—';
charts.push(`UI checks ${donutSvg(segments, rate, 'UI checks')} `);
}
if (model.latency.api) {
const bands: BandBar[] = model.latency.api.map((b) => ({ label: b.label, count: b.count, cssClass: `band-${b.band}` }));
charts.push(`API latency ${barsSvg(bands, 'API latency distribution')} `);
}
if (model.latency.ui) {
const bands: BandBar[] = model.latency.ui.map((b) => ({ label: b.label, count: b.count, cssClass: `band-${b.band}` }));
charts.push(`Page readiness ${barsSvg(bands, 'Page readiness distribution')} `);
}
const covCards: string[] = [];
if (model.coverage.api) covCards.push(coverageCard('API', model.coverage.api));
if (model.coverage.ui) covCards.push(coverageCard('UI', model.coverage.ui));
const passed = (api?.passed ?? 0) + (ui?.passed ?? 0);
const failed = (api?.failed ?? 0) + (ui?.failed ?? 0);
const skipped = (api?.skipped ?? 0) + (ui?.skipped ?? 0);
const subParts = [`${passed} passed`, `${failed} failed`, `${skipped} skipped`];
if (model.durationMs !== undefined) subParts.push(`run ${formatDurationMs(model.durationMs)}`);
return `
${model.verdict}
${subParts.map((p) => e(p)).join(' · ')}
${charts.join('')}
${covCards.length ? `${covCards.join('')}
` : ''}
`;
}
// ── Failures first ──────────────────────────────────────────────────────────
function apiFailureRow(r: ApiRunResult): string {
const expected = r.mode === 'authz_only' ? 'authz gate' : String(r.expected);
const details = [r.note, r.error].filter(Boolean).map(String);
const excerpt = r.bodyExcerpt
? `response ${e(r.bodyExcerpt)} `
: '';
return `
API
${e(r.method)} ${e(r.route)}
${e(r.role)}
${r.permission ? e(r.permission) : '— '}
${e(expected)} → ${r.actual ?? '—'}
${details.map((d) => e(d)).join(' · ')}${excerpt}
`;
}
function uiFailureRow(r: UiRunResult): string {
const details = [...r.warnings, r.error, r.flowDetail].filter(Boolean).map(String);
return `
UI
${e(r.routeId)}${r.kind !== 'page' ? ` ${e(r.kind)} ` : ''}
${e(r.role)}
${r.permission ? e(r.permission) : '— '}
${e(r.expected)} → ${e(r.actual)}
${details.map((d) => e(d)).join(' · ')}
`;
}
function renderFailuresFirst(model: ReportModel): string {
const totalFailed = (model.api?.failed ?? 0) + (model.ui?.failed ?? 0);
if (totalFailed === 0) {
return `Failures All green — no failed check in this run.
`;
}
const groups = model.failureGroups
.map((g: FailureGroup) => {
const count = g.api.length + g.ui.length;
const rows = [...g.api.map(apiFailureRow), ...g.ui.map(uiFailureRow)].join('\n');
return `
${e(g.label)} ${count}
Axis Unit Role Permission Expected → actual Detail
${rows}
`;
})
.join('\n');
return `
Failures first ${totalFailed} failed check${totalFailed > 1 ? 's' : ''}, grouped by cause
${groups}
`;
}
function renderPerfAdvisories(model: ReportModel): string {
const { api, ui } = model.perfOffenders;
const count = api.length + ui.length;
if (count === 0) return '';
const rows = [
...api.map(
(r) =>
`API ${e(r.method)} ${e(r.route)} ${e(r.role)} ${durationCell(r.durationMs, model.thresholds.apiWarnMs, model.thresholds.apiSlowMs)} `,
),
...ui.map(
(r) =>
`UI ${e(r.routeId)} ${e(r.role)} ${durationCell(r.perf.fullyReadyMs, model.thresholds.uiWarnMs, model.thresholds.uiSlowMs)} `,
),
].join('\n');
return `
Performance advisories ${count} over the warn budget (never a failure)
Axis Unit Role Duration
${rows}
`;
}
// ── RBAC matrices ───────────────────────────────────────────────────────────
function matrixCellHtml(cell: MatrixCell, role: string): string {
switch (cell.kind) {
case 'pass':
return `✓ `;
case 'fail':
return `✗ `;
case 'skip':
return `– `;
case 'absent':
return `· `;
}
}
function renderMatrix(matrix: RbacMatrix, title: string, unitHeader: string): string {
const roleHeads = matrix.roles.map((r) => `${e(r)} `).join('');
const groups = matrix.groups
.map((g) => {
const rows = g.rows
.map(
(row) =>
`${e(row.label)} ${
row.permission ? `${e(row.permission)} ` : ''
} ${row.cells.map((cell, i) => matrixCellHtml(cell, matrix.roles[i] ?? '')).join('')} `,
)
.join('\n');
return ` 0 ? ' open' : ''}>
${e(g.group)} ✓ ${g.passed} ✗ ${g.failed} – ${g.skipped}
`;
})
.join('\n');
return `
${e(title)} every cell = one check (hover for detail)
Expand all Collapse all
${groups}
`;
}
// ── Axis tables ─────────────────────────────────────────────────────────────
function renderApiSection(model: ReportModel): string {
const api = model.api;
if (!api) return 'Backend (API) No API run in this report.
';
const { apiWarnMs, apiSlowMs } = model.thresholds;
const rows = api.rows
.map((r) => {
const status = apiRowStatus(r);
const details = [r.note, r.error].filter(Boolean).map(String);
const excerpt = r.bodyExcerpt
? `response ${e(r.bodyExcerpt)} `
: '';
return `
${e(r.method)}
${e(r.route)}
${e(r.role)}
${r.permission ? e(r.permission) : '— '}
${e(r.mode)}
${r.expected}
${r.actual ?? '—'}
${e(status.label)}
${durationCell(r.durationMs, apiWarnMs, apiSlowMs)}
${humanBytes(r.sizeBytes)}
${details.length ? e(details.join(' · ')) : ''}${excerpt}
`;
})
.join('\n');
return `
Backend (API) ${api.passed}/${api.executed} passed · ${api.skipped} skipped
Method Endpoint Role Permission Mode Expected Actual Status Duration Size Notes
${rows}
`;
}
function screenshotCell(r: UiRunResult, model: ReportModel): string {
if (!r.screenshot) return ' ';
const label = `screenshot ${r.routeId} (${r.role})`;
const embed = model.screenshots[r.screenshot];
if (embed?.dataUri) {
return `📷 `;
}
const link = `📷 `;
if (embed?.note) return `${link} ${e(embed.note)} `;
return `${link} `;
}
function renderUiSection(model: ReportModel): string {
const ui = model.ui;
if (!ui) return 'Frontend (UI) No UI run in this report.
';
const { uiWarnMs, uiSlowMs } = model.thresholds;
const rows = ui.rows
.map((r) => {
const status = uiRowStatus(r);
const issues: string[] = [...r.warnings];
if (r.error) issues.push(r.error);
if (r.flowDetail) issues.push(r.flowDetail);
const errorsBlock =
r.consoleErrors.length || r.network.failed.length
? `${r.consoleErrors.length} console · ${r.network.failed.length} requests ${[
...r.consoleErrors.map((c) => `console: ${e(c)} `),
...r.network.failed.map((f) => `${f.status} ${e(f.url)} `),
].join('')} `
: '';
const isPage = r.kind === 'page';
const navCell = isPage ? durationCell(r.perf.navMs, uiWarnMs, uiSlowMs) : dashCell;
const ttfpCell =
isPage && r.perf.ttfpMs !== undefined
? `${r.perf.ttfpMs} ms `
: dashCell;
const readyCell = isPage ? durationCell(r.perf.fullyReadyMs, uiWarnMs, uiSlowMs) : dashCell;
const weightCell = isPage
? `${humanBytes(r.network.transferredBytes)} `
: dashCell;
const reqCell = isPage ? `${r.network.requestCount} ` : dashCell;
return `
${e(r.routeId)}${r.kind !== 'page' ? `${e(r.kind)} ` : ''}
${e(r.url)}
${e(r.role)}
${r.permission ? e(r.permission) : '— '}
${e(r.navigationUsed)}
${e(r.expected)}
${e(r.actual)}
${e(status.label)}
${navCell}
${ttfpCell}
${readyCell}
${weightCell}
${reqCell}
${issues.map((w) => e(w)).join(' ')}${errorsBlock}
${screenshotCell(r, model)}
`;
})
.join('\n');
return `
Frontend (UI) ${ui.passed}/${ui.executed - ui.indeterminate} passed · ${ui.indeterminate} indeterminate · ${ui.skipped} skipped
Route URL Role Permission Nav Expected Actual Status Nav ms TTFP Ready ms Weight Req Issues Shot
${rows}
`;
}
function renderRollups(model: ReportModel): string {
const roleRows = model.roleRollups
.map((r) => {
const total = r.apiPassed + r.apiFailed + r.uiPassed + r.uiFailed;
const passed = r.apiPassed + r.uiPassed;
const rate = total > 0 ? Math.round((passed / total) * 100) : null;
return `${e(r.role)} ${r.apiPassed} ${r.apiFailed} ${r.uiPassed} ${r.uiFailed} ${
rate === null ? '— ' : ``
} `;
})
.join('\n');
const groupRows = model.groupRollups
.map(
(g) =>
`${e(g.group)} ${g.apiPassed} ${g.apiFailed} ${g.uiPassed} ${g.uiFailed} `,
)
.join('\n');
return `
By role
Role API ✓ API ✗ UI ✓ UI ✗ Pass rate ${roleRows}
By module
Group API ✓ API ✗ UI ✓ UI ✗ ${groupRows}
`;
}
const STYLE = `
:root{--bg:#f6f7f9;--card:#fff;--ink:#1c2430;--muted:#68727f;--line:#e3e7ec;--pass:#16803c;--pass-bg:#e8f6ee;--fail:#b42318;--fail-bg:#fdebe9;--skip:#68727f;--skip-bg:#eef0f3;--ind:#955d00;--ind-bg:#fdf3e0;--warn:#955d00;--slow:#b42318;--accent:#2456d6}
@media (prefers-color-scheme: dark){:root{--bg:#12161c;--card:#1a2029;--ink:#e6eaef;--muted:#8b96a3;--line:#2a3341;--pass:#4ade80;--pass-bg:#12301e;--fail:#f87171;--fail-bg:#3a1715;--skip:#8b96a3;--skip-bg:#232a35;--ind:#fbbf24;--ind-bg:#33270e;--warn:#fbbf24;--slow:#f87171;--accent:#7ba3f7}}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,Segoe UI,Roboto,sans-serif;padding:24px}
h1{font-size:22px;margin:0 0 4px}h2{font-size:16px;margin:0 0 10px}section{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px;margin:14px 0;overflow-x:auto}
.meta{color:var(--muted);font-size:12.5px;display:flex;gap:14px;flex-wrap:wrap;margin-bottom:10px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}.kpi{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.kpi-v{font-size:22px;font-weight:650}.kpi-l{color:var(--muted);font-size:12px;margin-top:2px}.kpi-s{color:var(--muted);font-size:11.5px;margin-top:4px}
table.grid{border-collapse:collapse;width:100%;font-size:13px}.grid th,.grid td{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}
.grid thead th{position:sticky;top:0;background:var(--card);cursor:default;white-space:nowrap}.grid th[data-sort]{cursor:pointer}.grid th[data-sort]:hover{color:var(--accent)}
.num{text-align:right;white-space:nowrap}.mono{font-family:ui-monospace,Consolas,monospace;font-size:12.5px}.small{font-size:12px}.muted{color:var(--muted);font-weight:400;font-size:12.5px}
.badge{display:inline-block;padding:1px 8px;border-radius:99px;font-size:11.5px;font-weight:600;white-space:nowrap}
.badge-pass{color:var(--pass);background:var(--pass-bg)}.badge-fail{color:var(--fail);background:var(--fail-bg)}.badge-skip{color:var(--skip);background:var(--skip-bg)}.badge-ind{color:var(--ind);background:var(--ind-bg)}
.dur-warn{color:var(--warn);font-weight:600}.dur-slow{color:var(--slow);font-weight:700}
.flow{margin-left:6px;font-size:10.5px;color:var(--accent);border:1px solid currentColor;border-radius:4px;padding:0 4px;vertical-align:1px}
.cols{display:grid;grid-template-columns:1fr 1fr;gap:18px}@media(max-width:900px){.cols{grid-template-columns:1fr}}
.bar{position:relative;background:var(--skip-bg);border-radius:6px;height:18px;min-width:120px}.bar-fill{background:var(--pass);height:100%;border-radius:6px}.bar-warn{background:var(--ind)}.bar span{position:absolute;inset:0;text-align:center;font-size:11px;line-height:18px}
.filters{display:flex;gap:10px;align-items:center;margin:6px 0 12px;flex-wrap:wrap}.filters select,.filters button,.filters input{font:inherit;padding:4px 10px;border:1px solid var(--line);border-radius:7px;background:var(--card);color:var(--ink);cursor:pointer}
.filters input{cursor:text;min-width:220px}.filters button.on{border-color:var(--accent);color:var(--accent);font-weight:600}
details{margin-top:4px}details summary{cursor:pointer;color:var(--accent);font-size:12px}details ul{margin:6px 0 0;padding-left:16px}
.exec .verdict-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.verdict{font-size:26px;font-weight:800;letter-spacing:1px;padding:6px 22px;border-radius:10px}
.verdict-pass{color:var(--pass);background:var(--pass-bg)}.verdict-fail{color:var(--fail);background:var(--fail-bg)}
.exec-sub{color:var(--muted);font-size:14px}
.charts{display:flex;gap:26px;flex-wrap:wrap;margin-top:14px;align-items:flex-end}
.charts figure{margin:0}.charts figcaption{color:var(--muted);font-size:12px;margin-bottom:6px}
.seg-track{stroke:var(--skip-bg)}.seg-pass{stroke:var(--pass)}.seg-fail{stroke:var(--fail)}.seg-skip{stroke:var(--skip)}.seg-ind{stroke:var(--ind)}
.donut-center{fill:var(--ink);font-size:19px;font-weight:650}
.band-fast{fill:var(--pass)}.band-ok{fill:var(--accent)}.band-warn{fill:var(--ind)}.band-slow{fill:var(--fail)}
.bar-label,.bar-count{fill:var(--muted);font-size:11px}
.cov{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}.cov-card{border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:12.5px}
.cov-card strong{margin-right:8px}.chips{margin-top:6px}.chip{display:inline-block;background:var(--skip-bg);color:var(--muted);border-radius:99px;padding:1px 8px;font-size:11px;margin:0 4px 4px 0}
.failures details.fgroup summary{font-size:13.5px;font-weight:600}
.excerpt{white-space:pre-wrap;word-break:break-word;background:var(--skip-bg);border-radius:6px;padding:8px;max-width:640px;max-height:220px;overflow:auto}
.perf-adv summary h2{display:inline;font-size:14px}
.mx-tools{display:flex;gap:8px;margin-bottom:8px}.mx-tools button{font:inherit;font-size:12px;padding:3px 10px;border:1px solid var(--line);border-radius:7px;background:var(--card);color:var(--ink);cursor:pointer}
details.mx{border:1px solid var(--line);border-radius:8px;padding:4px 10px;margin:6px 0}
details.mx summary{display:flex;justify-content:space-between;gap:12px;align-items:center}
.mx-counts{font-size:12px}.c-pass{color:var(--pass);margin-right:8px}.c-fail{color:var(--fail);margin-right:8px}.c-skip{color:var(--skip)}
.mx-scroll{overflow-x:auto;margin:8px 0 4px}
table.mx-table{border-collapse:collapse;font-size:12px}
.mx-table th,.mx-table td{border:1px solid var(--line);padding:2px 6px}
.mx-table thead th{background:var(--card);position:sticky;top:0}
.mx-table .mx-unit{position:sticky;left:0;background:var(--card);text-align:left;max-width:420px;font-weight:400}
.mx-perm{display:block;color:var(--muted);font-size:10.5px}
.mx-table td.cell{width:26px;min-width:26px;text-align:center;font-weight:700}
.cell-pass{color:var(--pass);background:var(--pass-bg)}.cell-fail{color:var(--fail);background:var(--fail-bg)}.cell-skip{color:var(--skip);background:var(--skip-bg)}.cell-absent{color:var(--muted)}
footer{color:var(--muted);font-size:12px;margin-top:18px}
@media print{.filters,.mx-tools{display:none}body{background:#fff;padding:0}section{break-inside:avoid;border:none}details.mx,details.fgroup{border:none}details:not([open])>*:not(summary){display:revert}}
`;
const SCRIPT = `
(function(){
var role=document.getElementById('f-role'),search=document.getElementById('f-search'),buttons=[].slice.call(document.querySelectorAll('.filters button[data-f]')),status='all';
function apply(){var r=role?role.value:'all';var q=search?search.value.toLowerCase():'';
[].slice.call(document.querySelectorAll('table.grid tbody tr')).forEach(function(tr){
if(!tr.hasAttribute('data-role'))return;var okR=r==='all'||tr.getAttribute('data-role')===r;var s=tr.getAttribute('data-status');
var okS=status==='all'||(status==='fail'?s==='fail':status==='pass'?s==='pass':s==='skip'||s==='indeterminate');
var okQ=q===''||((tr.children[0]?tr.children[0].textContent:'')+' '+(tr.children[1]?tr.children[1].textContent:'')).toLowerCase().indexOf(q)>-1;
tr.style.display=okR&&okS&&okQ?'':'none';});}
if(role)role.addEventListener('change',apply);
if(search)search.addEventListener('input',apply);
buttons.forEach(function(b){b.addEventListener('click',function(){status=b.getAttribute('data-f');buttons.forEach(function(x){x.classList.toggle('on',x===b)});apply();});});
[].slice.call(document.querySelectorAll('th[data-sort]')).forEach(function(th){th.addEventListener('click',function(){
var table=th.closest('table'),tbody=table.querySelector('tbody'),idx=[].indexOf.call(th.parentNode.children,th);
var dir=th._d=th._d===1?-1:1;
th.setAttribute('aria-sort',dir===1?'ascending':'descending');
[].slice.call(th.parentNode.children).forEach(function(x){if(x!==th)x.removeAttribute('aria-sort')});
[].slice.call(tbody.querySelectorAll('tr')).sort(function(a,b){
var ca=a.children[idx],cb=b.children[idx];var va=ca&&ca.getAttribute('data-v'),vb=cb&&cb.getAttribute('data-v');
if(va!==null&&va!==undefined&&vb!==null&&vb!==undefined)return(Number(va)-Number(vb))*dir;
return String(ca?ca.textContent:'').localeCompare(String(cb?cb.textContent:''))*dir;
}).forEach(function(tr){tbody.appendChild(tr)});});});
[].slice.call(document.querySelectorAll('.mx-tools button[data-mx]')).forEach(function(b){b.addEventListener('click',function(){
var open=b.getAttribute('data-mx')==='open';var sec=b.closest('section');
[].slice.call(sec.querySelectorAll('details.mx')).forEach(function(d){d.open=open});});});
})();
`;
export function renderHtml(model: ReportModel): string {
const api = model.api;
const ui = model.ui;
const kpis: string[] = [];
kpis.push(
kpiCard(
'Overall pass rate',
model.overallPassRate === null ? '—' : `${model.overallPassRate}%`,
`${(api?.passed ?? 0) + (ui?.passed ?? 0)} passed · ${(api?.failed ?? 0) + (ui?.failed ?? 0)} failed`,
),
);
if (api) {
kpis.push(kpiCard('API checks', `${api.passed}/${api.executed}`, `${api.skipped} skipped`));
kpis.push(kpiCard('API latency', `${api.avgDurationMs} ms avg`, `p95 ${api.p95DurationMs} ms`));
kpis.push(kpiCard('API payload', humanBytes(api.totalBytes), 'total received'));
}
if (ui) {
kpis.push(kpiCard('UI checks', `${ui.passed}/${ui.executed - ui.indeterminate}`, `${ui.indeterminate} indeterminate · ${ui.skipped} skipped`));
kpis.push(kpiCard('Page readiness', `${ui.avgFullyReadyMs} ms avg`, `p95 ${ui.p95FullyReadyMs} ms · ${ui.perfWarnings} over budget`));
kpis.push(kpiCard('Page weight', humanBytes(ui.totalBytes), 'total transferred'));
kpis.push(kpiCard('Console errors', String(ui.consoleErrorCount), 'on executed pages'));
}
const roleOptions = ['all', ...model.roles].map((r) => `${e(r)} `).join('');
const warningsBlock = model.runWarnings.length
? `Run warnings ${model.runWarnings.map((w) => `${e(w)} `).join('')} `
: '';
function renderSecurityFindings(m: ReportModel): string {
const sf = m.securityFindings;
if (!sf) return '';
const ungatedRows = sf.ungated
.map(
(u) =>
`${e(u.method)} ${e(u.route)} ${u.controller ? ` (${e(u.controller)}) ` : ''} — no [RequirePermission], no [AllowAnonymous]: every authenticated role passes. The green rows above are the DEPLOYED behaviour, not a certification — fix the controller (DEV-API-033). `,
)
.join('');
const unseededRows = sf.unseeded
.map(
(u) =>
`${e(u.method)} ${e(u.route)} — declared permission ${u.permission ? `${e(u.permission)} ` : ''}exists in NO live permission row: every role is denied (lost-seed drift — check the seed / DEV-CORE-011). `,
)
.join('');
return `⚠ Security surface — findings the run cannot certify
${ungatedRows ? `Ungated endpoints (${sf.ungated.length}) ` : ''}
${unseededRows ? `Declared but unseeded permissions (${sf.unseeded.length}) ` : ''}
`;
}
return `
${e(model.title)}
${e(model.title)}
run ${e(model.runId)}
${model.startedAt ? `started ${e(model.startedAt)} ` : ''}
${model.finishedAt ? `finished ${e(model.finishedAt)} ` : ''}
${model.durationMs !== undefined ? `duration ${e(formatDurationMs(model.durationMs))} ` : ''}
${model.apiUrl ? `API ${e(model.apiUrl)} ` : ''}
${model.frontendUrl ? `frontend ${e(model.frontendUrl)} ` : ''}
${model.planPath ? `plan ${e(model.planPath)} ` : ''}
${model.planSignatureShort ? `signature ${e(model.planSignatureShort)} ` : ''}
${renderExecSummary(model)}
${kpis.join('')}
${renderSecurityFindings(model)}
${renderFailuresFirst(model)}
Role ${roleOptions}
All
Failures
Passed
Skipped / indeterminate
Search
${renderRollups(model)}
${model.apiMatrix ? renderMatrix(model.apiMatrix, 'RBAC matrix — API', 'Endpoint') : ''}
${model.uiMatrix ? renderMatrix(model.uiMatrix, 'RBAC matrix — UI', 'Route') : ''}
${renderApiSection(model)}
${renderUiSection(model)}
${renderPerfAdvisories(model)}
${warningsBlock}
Generated ${e(model.generatedAt)} · SmartStack /uat
`;
}