134 lines
10 KiB
HTML
134 lines
10 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ASSESSOR · Analysis preview</title>
|
|
<style>
|
|
@font-face { font-family: Inter; src: url('../node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2') format('woff2'); font-weight: 100 900; }
|
|
:root { color-scheme: dark; --bg:#17171b; --panel:#24242a; --tile:#303036; --tile2:#38383f; --line:#4d4d56; --text:#f1f0f3; --muted:#aaa8b0; --yellow:#d9962a; --red:#f04452; --orange:#ef8a36; --blue:#4b9cff; --green:#42b978; }
|
|
* { box-sizing: border-box; }
|
|
body { margin:0; background:var(--bg); color:var(--text); font-family:Inter, sans-serif; font-size:14px; line-height:1.5; }
|
|
.shell { width:min(1500px, 100%); margin:auto; padding:28px; }
|
|
header { display:flex; align-items:flex-end; justify-content:space-between; gap:24px; padding-bottom:22px; border-bottom:1px solid var(--line); }
|
|
.eyebrow,.label { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.09em; text-transform:uppercase; }
|
|
h1 { margin:3px 0 0; font-size:42px; line-height:1.05; font-weight:600; letter-spacing:-.035em; }
|
|
h2 { margin:0 0 14px; font-size:20px; font-weight:600; }
|
|
h3 { margin:0; font-size:16px; font-weight:600; }
|
|
p { margin:0; }
|
|
.badge { display:inline-flex; align-items:center; gap:8px; padding:7px 10px; background:var(--tile2); font-size:12px; font-weight:700; text-transform:uppercase; }
|
|
.badge:before { content:''; width:8px; height:8px; background:currentColor; }
|
|
.critical { color:var(--red); }
|
|
.high { color:var(--orange); }
|
|
.medium { color:var(--yellow); }
|
|
.low { color:var(--green); }
|
|
.top-grid { display:grid; grid-template-columns:1.65fr 1fr; gap:12px; margin-top:20px; }
|
|
.panel { background:var(--panel); padding:20px; }
|
|
.summary { font-size:16px; line-height:1.65; }
|
|
.metrics { display:grid; grid-template-columns:repeat(2,1fr); gap:1px; background:var(--line); }
|
|
.metric { min-height:104px; padding:16px; background:var(--tile); }
|
|
.metric strong { display:block; margin-top:8px; font-size:25px; font-weight:600; }
|
|
.metric span { color:var(--muted); font-size:12px; }
|
|
section { margin-top:28px; }
|
|
.section-head { display:flex; align-items:end; justify-content:space-between; margin-bottom:12px; }
|
|
.section-head p { color:var(--muted); font-size:12px; }
|
|
.themes { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; }
|
|
.theme { position:relative; min-height:235px; padding:20px; background:var(--tile); overflow:hidden; }
|
|
.theme:before { content:''; position:absolute; inset:0 auto 0 0; width:3px; background:var(--severity); }
|
|
.theme .theme-meta { display:flex; justify-content:space-between; margin-bottom:16px; }
|
|
.theme .index { color:var(--muted); font-size:12px; }
|
|
.theme p { margin-top:12px; color:#d0ced4; font-size:13px; }
|
|
.refs { display:flex; flex-wrap:wrap; gap:5px; margin-top:16px; }
|
|
.ref { padding:3px 6px; background:#202025; color:#c8c6cd; font-size:10px; }
|
|
.roadmap { display:grid; grid-template-columns:repeat(5,1fr); gap:12px; counter-reset:phase; }
|
|
.phase { padding:18px; background:var(--tile); }
|
|
.phase-number { display:grid; place-items:center; width:34px; height:34px; margin-bottom:16px; background:var(--yellow); color:#1d1710; font-weight:700; }
|
|
.phase ul { margin:12px 0 0; padding-left:17px; color:#ccc9d1; }
|
|
.phase li { margin:7px 0; }
|
|
.priority-layout { display:grid; grid-template-columns:1.45fr .75fr; gap:12px; align-items:start; }
|
|
.priority-list { display:grid; gap:8px; }
|
|
.priority { display:grid; grid-template-columns:46px minmax(210px,.8fr) 110px 1.5fr; gap:14px; align-items:start; padding:15px 16px; background:var(--tile); }
|
|
.rank { color:var(--yellow); font-size:22px; font-weight:600; }
|
|
.timeframe { color:var(--muted); font-size:11px; font-weight:700; text-transform:uppercase; }
|
|
.priority .why { color:#ccc9d1; font-size:13px; }
|
|
.side-stack { display:grid; gap:12px; }
|
|
.quick-list,.gap-list { display:grid; gap:8px; }
|
|
.quick,.gap { padding:14px; background:var(--tile); }
|
|
.quick { border-left:3px solid var(--green); }
|
|
.gap { border-left:3px solid #85838b; }
|
|
.gap p { margin-top:6px; color:var(--muted); font-size:12px; }
|
|
footer { margin-top:30px; padding:18px 0; border-top:1px solid var(--line); color:var(--muted); font-size:12px; }
|
|
.error { margin:40px; padding:20px; background:#4b2025; }
|
|
@media (max-width:1100px) { .themes{grid-template-columns:repeat(2,1fr)} .roadmap{grid-template-columns:repeat(2,1fr)} .priority-layout,.top-grid{grid-template-columns:1fr} }
|
|
@media (max-width:700px) { .shell{padding:16px} h1{font-size:32px} .themes,.roadmap{grid-template-columns:1fr} .priority{grid-template-columns:36px 1fr} .priority .timeframe,.priority .why{grid-column:2} }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="shell" id="app" aria-live="polite"></main>
|
|
<script src="./test-output-data.js"></script>
|
|
<script>
|
|
const el = (tag, className, text) => {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
};
|
|
const append = (parent, ...children) => { children.filter(Boolean).forEach(child => parent.append(child)); return parent; };
|
|
const refs = ids => {
|
|
const box = el('div', 'refs');
|
|
ids.slice(0, 8).forEach(id => box.append(el('span', 'ref', id)));
|
|
if (ids.length > 8) box.append(el('span', 'ref', `+${ids.length - 8}`));
|
|
return box;
|
|
};
|
|
const riskColor = risk => ({ critical:'var(--red)', high:'var(--orange)', medium:'var(--yellow)', low:'var(--green)' }[risk] || 'var(--blue)');
|
|
|
|
function render(data) {
|
|
const app = document.querySelector('#app');
|
|
const header = el('header');
|
|
const heading = el('div');
|
|
append(heading, el('div', 'eyebrow', 'ASSESSOR · LLM analysis preview'), el('h1', '', 'Tenant security overview'));
|
|
append(header, heading, el('span', `badge ${data.assessment.riskLevel}`, `${data.assessment.riskLevel} risk · ${data.assessment.confidence} confidence`));
|
|
|
|
const top = el('div', 'top-grid');
|
|
const executive = el('article', 'panel');
|
|
append(executive, el('div', 'label', 'Executive summary'), el('p', 'summary', data.executiveSummary));
|
|
const metrics = el('div', 'metrics');
|
|
[['Risk', data.assessment.riskLevel, data.assessment.rationale], ['Confidence', data.assessment.confidence, 'Evidence quality and coverage'], ['Themes', data.themes.length, 'Consolidated risk areas'], ['Priorities', data.priorities.length, 'Ranked remediation actions']].forEach(([label,value,note]) => {
|
|
const metric=el('div','metric'); append(metric,el('div','label',label),el('strong',String(value).includes('critical')?'critical':'',String(value)),el('span','',note)); metrics.append(metric);
|
|
});
|
|
append(top, executive, metrics);
|
|
|
|
const themeSection=el('section'); const themeHead=el('div','section-head'); append(themeHead,el('h2','','Risk themes'),el('p','',`${data.themes.length} evidence-backed themes`));
|
|
const themeGrid=el('div','themes');
|
|
data.themes.forEach(theme => {
|
|
const card=el('article','theme'); card.style.setProperty('--severity',riskColor(theme.riskLevel));
|
|
const meta=el('div','theme-meta'); append(meta,el('span','index',theme.id),el('span',`badge ${theme.riskLevel}`,theme.riskLevel));
|
|
append(card,meta,el('h3','',theme.title),el('p','',theme.summary),refs(theme.findingIds)); themeGrid.append(card);
|
|
}); append(themeSection,themeHead,themeGrid);
|
|
|
|
const roadSection=el('section'); const roadHead=el('div','section-head'); append(roadHead,el('h2','','Five-phase roadmap'),el('p','','Contain → harden → govern → sustain'));
|
|
const road=el('div','roadmap'); data.roadmap.forEach(item => { const card=el('article','phase'); const list=el('ul'); item.tasks.forEach(task=>list.append(el('li','',task))); append(card,el('div','phase-number',String(item.phase)),el('h3','',item.title),list); road.append(card); }); append(roadSection,roadHead,road);
|
|
|
|
const actionSection=el('section'); const actionHead=el('div','section-head'); append(actionHead,el('h2','','Action plan'),el('p','',`${data.priorities.length} priorities · ${data.quickWins.length} quick wins`));
|
|
const layout=el('div','priority-layout'); const plist=el('div','priority-list');
|
|
data.priorities.forEach(item => { const row=el('article','priority'); const title=el('div'); append(title,el('h3','',item.title),el('span','label',item.owner)); append(row,el('div','rank',String(item.rank).padStart(2,'0')),title,el('div','timeframe',item.timeframe.replace('_',' ')),el('p','why',item.rationale)); plist.append(row); });
|
|
const side=el('aside','side-stack'); const quickPanel=el('div','panel'); append(quickPanel,el('h2','','Quick wins')); const quickList=el('div','quick-list'); data.quickWins.forEach(item=>{const q=el('div','quick');append(q,el('h3','',item.title),refs(item.findingIds));quickList.append(q)});quickPanel.append(quickList);
|
|
const gapPanel=el('div','panel');append(gapPanel,el('h2','','Coverage gaps'));const gapList=el('div','gap-list');data.coverageGaps.forEach(item=>{const g=el('div','gap');append(g,el('h3','',item.title),el('p','',item.impact),refs(item.evidenceIds));gapList.append(g)});gapPanel.append(gapList);append(side,quickPanel,gapPanel);append(layout,plist,side);append(actionSection,actionHead,layout);
|
|
|
|
append(app,header,top,themeSection,roadSection,actionSection,el('footer','',`Static preview · Schema ${data.schemaVersion} · Source: test-output.json · Not connected to ASSESSOR`));
|
|
}
|
|
const embeddedData = window.ASSESSOR_PREVIEW_DATA;
|
|
if (location.protocol === 'file:' && embeddedData) {
|
|
render(embeddedData);
|
|
} else {
|
|
fetch('/test-output.json').then(response => {
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
return response.json();
|
|
}).then(render).catch(error => embeddedData
|
|
? render(embeddedData)
|
|
: document.querySelector('#app').append(el('div','error',`Could not load test-output.json: ${error.message}`)));
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|