generated from mathias/template-go-web
The CI stage now shows the pipeline's latest real execution, read live from the in-cluster Gitea Actions API (public read, no token, gitea-http.gitea.svc): "▶ run #N · <state>" coloured by outcome, prepended to the generated job list. LatestRun + RunNode built test-first; cached in the same 30s liveOverlay; falls back cleanly when Gitea is unreachable. assessor-loop ledger / session_log deferred: no live CAD data exists for this repo yet (brain confirms). Gitea run history is the real available trace. Verified: build/vet/lint(0)/test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
251 lines
12 KiB
HTML
251 lines
12 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)}
|
|
|
|
.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>v0.7 · Phase C: live CI runs (Gitea Actions) on the pipeline</em></span>
|
|
<div class="controls">
|
|
<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="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>
|
|
</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=[];
|
|
|
|
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());
|
|
STAGES.forEach(s=>{
|
|
const st=document.createElement('div');st.className='stage '+s.cls;
|
|
let h=`<div class="no mono">${s.no}</div><h2>${s.title}</h2><div class="path mono">${s.path||''}</div>`;
|
|
s.nodes.forEach(n=>{
|
|
const pill=n.pill?`<span class="pill" style="background:${n.pill}"></span>`:'';
|
|
let inner=`<div class="t">${pill}${n.t}</div><div class="d">${n.d}</div>`;
|
|
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);
|
|
});
|
|
}
|
|
|
|
/* ---- geometry ---- */
|
|
const spine=document.getElementById('spine'), spinePath=document.getElementById('spinePath'),
|
|
loopPath=document.getElementById('loopPath'), loopLbl=document.getElementById('loopLbl'),
|
|
pulse=document.getElementById('pulse');
|
|
const 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);
|
|
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='feedback bus · outcome → goal';
|
|
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();};
|
|
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;
|
|
}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;
|
|
}
|
|
renderAtlas();
|
|
replay();
|
|
}
|
|
window.addEventListener('load',init);
|
|
</script>
|
|
</body>
|
|
</html>
|