aere-quantum/control-plane/consola-web/index.html

342 lines
21 KiB
HTML

<title>AERE Verification Console</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
<style>
:root{
color-scheme: dark;
--bg:#0a0e13; --panel:#111721; --panel2:#0e141d; --line:#1e2732; --line2:#2a3644;
--ink:#e7edf4; --mut:#8a99ab; --dim:#5f6e80;
--accent:#39d3c8; --accent-dim:#1b6d69;
--ok:#4bcf8e; --warn:#e3ab4d; --crit:#f2685f;
--shadow:0 1px 0 rgba(255,255,255,.02), 0 12px 30px -18px rgba(0,0,0,.8);
--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
--sans:"IBM Plex Sans",system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
}
:root[data-theme="light"]{
color-scheme: light;
--bg:#f3f5f8; --panel:#ffffff; --panel2:#f7f9fb; --line:#e3e8ef; --line2:#d3dbe5;
--ink:#16202c; --mut:#5a6b7d; --dim:#8a99ab;
--accent:#0e928a; --accent-dim:#a7e6e1;
--ok:#12915c; --warn:#a9720a; --crit:#c93a30;
--shadow:0 1px 2px rgba(16,32,48,.06), 0 12px 28px -20px rgba(16,32,48,.35);
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
.wrap{max-width:1000px;margin:0 auto;padding-inline:16px;padding-block:0 56px}
a{color:var(--accent)}
.mono{font-family:var(--mono);font-variant-ligatures:none}
.tnum{font-variant-numeric:tabular-nums}
/* header */
header{position:sticky;top:env(safe-area-inset-top,0px);z-index:5;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.hbar{max-width:1000px;margin:0 auto;padding:14px 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.mark{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em}
.glyph{width:26px;height:26px;flex:none}
.mark .sub{font-weight:500;color:var(--mut);font-size:12px;letter-spacing:.06em;text-transform:uppercase}
.host{margin-left:auto;display:flex;align-items:center;gap:8px;color:var(--mut);font-size:12.5px}
.host .mono{color:var(--ink)}
.verdict{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;font-weight:700;font-size:13px;letter-spacing:.03em;border:1px solid}
.verdict.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
.verdict.bad{color:var(--crit);border-color:color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 12%,transparent)}
.dot{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px color-mix(in srgb,currentColor 22%,transparent)}
/* intro line */
.lead{color:var(--mut);font-size:13px;margin:18px 0 4px}
.lead b{color:var(--ink);font-weight:600}
/* tiles */
.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0 8px}
@media(max-width:760px){.tiles{grid-template-columns:repeat(2,1fr)}}
@media(max-width:420px){.tiles{grid-template-columns:1fr}}
.tile{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 15px;box-shadow:var(--shadow);display:flex;flex-direction:column;gap:6px;min-height:98px}
.tile .k{font-size:11px;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);display:flex;align-items:center;gap:7px}
.tile .v{font-size:24px;font-weight:700;font-family:var(--mono);line-height:1.1}
.tile .n{font-size:12px;color:var(--mut)}
.stripe{height:3px;border-radius:2px;margin-top:auto}
.s-ok{background:var(--ok)} .s-warn{background:var(--warn)} .s-crit{background:var(--crit)} .s-acc{background:var(--accent)}
.pin{width:7px;height:7px;border-radius:50%} .pin.ok{background:var(--ok)} .pin.warn{background:var(--warn)} .pin.crit{background:var(--crit)} .pin.acc{background:var(--accent)}
/* section */
section{margin-top:28px}
h2{font-size:13px;text-transform:uppercase;letter-spacing:.1em;color:var(--mut);font-weight:600;margin:0 0 12px;display:flex;align-items:center;gap:9px}
h2 .rule{height:1px;background:var(--line);flex:1}
/* chain */
.chain{display:flex;flex-direction:column;gap:0}
.node{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:13px 15px;box-shadow:var(--shadow)}
.node + .link{height:26px;margin-left:26px;border-left:2px dashed var(--line2);position:relative}
.node + .link::after{content:"prev";position:absolute;left:12px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:.05em;background:var(--bg);padding:0 6px}
.node.head{border-color:color-mix(in srgb,var(--accent) 55%,var(--line));box-shadow:var(--shadow),0 0 0 1px color-mix(in srgb,var(--accent) 30%,transparent)}
.nrow{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.seq{font-family:var(--mono);font-weight:600;color:var(--accent);font-size:13px;min-width:30px}
.kind{display:inline-flex;align-items:center;gap:7px;font-weight:600;font-size:13.5px}
.kind .tag{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);border:1px solid var(--line2);border-radius:5px;padding:1px 6px}
.nname{color:var(--mut);font-size:12.5px}
.nat{margin-left:auto;color:var(--dim);font-size:11.5px;font-family:var(--mono)}
.headflag{font-size:10.5px;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);border-radius:999px;padding:1px 8px;letter-spacing:.05em;text-transform:uppercase}
.hashline{margin-top:9px;display:grid;grid-template-columns:auto 1fr auto;gap:8px 10px;align-items:center}
.hlab{font-family:var(--mono);font-size:10.5px;color:var(--dim);text-transform:uppercase;letter-spacing:.05em}
.hval{font-family:var(--mono);font-size:12px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hval.dim{color:var(--mut)}
.copy{border:1px solid var(--line2);background:transparent;color:var(--mut);border-radius:6px;font-family:var(--mono);font-size:10.5px;padding:2px 8px;cursor:pointer;transition:.15s}
.copy:hover{color:var(--ink);border-color:var(--accent)}
.copy.done{color:var(--ok);border-color:var(--ok)}
/* ladder */
.ladder{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
@media(max-width:640px){.ladder{grid-template-columns:1fr 1fr}}
.step{border:1px solid var(--line);background:var(--panel2);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:6px;position:relative;opacity:.62}
.step.cur{opacity:1;background:var(--panel);border-color:color-mix(in srgb,var(--accent) 50%,var(--line));box-shadow:var(--shadow)}
.step .lv{font-family:var(--mono);font-size:11px;color:var(--dim)}
.step .nm{font-weight:600;font-size:13px}
.step .ds{font-size:11.5px;color:var(--mut)}
.step.cur .nm{color:var(--accent)}
/* migration bars */
.mig{display:grid;grid-template-columns:1.3fr 1fr;gap:14px}
@media(max-width:640px){.mig{grid-template-columns:1fr}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:15px;box-shadow:var(--shadow)}
.bar{height:10px;border-radius:6px;background:var(--panel2);overflow:hidden;display:flex;margin:10px 0}
.bar i{display:block;height:100%}
.legend{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:12px;color:var(--mut)}
.legend span{display:inline-flex;align-items:center;gap:7px}
.mrow{display:flex;justify-content:space-between;align-items:baseline;padding:7px 0;border-top:1px solid var(--line)}
.mrow:first-of-type{border-top:0}
.mrow .b{font-weight:600;color:var(--ink);font-family:var(--mono)}
/* paste */
.paste{margin-top:14px}
textarea{width:100%;min-height:120px;background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:10px;padding:12px;font-family:var(--mono);font-size:12px;resize:vertical}
textarea:focus{outline:2px solid color-mix(in srgb,var(--accent) 50%,transparent);outline-offset:1px;border-color:var(--accent)}
.row{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
button.act{border:1px solid var(--accent-dim);background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-family:var(--sans);font-weight:600;font-size:13px;padding:8px 16px;border-radius:8px;cursor:pointer}
button.act:hover{background:color-mix(in srgb,var(--accent) 26%,transparent)}
button.ghost{border:1px solid var(--line2);background:transparent;color:var(--mut)}
.err{color:var(--crit);font-size:12.5px;font-family:var(--mono)}
.foot{margin-top:34px;padding-top:16px;border-top:1px solid var(--line);color:var(--dim);font-size:11.5px;line-height:1.7}
.foot code{font-family:var(--mono);color:var(--mut)}
.warnbox{margin-top:12px;border:1px solid color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 10%,transparent);color:var(--crit);border-radius:10px;padding:11px 13px;font-size:12.5px}
</style>
<header>
<div class="hbar">
<div class="mark">
<svg class="glyph" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M16 2 3 8v8c0 8 5.4 12.6 13 14 7.6-1.4 13-6 13-14V8L16 2Z" fill="none" stroke="var(--accent)" stroke-width="1.6"/>
<path d="M10.5 16.2 14.4 20l7.3-8.2" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>AERE Verification Console<div class="sub">quantum security control plane</div></div>
</div>
<div class="host">
<span>host</span><span class="mono" id="host">—</span>
<span id="verdict" class="verdict ok"><span class="dot"></span><span id="vtext">OK</span></span>
</div>
</div>
</header>
<div class="wrap">
<p class="lead">The state of a deployment, <b>checked again independently</b> by the control plane: the audit chain is recomputed from its entries, and the integrity of every AIP-23 envelope is recomputed. The console does not trust what the sidecar says about itself.</p>
<div class="tiles" id="tiles"></div>
<div id="alert"></div>
<section>
<h2>Audit chain <span class="rule"></span> <span id="chainmeta" class="mono" style="font-size:11px;color:var(--dim);text-transform:none;letter-spacing:0"></span></h2>
<div class="chain" id="chain"></div>
</section>
<section>
<h2>Post-quantum finality <span class="rule"></span></h2>
<div class="ladder" id="ladder"></div>
</section>
<section>
<h2>Post-quantum migration <span class="rule"></span></h2>
<div class="mig">
<div class="card">
<div style="font-weight:600;margin-bottom:2px">Migration plan</div>
<div class="n" style="color:var(--mut);font-size:12px">actions derived from the cryptographic inventory of the deployment</div>
<div class="bar" id="migbar"></div>
<div class="legend">
<span><i class="pin crit"></i> critical</span>
<span><i class="pin acc"></i> automatic</span>
<span><i class="pin warn"></i> manual</span>
</div>
</div>
<div class="card" id="migrows"></div>
</div>
</section>
<section class="paste">
<h2>Render your own output <span class="rule"></span></h2>
<p class="lead" style="margin-top:0">Paste the output of <code class="mono" style="color:var(--ink)">node consola.mjs --bundle b.json --json</code> or a bundle from <code class="mono" style="color:var(--ink)">node sidecar.mjs bundle</code>.</p>
<textarea id="in" spellcheck="false" placeholder='{ "kind":"aere-control-plane-console", ... }'></textarea>
<div class="row">
<button class="act" id="render">Render</button>
<button class="act ghost" id="reset">Example</button>
<span class="err" id="perr" hidden></span>
</div>
</section>
<div class="foot">
A rendering of the output of the AUTHORITATIVE tools (<code>consola.mjs</code> and the sidecar). The verdict and the integrity of the chain and of the envelopes are computed by the tools, not in the browser: this page shows them as they are. The audit chain is an append-only hash chain where every entry covers <code>sha256(seq | prev | envelope)</code>; its head can be given post-quantum finality by notarizing it on AereNotary (Aere Network). The envelopes follow the AERE Proof Protocol (AIP-23). The data shown on opening is an example deployment generated by the tools; the finality step shown is the one of that example (a head that can be notarized and is not yet).
</div>
</div>
<script>
const GEN = "0x"+"00".repeat(32);
const short = h => !h ? "—" : (h.length>22 ? h.slice(0,12)+"…"+h.slice(-8) : h);
const KINDNAME = {
"aere-proof-of-runtime":["Runtime","the running binary is the attested one"],
"aere-proof-of-data":["Deployment","the deployed artifact, as a digest"],
"aere-audit-head":["Log head","can be notarized"],
};
// an example generated by the verification layer and the control plane (console output format 2)
const SAMPLE = {
v:2, kind:"aere-control-plane-console", host:"aws-eu-prod-1",
auditChain:{ok:true,count:3,head:"0x5f63faec605908668fee3703303b006e2ebe66b3318b9fc8d1f01e86edfb41ee",brokenAtSeq:null,reason:null},
envelopes:{total:3,intact:3,bad:[]},
selfReportSuspect:null,
migration:{total:3,critical:2,auto:2,manual:1,blocked:0},
execution:null,
verdict:"OK",
entries:[
{seq:0,kind:"aere-proof-of-runtime",at:"2026-09-26T08:00:00Z",name:"aws-eu-prod-1",statementHash:"0x1c0183e10520f70884dea2947c763413f8514606026e9fe294bc4cfdb380be67",hash:"0x6168463c93c47acbd8b40f02ed5089e87a77c07a0a2e8a431026bd7f765f8969",prev:GEN},
{seq:1,kind:"aere-proof-of-data",at:"2026-09-26T08:05:00Z",name:"checkout-api@4.2.1",statementHash:"0x5fa286c04da5fe37f4a9c9963999b4162926d81064b95af28a91a665f86b2c68",hash:"0xf2ff1835e76511a902ec02f8fa523a455d114743e1e916abe11cfc8b822579e8",prev:"0x6168463c93c47acbd8b40f02ed5089e87a77c07a0a2e8a431026bd7f765f8969"},
{seq:2,kind:"aere-proof-of-runtime",at:"2026-09-26T08:10:00Z",name:"aws-eu-prod-1",statementHash:"0xa9358d663d8ec8b3a59c7db6a2eab2ed842d2adac61f6d84d43657a7b831c4d5",hash:"0x5f63faec605908668fee3703303b006e2ebe66b3318b9fc8d1f01e86edfb41ee",prev:"0xf2ff1835e76511a902ec02f8fa523a455d114743e1e916abe11cfc8b822579e8"}
]
};
function normalize(o){
// accepts a console state OR a sidecar bundle (it has entries + chainOk); a bundle is shown as the bundle declares it, since the
// browser does not recompute anything: run consola.mjs on it for a checked verdict
if(o.kind==="aere-verify-layer-bundle" || (o.entries && !o.auditChain)){
return {
kind:"aere-control-plane-console", host:o.host||"?",
auditChain:{ok:!!o.chainOk,count:o.count??(o.entries||[]).length,head:o.head||null,brokenAtSeq:null,reason:null},
envelopes:{total:(o.entries||[]).length,intact:(o.entries||[]).length,bad:[]},
selfReportSuspect:null, migration:null, execution:null, verdict:"UNCHECKED",
note:"A bundle pasted here is shown as it declares itself (chainOk="+(!!o.chainOk)+"): the browser checks nothing. Run node consola.mjs --bundle on it for a checked verdict.",
entries:(o.entries||[]).map(e=>({seq:e.seq,kind:e.proof?.statement?.kind||"?",at:e.proof?.statement?.createdAt||"",name:e.proof?.statement?.name||e.proof?.statement?.host||"",statementHash:e.proof?.statementHash||"",hash:e.hash,prev:e.prev}))
};
}
return o;
}
function el(t,c,x){const n=document.createElement(t);if(c)n.className=c;if(x!=null)n.textContent=x;return n;}
function copyBtn(text){
const b=el("button","copy","copy");
b.addEventListener("click",async()=>{
try{await navigator.clipboard.writeText(text);}catch(e){/* the clipboard is not available here */}
b.textContent="copied";b.classList.add("done");setTimeout(()=>{b.textContent="copy";b.classList.remove("done");},1200);
});
return b;
}
function render(d){
d=normalize(d);
const ok = d.verdict==="OK";
document.getElementById("host").textContent=d.host||"?";
const v=document.getElementById("verdict"); v.className="verdict "+(ok?"ok":"bad");
document.getElementById("vtext").textContent=d.verdict||"?";
// tiles
const ac=d.auditChain||{}, pl=d.envelopes||{}, mg=d.migration, ex=d.execution;
const bad=(pl.bad&&pl.bad.length);
const tiles=[
{k:"Audit chain",pin:ac.ok?"ok":"crit",v:ac.ok?"INTACT":"BROKEN",n:(ac.count??0)+" entries"+(ac.ok?"":" · broken at seq "+ac.brokenAtSeq),s:ac.ok?"s-ok":"s-crit"},
{k:"AIP-23 envelopes",pin:bad?"crit":"ok",v:(pl.intact??0)+"/"+(pl.total??0),n:bad?"intact · bad: "+pl.bad.map(x=>x.seq).join(","):"intact",s:bad?"s-crit":"s-ok"},
{k:"PQ migration",pin:mg?(mg.critical?"crit":"acc"):"acc",v:mg&&mg.total!=null?String(mg.total):"—",n:mg?(mg.total==null?(mg.error||"plan not readable"):(mg.critical+" critical · "+mg.auto+" automatic")):"no plan attached",s:mg&&mg.critical?"s-crit":"s-acc"},
ex?{k:"Execution",pin:ex.chainOk?(ex.failed?"crit":"ok"):"crit",v:ex.chainOk?"INTACT":"BROKEN",n:ex.actions+" actions · "+ex.ok+" OK · "+ex.failed+" failed",s:ex.chainOk&&!ex.failed?"s-ok":"s-crit"}
:{k:"Finality",pin:"warn",v:"ready",n:"the head can be notarized (not notarized)",s:"s-warn"},
];
const T=document.getElementById("tiles");T.innerHTML="";
for(const t of tiles){
const c=el("div","tile");
const k=el("div","k");k.append(el("i","pin "+t.pin),document.createTextNode(t.k));
c.append(k,el("div","v",t.v),el("div","n",t.n),el("div","stripe "+t.s));
T.append(c);
}
// alert
const A=document.getElementById("alert");A.innerHTML="";
if(d.selfReportSuspect){const w=el("div","warnbox");w.textContent="Suspect self-report: "+d.selfReportSuspect;A.append(w);}
if(d.note){const w=el("div","warnbox");w.textContent=d.note;A.append(w);}
// chain
const C=document.getElementById("chain");C.innerHTML="";
const cm=document.getElementById("chainmeta");
cm.textContent = ac.head ? ("head "+short(ac.head)) : "";
const entries=d.entries||[];
if(!entries.length){
C.append(el("div","card","No per-entry detail (only the console summary was pasted). Paste a bundle from the sidecar for the whole chain."));
}
entries.forEach((e,i)=>{
if(i>0){C.append(el("div","link"));}
const isHead = e.hash===ac.head;
const n=el("div","node"+(isHead?" head":""));
const row=el("div","nrow");
row.append(el("span","seq","#"+e.seq));
const kn=KINDNAME[e.kind]||[e.kind,""];
const kind=el("span","kind");kind.append(document.createTextNode(kn[0]));const tag=el("span","tag",String(e.kind).replace("aere-proof-of-","").replace("aere-",""));kind.append(tag);
row.append(kind);
if(e.name)row.append(el("span","nname",e.name));
if(isHead)row.append(el("span","headflag","head"));
row.append(el("span","nat",(e.at||"").replace("T"," ").replace("Z","")));
n.append(row);
const hl=el("div","hashline");
hl.append(el("span","hlab","hash"));const hv=el("span","hval",e.hash||"—");hl.append(hv,copyBtn(e.hash||""));
hl.append(el("span","hlab","prev"));const pv=el("span","hval dim",e.prev===GEN?"genesis 0x00…00":short(e.prev));hl.append(pv,el("span"));
n.append(hl);
C.append(n);
});
// ladder: the finality steps of the AIP-23 verifier; the step marked is the one of the example (a head not yet notarized)
const steps=[
["1","not notarized","the statementHash of the head is not on AereNotary yet"],
["2","pending","a notarization transaction was seen, not yet under an anchor"],
["3","anchor without certificate","in a block, under an anchor without a complete post-quantum certificate"],
["4","post-quantum","under an anchor with a Falcon / SLH-DSA certificate"],
];
const cur = 1;
const L=document.getElementById("ladder");L.innerHTML="";
steps.forEach((s,i)=>{
const st=el("div","step"+(i+1===cur?" cur":""));
st.append(el("div","lv","step "+s[0]),el("div","nm",s[1]),el("div","ds",s[2]));
L.append(st);
});
// migration
const mb=document.getElementById("migbar");mb.innerHTML="";mb.style.display="";
const mr=document.getElementById("migrows");mr.innerHTML="";
if(mg && mg.total){
// the bar: critical / automatic / manual scaled to their sum (a visual approximation over categories of urgency and method)
const parts=[["var(--crit)",mg.critical,"critical"],["var(--accent)",mg.auto,"automatic"],["var(--warn)",mg.manual,"manual"]];
const sum=parts.reduce((a,p)=>a+p[1],0)||1;
for(const p of parts){const i=el("i");i.style.background=p[0];i.style.width=(p[1]/sum*100)+"%";mb.append(i);}
for(const [lab,val] of [["Total actions",mg.total],["Critical",mg.critical],["Automatic",mg.auto],["Manual",mg.manual],["Blocked",mg.blocked??0]]){
const r=el("div","mrow");r.append(el("span",null,lab),el("span","b tnum",String(val)));mr.append(r);
}
} else {
mb.style.display="none";
mr.append(el("div","n","No migration plan attached. Run node consola.mjs --bundle b.json --plan plan.json."));
}
}
document.getElementById("render").addEventListener("click",()=>{
const e=document.getElementById("perr");e.hidden=true;
try{ const o=JSON.parse(document.getElementById("in").value.trim()); render(o); }
catch(err){ e.hidden=false; e.textContent="invalid JSON: "+err.message; }
});
document.getElementById("reset").addEventListener("click",()=>{
document.getElementById("in").value=JSON.stringify(SAMPLE,null,1);
document.getElementById("perr").hidden=true; render(SAMPLE);
});
render(SAMPLE);
</script>