Files
cad-atlas/internal/web/static/cad-atlas.html
T
mathiasandClaude Opus 4.8 5048450b79
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 5s
CD / Build & Import (push) Successful in 18s
CD / Deploy via GitOps (push) Has been skipped
feat(atlas): UX sprint — progressive disclosure (Plain default + transitions)
Makes the atlas self-explanatory. Every stage/node gains a plain-language layer
(plain_title + plain "what happens" + jargon-free node text); the previous
technical copy demotes to a subtitle + on-demand detail. A Plain⇄Technical
toggle (default Plain, persisted) flips the whole atlas. Biggest win: every
transition arrow is now LABELLED with "what must be true to advance" (the gated-
flow story that was invisible), gate hops (human @04, CI @06) styled distinctly.
Spine repositioned into a uniform header band so labels never collide with copy.

Copy grounded in a fresh-eyes UX review (docs/UX-REVIEW.md, reviewer≠implementer).
Data model: plain_title/plain/trans_label/trans on Stage, plain on Node — guarded
by a test (every stage has plain_title + a transition). Live overlays unchanged.

Verified: build/vet/lint(0)/test green; Plain render screenshot-checked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 08:22:21 +02:00

325 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CAD Atlas · From Signal to Pod</title>
<style>
:root{
--bg:#080b12; --panel:#0e1420; --panel2:#121a2b; --line:#1e2b44;
--ink:#cdd9ef; --dim:#7f92b5; --mono:#8fb4ff;
--blue:#4aa8ff; --amber:#f5b942; --coral:#ff7a5c; --green:#4ad07a; --violet:#9b8cff; --gold:#e6c15a;
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
background:
radial-gradient(circle at 1px 1px,#16213a 1px,transparent 0) 0 0/26px 26px,
var(--bg);
color:var(--ink);
font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif;
-webkit-font-smoothing:antialiased;
}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
header{
position:sticky;top:0;z-index:30;backdrop-filter:blur(8px);
background:linear-gradient(180deg,rgba(8,11,18,.94),rgba(8,11,18,.72));
border-bottom:1px solid var(--line);
display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:14px 22px;
}
header h1{font-size:19px;margin:0}
header h1 b{color:var(--blue)}
header .sub{color:var(--dim);font-size:12px}
header .sub em{color:var(--amber);font-style:normal}
.controls{margin-left:auto;display:flex;gap:8px}
button{
font:inherit;font-size:12px;color:var(--ink);cursor:pointer;
background:var(--panel2);border:1px solid var(--line);border-radius:8px;
padding:7px 13px;display:inline-flex;align-items:center;gap:7px;
}
button:hover{border-color:var(--blue)}
button.on{border-color:var(--amber);color:var(--amber)}
button .dot{width:7px;height:7px;border-radius:50%;background:var(--blue)}
button.on .dot{background:var(--amber)}
.substrate{display:flex;gap:10px;flex-wrap:wrap;align-items:center;
padding:10px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
.substrate .lbl{color:var(--dim);font-size:11px;letter-spacing:1.5px;margin-right:4px}
.host{border:1px solid var(--line);border-radius:8px;padding:6px 11px;
background:var(--panel2);font-size:12px;display:flex;gap:8px;align-items:center}
.host b{color:var(--blue)}
.host .k{color:var(--dim);font-size:11px}
.host.mesh{border-style:dashed;color:var(--dim)}
.tl{display:none;gap:5px;align-items:center;flex-wrap:wrap;
padding:8px 22px;border-bottom:1px solid var(--line);background:var(--panel)}
.tl .lbl{color:var(--dim);font-size:11px;letter-spacing:1.5px;margin-right:4px}
.rundot{width:22px;height:16px;border-radius:4px;border:1px solid rgba(0,0,0,.35);
display:inline-flex;align-items:center;justify-content:center;
font-size:9px;color:#08121f;font-weight:600}
/* progressive disclosure */
.tech-sub{color:var(--dim);font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;margin:1px 0 5px}
.plain-what{color:var(--ink);font-size:12.5px;line-height:1.45;margin-bottom:2px;opacity:.92}
.translabel{fill:var(--mono);font-size:10px;font-family:ui-monospace,SFMono-Regular,monospace}
.translabel-gate{fill:var(--gold);font-weight:700}
.stagehead{min-height:64px}
body.plain .stagehead{min-height:188px}
.stage .node:first-of-type{margin-top:24px}
.scroll{overflow-x:auto;padding:24px 22px 20px}
.track{position:relative;display:flex;align-items:flex-start;min-width:max-content}
svg.spine{position:absolute;left:0;top:0;z-index:0;pointer-events:none;overflow:visible}
.pulse{position:absolute;top:0;left:0;width:13px;height:13px;border-radius:50%;
background:var(--amber);box-shadow:0 0 15px 4px rgba(245,185,66,.75);
transform:translate(-6.5px,-6.5px);z-index:6}
.stage{width:300px;flex:0 0 300px;padding:0 14px;position:relative;z-index:2}
.stage .no{color:var(--dim);font-size:11px;letter-spacing:2px}
.stage h2{font-size:17px;margin:6px 0 2px}
.stage .path{color:var(--mono);font-size:11.5px;margin-bottom:6px;min-height:16px}
.node{border:1px solid var(--line);border-radius:11px;background:var(--panel);
padding:12px 13px;margin-top:12px;position:relative;
transition:border-color .25s,box-shadow .25s,transform .25s}
.node .t{font-weight:600;margin-bottom:3px;display:flex;align-items:center;gap:7px}
.node .d{color:var(--dim);font-size:12px}
.node .tag{display:inline-block;font-size:10.5px;padding:2px 7px;border-radius:20px;
border:1px solid var(--line);color:var(--dim);margin:6px 6px 0 0}
.pill{width:8px;height:8px;border-radius:50%;flex:0 0 8px}
.stage.telos h2{color:var(--violet)}
.stage.gate h2{color:var(--amber)}
.stage.exec h2{color:var(--coral)}
.stage.cd h2{color:var(--green)}
.node.win{border-color:var(--coral);box-shadow:0 0 0 1px rgba(255,122,92,.25)}
.node.gateway{border-color:var(--amber);background:linear-gradient(180deg,rgba(245,185,66,.06),transparent)}
.node.council{border-color:var(--violet)}
.node.bridge{border-color:var(--blue);border-style:dashed}
.node.oath{border-color:var(--gold);border-style:dashed;background:linear-gradient(180deg,rgba(230,193,90,.05),transparent)}
.stage.active h2{text-shadow:0 0 18px currentColor}
.stage.active .node{border-color:var(--amber);
box-shadow:0 0 0 1px rgba(245,185,66,.35),0 10px 30px -12px rgba(245,185,66,.4);transform:translateY(-2px)}
.risk{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.risk span{font-size:10.5px;padding:2px 7px;border-radius:6px;border:1px solid var(--line)}
.risk .lo{color:var(--green);border-color:rgba(74,208,122,.4)}
.risk .md{color:var(--amber);border-color:rgba(245,185,66,.4)}
.risk .hi{color:var(--coral);border-color:rgba(255,122,92,.4)}
.gatebtns{display:flex;gap:8px;margin-top:10px}
.gatebtns .g{flex:1;text-align:center;padding:8px;border-radius:8px;font-size:12px;border:1px solid}
.gatebtns .ok{border-color:rgba(74,208,122,.5);color:var(--green)}
.gatebtns .no{border-color:rgba(255,122,92,.5);color:var(--coral)}
footer{padding:14px 22px 30px;color:var(--dim);font-size:11px;border-top:1px solid var(--line)}
footer code{color:var(--mono)}
/* ---- responsive: stack vertically, drop the SVG spine ---- */
@media(max-width:820px){
.scroll{overflow-x:visible;padding:16px}
.track{flex-direction:column;min-width:0}
svg.spine,.pulse{display:none}
.stage{width:100%;flex:auto;padding:0;border-left:2px solid var(--line);margin-left:6px;
padding-left:16px;padding-bottom:8px}
.stage.telos{border-color:var(--violet)} .stage.gate{border-color:var(--amber)}
.stage.exec{border-color:var(--coral)} .stage.cd{border-color:var(--green)}
}
</style>
</head>
<body>
<header>
<h1><b>CAD</b> Atlas · From Signal to Pod</h1>
<span class="sub mono">one human gate · everything up- and downstream is agents · <em id="ver">dev</em></span>
<div class="controls">
<button id="mode" class="on">View · <span id="modeState">Plain</span></button>
<button id="replay"><span class="dot"></span> Replay</button>
<button id="slowmo">Slow-mo · <span id="slowState">off</span></button>
</div>
</header>
<div class="substrate" id="substrate"><span class="lbl mono">SUBSTRATE</span></div>
<div class="tl mono" id="timeline"></div>
<div class="scroll">
<div class="track" id="track">
<svg class="spine" id="spine">
<defs>
<marker id="arw" markerWidth="9" markerHeight="9" refX="6" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#33507f"></path>
</marker>
</defs>
<path id="spinePath" fill="none" stroke="#33507f" stroke-width="2"
marker-mid="url(#arw)" marker-end="url(#arw)"></path>
<path id="loopPath" fill="none" stroke="#9b8cff" stroke-width="1.6"
stroke-dasharray="5 5" opacity=".7"></path>
<text id="loopLbl" fill="#9b8cff" font-size="11"
font-family="ui-monospace,monospace" opacity=".85"></text>
<g id="translabels"></g>
</svg>
<div class="pulse" id="pulse"></div>
</div>
</div>
<footer class="mono">
CAD → CI → CD · intent→specify→dispatch · build→test→validate · deploy→ship.
Dashed violet = feedback bus (stage 08 → TELOS: deploy outcome scored vs originating goal).
Data served from <code>/api/atlas.json</code> (authored <code>atlas.json</code> + CI stage from the live <code>cd.yml</code> + substrate from the live cluster nodes).
Phase C: <code>Gitea run API</code> live (latest run on the CI stage). Pending: <code>assessor-loop</code> ledger · <code>session_log</code> · per-job status · Flux events.
</footer>
<script>
// Single source of truth: /api/atlas.json (Go app — authored atlas.json + CI
// stage from the live cd.yml + substrate from the live cluster). These start
// empty and are filled by init()'s fetch; on failure the page shows an error
// banner rather than stale inline data.
let SUBSTRATE=[], NS="", STAGES=[], TIMELINE=[];
let MODE = localStorage.getItem('atlas-mode') || 'plain'; // 'plain' | 'technical'
const track=document.getElementById('track');
let stageEls=[];
function renderAtlas(){
const sub=document.getElementById('substrate');
sub.querySelectorAll('.host').forEach(el=>el.remove());
SUBSTRATE.forEach(h=>{const el=document.createElement('div');el.className='host';
el.innerHTML=`<b>${h.n}</b><span class="k mono">${h.k}</span>`;sub.appendChild(el);});
const mesh=document.createElement('div');mesh.className='host mesh mono';mesh.textContent=NS;sub.appendChild(mesh);
stageEls=[];
track.querySelectorAll('.stage').forEach(el=>el.remove());
const plain = MODE==='plain';
STAGES.forEach(s=>{
const st=document.createElement('div');st.className='stage '+(s.cls||'');
const head = plain
? `<div class="no mono">${s.no}</div><h2>${s.plain_title||s.title}</h2>`+
`<div class="tech-sub">${s.title}</div>`+(s.plain?`<div class="plain-what">${s.plain}</div>`:'')
: `<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
let h = `<div class="stagehead">${head}</div>`;
(s.nodes||[]).forEach(n=>{
const pill=n.pill?`<span class="pill" style="background:${n.pill}"></span>`:'';
const body = plain ? (n.plain||n.d||'') : (n.d||'');
let inner=`<div class="t">${pill}${n.t}</div>`+(body?`<div class="d">${body}</div>`:'');
if(!plain){
if(n.tags&&n.tags.length)inner+=n.tags.map(t=>`<span class="tag mono">${t}</span>`).join('');
if(n.risk)inner+=`<div class="risk mono"><span class="lo">LOW · auto</span><span class="md">MED · ntfy gate</span><span class="hi">HIGH · blocked</span></div>`;
}
if(n.gate)inner+=`<div class="gatebtns mono"><div class="g ok">✓ approve</div><div class="g no">✕ reject</div></div>`;
h+=`<div class="node ${n.cls||''}">${inner}</div>`;
});
st.innerHTML=h;track.appendChild(st);stageEls.push(st);
});
}
function renderTimeline(){
const tl=document.getElementById('timeline');
if(!TIMELINE.length){tl.style.display='none';return;}
tl.innerHTML='<span class="lbl">RECENT RUNS</span>';
TIMELINE.forEach(d=>{
const c=d.state==='success'?'var(--green)':(d.state==='failure'?'var(--coral)':'var(--amber)');
const el=document.createElement('span');
el.className='rundot';el.style.background=c;
el.title='run #'+d.number+' · '+d.state;el.textContent=d.number;
tl.appendChild(el);
});
tl.style.display='flex';
}
/* ---- geometry ---- */
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
loopPath=document.getElementById('loopPath'), loopLbl=document.getElementById('loopLbl'),
pulse=document.getElementById('pulse');
let RAILY=70;
let cs=[],loopY=0,spineLen=0,loopLen=0,slow=false,raf=null,t0=null,mobile=false;
function build(){
mobile=window.matchMedia('(max-width:820px)').matches;
if(mobile)return;
cs=stageEls.map(s=>s.offsetLeft+s.offsetWidth/2);
// spine sits in the band between the (uniform) stage headers and the first node
const firstTops=stageEls.map(s=>{const n=s.querySelector('.node');return n?s.offsetTop+n.offsetTop:120;});
RAILY=Math.max(66, Math.min(...firstTops)-16);
const maxBottom=Math.max(...stageEls.map(s=>s.offsetTop+s.offsetHeight));
loopY=maxBottom+40;
spine.setAttribute('width',track.scrollWidth);
spine.setAttribute('height',loopY+70);
track.style.minHeight=(loopY+70)+'px';
spinePath.setAttribute('d','M '+cs[0]+' '+RAILY+cs.slice(1).map(x=>' L '+x+' '+RAILY).join(''));
const lastX=cs[cs.length-1], telosX=cs[1];
loopPath.setAttribute('d',`M ${lastX} ${RAILY} L ${lastX} ${loopY} L ${telosX} ${loopY} L ${telosX} ${RAILY}`);
loopLbl.setAttribute('x',(telosX+lastX)/2-90);loopLbl.setAttribute('y',loopY-8);
loopLbl.textContent=(STAGES[8]&&STAGES[8].trans_label?STAGES[8].trans_label+' · ':'')+'feedback bus';
// transition labels on the spine — the "what must be true to advance" story.
// Always visible (default layer). Gate hops (04→05 human, 06→07 CI) in gold.
const tg=document.getElementById('translabels'); tg.innerHTML='';
for(let i=0;i<cs.length-1;i++){
const s=STAGES[i]; if(!s||!s.trans_label) continue;
const gate = (i===4||i===6);
const t=document.createElementNS('http://www.w3.org/2000/svg','text');
t.setAttribute('x',(cs[i]+cs[i+1])/2);t.setAttribute('y',RAILY-9);
t.setAttribute('text-anchor','middle');
t.setAttribute('class',gate?'translabel translabel-gate':'translabel');
t.textContent=(gate?'▸ ':'')+s.trans_label;
const ttl=document.createElementNS('http://www.w3.org/2000/svg','title');
ttl.textContent=s.trans||''; t.appendChild(ttl);
tg.appendChild(t);
}
spineLen=spinePath.getTotalLength();loopLen=loopPath.getTotalLength();
}
function run(ts){
if(mobile)return;
if(!t0)t0=ts;
const dur=slow?16000:7000;
const p=Math.min((ts-t0)/dur,1);
const total=spineLen+loopLen, dist=total*p;
let pt,onLoop=dist>spineLen;
pt=onLoop?loopPath.getPointAtLength(dist-spineLen):spinePath.getPointAtLength(dist);
pulse.style.left=pt.x+'px';pulse.style.top=pt.y+'px';
pulse.style.background=onLoop?'var(--violet)':'var(--amber)';
pulse.style.boxShadow=onLoop?'0 0 15px 4px rgba(155,140,255,.75)':'0 0 15px 4px rgba(245,185,66,.75)';
let act=onLoop?1:0; // during loop, highlight TELOS
if(!onLoop)cs.forEach((c,i)=>{if(pt.x>=c-150)act=i;});
stageEls.forEach((s,i)=>s.classList.toggle('active',i===act));
if(p<1)raf=requestAnimationFrame(run);
}
function replay(){cancelAnimationFrame(raf);t0=null;build();if(!mobile)raf=requestAnimationFrame(run);}
document.getElementById('replay').onclick=replay;
document.getElementById('slowmo').onclick=e=>{slow=!slow;e.currentTarget.classList.toggle('on',slow);
document.getElementById('slowState').textContent=slow?'on':'off';replay();};
function applyMode(){
document.getElementById('modeState').textContent = MODE==='plain'?'Plain':'Technical';
document.getElementById('mode').classList.toggle('on', MODE==='plain');
document.body.classList.toggle('plain', MODE==='plain');
}
document.getElementById('mode').onclick=()=>{
MODE = MODE==='plain' ? 'technical' : 'plain';
localStorage.setItem('atlas-mode',MODE);
applyMode(); renderAtlas(); replay();
};
window.addEventListener('resize',()=>{clearTimeout(window._r);window._r=setTimeout(replay,150);});
async function init(){
try{
const r=await fetch('/api/atlas.json');
if(!r.ok) throw new Error('atlas.json '+r.status);
const data=await r.json();
if(Array.isArray(data.substrate)) SUBSTRATE=data.substrate;
if(typeof data.ns==='string') NS=data.ns;
if(Array.isArray(data.stages)) STAGES=data.stages;
if(Array.isArray(data.timeline)) TIMELINE=data.timeline;
if(data.version) document.getElementById('ver').textContent=data.version;
}catch(e){
console.error('atlas: failed to load /api/atlas.json —',e);
document.getElementById('track').insertAdjacentHTML('beforeend',
'<div class="stage"><div class="no mono">ERROR</div>'+
'<h2 style="color:var(--coral)">Data unavailable</h2>'+
'<div class="path mono">/api/atlas.json failed to load</div></div>');
return;
}
applyMode();
renderAtlas();
renderTimeline();
replay();
}
window.addEventListener('load',init);
</script>
</body>
</html>