← back to AbramsEgo
public/dungeon.html
478 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>AbramsEgo — The Dungeon</title>
<style>
:root{--bg:#0a0810;--wall:#1a1526;--wall2:#221a33;--floor:#14101e;--ink:#e9e4f5;--mut:#8a7fa8;--gold:#d4af6a;
--good:#37d67a;--warn:#e6b84c;--bad:#e5484d;--work:#5b8cff;--idle:#4a4360}
*{box-sizing:border-box;margin:0}
html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);
font:14px/1.4 "Avenir Next",system-ui,sans-serif}
#hud{position:fixed;top:0;left:0;right:0;z-index:10;display:flex;gap:16px;align-items:center;
padding:10px 18px;background:linear-gradient(#0a0810ee,#0a081099);border-bottom:1px solid #2a2338}
#hud h1{font-size:16px;font-weight:600;letter-spacing:.14em;text-transform:uppercase}
#hud h1 em{color:var(--gold);font-style:normal}
.stat{color:var(--mut);font-size:12.5px}.stat b{color:var(--ink);font-variant-numeric:tabular-nums}
.legend{margin-left:auto;display:flex;gap:12px;font-size:11.5px;color:var(--mut)}
.legend span::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
.lg-online::before{background:var(--good)}.lg-work::before{background:var(--work)}
.lg-idle::before{background:var(--idle)}.lg-bad::before{background:var(--bad)}
canvas{display:block;cursor:grab}canvas:active{cursor:grabbing}
#tip{position:fixed;z-index:20;pointer-events:none;background:#0d0a16f0;border:1px solid var(--gold);
border-radius:8px;padding:6px 10px;font-size:12px;max-width:240px;display:none;box-shadow:0 6px 24px #000a}
#tip b{color:var(--gold)}
#panel{position:fixed;top:0;right:0;bottom:0;width:360px;z-index:30;background:#0d0a16f7;
border-left:1px solid var(--gold);transform:translateX(100%);transition:transform .22s;
padding:54px 20px 20px;overflow:auto;backdrop-filter:blur(6px)}
#panel.open{transform:none}
#panel .close{position:absolute;top:14px;right:16px;cursor:pointer;color:var(--mut);font-size:20px}
#panel h2{font-size:18px;margin-bottom:2px}#panel .role{color:var(--gold);font-size:12px;letter-spacing:.06em;text-transform:uppercase;margin-bottom:14px}
.kv{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid #211a30;font-size:13px}
.kv span:first-child{color:var(--mut)}.kv b{font-weight:600;font-variant-numeric:tabular-nums}
.doing{margin:14px 0;padding:12px;border-radius:10px;background:#160f22;border:1px solid #2a2038}
.doing .lbl{color:var(--gold);font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px}
.pill{display:inline-block;padding:2px 9px;border-radius:99px;font-size:11px;font-weight:600}
.pill.good{background:#0f2a1c;color:var(--good)}.pill.work{background:#141f3a;color:var(--work)}
.pill.idle{background:#1a1826;color:var(--mut)}.pill.bad{background:#2a1112;color:var(--bad)}
.hint{position:fixed;bottom:12px;left:18px;z-index:10;color:var(--mut);font-size:11.5px}
#zoombar{position:fixed;bottom:12px;right:18px;z-index:10;display:flex;gap:6px}
#zoombar button{width:30px;height:30px;border-radius:7px;border:1px solid #2a2338;background:#140f20;color:var(--ink);cursor:pointer;font-size:16px}
#mini{position:fixed;left:18px;bottom:44px;z-index:10;width:200px;height:132px;border:1px solid #2a2338;border-radius:8px;background:#0a0810cc;cursor:pointer}
</style></head><body>
<div id="hud">
<h1>The <em>Dungeon</em></h1>
<div class="stat">Chambers <b id="sChambers">–</b></div>
<div class="stat">Agents <b id="sAgents">–</b></div>
<div class="stat">Working <b id="sWork">–</b></div>
<div class="stat">Alerts <b id="sBad">–</b></div>
<div class="stat">Slain <b id="sSlain">0</b></div>
<div class="stat" style="color:var(--gold)">🕓 <b id="sClock">–</b></div>
<div class="legend"><span class="lg-online">online</span><span class="lg-work">working</span><span class="lg-idle">idle</span><span class="lg-bad">alert</span></div>
</div>
<canvas id="c"></canvas>
<div id="tip"></div>
<div id="panel"><div class="close" onclick="closePanel()">✕</div><div id="panelBody"></div></div>
<div class="hint">drag to pan · scroll to zoom · click an agent to see what it's doing · auto-refreshes every 15s</div>
<canvas id="mini"></canvas>
<div id="zoombar"><button id="followBtn" onclick="toggleFollow()" title="auto-tour">🎥</button><button id="sfxBtn" onclick="toggleSfx()" title="sound">🔇</button><button onclick="zoomBy(1.2)">+</button><button onclick="zoomBy(0.83)">−</button><button onclick="resetView()">⌂</button></div>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
let W,H,DPR;function resize(){DPR=devicePixelRatio||1;W=cv.width=innerWidth*DPR;H=cv.height=innerHeight*DPR;cv.style.width=innerWidth+'px';cv.style.height=innerHeight+'px';}
addEventListener('resize',resize);resize();
// ---- world state ----
let cam={x:0,y:0,z:1},chambers=[],agents=[],selected=null,hover=null,t=0;
let particles=[],monsters=[],couriers=[],prevAlertKeys=new Set(),slain=0;
let ultron={x:0,y:0,ph:0},packets=[];
// data-flow packet traveling one point→another along an edge
function packet(ax,ay,bx,by,color){packets.push({ax,ay,bx,by,tt:0,color:color||'#d4af6a'});}
function updatePackets(){for(const p of packets){p.tt+=0.025;}packets=packets.filter(p=>p.tt<1);}
const AUTH=''; // same-origin, cookie/basic-auth already present
// ---- FX: particle bursts (explosions, sparks, poofs) ----
function burst(x,y,color,n,spd){for(let i=0;i<n;i++){const a=Math.random()*6.28,s=(spd||2)*(.4+Math.random());
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,color,r:1.5+Math.random()*2.5});}}
function updateParticles(){for(const p of particles){p.x+=p.vx;p.y+=p.vy;p.vy+=0.04;p.vx*=.96;p.life-=0.02;}
particles=particles.filter(p=>p.life>0);}
function drawParticles(){for(const p of particles){ctx.globalAlpha=Math.max(0,p.life);ctx.fillStyle=p.color;
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,6.28);ctx.fill();}ctx.globalAlpha=1;}
// ---- sound (WebAudio synth, off by default) ----
let sfxOn=false,actx=null,lastKey=0;
function toggleSfx(){sfxOn=!sfxOn;document.getElementById('sfxBtn').textContent=sfxOn?'🔊':'🔇';
if(sfxOn&&!actx){actx=new(window.AudioContext||window.webkitAudioContext)();}}
function beep(freq,dur,type,vol){if(!sfxOn||!actx)return;const o=actx.createOscillator(),g=actx.createGain();
o.type=type||'square';o.frequency.value=freq;g.gain.value=(vol||.05);o.connect(g).connect(actx.destination);
const n=actx.currentTime;g.gain.setValueAtTime(vol||.05,n);g.gain.exponentialRampToValueAtTime(.0001,n+(dur||.1));
o.start(n);o.stop(n+(dur||.1));}
const sfx={clash:()=>beep(320+Math.random()*140,.06,'square',.03),
deliver:()=>{beep(660,.07,'sine',.04);setTimeout(()=>beep(990,.08,'sine',.04),60);},
boom:()=>beep(70,.35,'sawtooth',.08),
slain:()=>{beep(523,.09,'triangle',.05);setTimeout(()=>beep(784,.12,'triangle',.05),90);},
type:()=>{const now=performance.now();if(now-lastKey>55){lastKey=now;beep(1400+Math.random()*400,.02,'square',.015);}}};
// ---- minimap ----
const mini=document.getElementById('mini'),mctx=mini.getContext('2d');
function drawMini(){const w=mini.width=200,h=mini.height=132;mctx.clearRect(0,0,w,h);
if(!chambers.length)return;const b=worldBounds();const sw=b.x1-b.x0||1,sh=b.y1-b.y0||1;
const pad=8,sc=Math.min((w-2*pad)/sw,(h-2*pad)/sh),ox=pad-b.x0*sc,oy=pad-b.y0*sc;
chambers.forEach(c=>{mctx.fillStyle='#221a33';mctx.fillRect(c.x*sc+ox,c.y*sc+oy,c.w*sc,c.h*sc);});
agents.forEach(a=>{const st=statusOf(a);mctx.fillStyle=COL[st];
mctx.fillRect(a.x*sc+ox-1,a.y*sc+oy-1,st==='bad'?3:2,st==='bad'?3:2);});
monsters.forEach(m=>{mctx.fillStyle='#e5484d';mctx.fillRect(m.x*sc+ox-1,m.y*sc+oy-1,3,3);});
// viewport rect
const vx=cam.x*sc+ox,vy=cam.y*sc+oy,vw=(innerWidth/cam.z)*sc,vh=(innerHeight/cam.z)*sc;
mctx.strokeStyle='#d4af6a';mctx.lineWidth=1;mctx.strokeRect(vx,vy,vw,vh);
mini._m={sc,ox,oy};}
mini.addEventListener('click',e=>{if(!mini._m)return;const r=mini.getBoundingClientRect();
const {sc,ox,oy}=mini._m;const wx=((e.clientX-r.left)-ox)/sc,wy=((e.clientY-r.top)-oy)/sc;
cam.x=wx-innerWidth/2/cam.z;cam.y=wy-innerHeight/2/cam.z;followOn=false;syncFollowBtn();});
// ---- chatter: real commit subjects + role-flavored lines ----
let commits=[];
const CHATTER={proc_scrape:['scraping…','parsing feed','importing SKUs','crawling'],
proc_build:['building…','running tests','committing','deploying'],
proc_dash:['serving…','rendering panel','200 OK'],
proc_watch:['watching…','all green','scanning drift','heartbeat'],
proc_mail:['checking inbox','triaging','drafting reply'],
proc_render:['rendering room','gen image','upscaling'],
proc:['working…','processing','ticking'],
officer:['delegating…','reviewing','who owns this?','ship it'],
canary_ok:['all clear','PASS ✓','nothing to report'],
session:['thinking…','editing…','running…']};
function chatterFor(a){
// real commit for a matching repo?
const c=commits.find(c=>{const r=(c.repo||'').toLowerCase(),n=a.name.toLowerCase();
return r&&(n.includes(r.slice(0,6))||r.includes(n.slice(0,6)));});
if(c&&Math.random()<0.5)return '✓ '+(c.subject||'').slice(0,34);
if(a.kind==='officer')return pick(CHATTER.officer);
if(a.kind==='session')return a.doing&&Math.random()<.6?('“'+(a.doing.replace(/^“|”.*$/g,'')).slice(0,26)+'”'):pick(CHATTER.session);
if(a.kind==='canary')return a.bad?'⚠ '+(a.verdict||'FAIL'):pick(CHATTER.canary_ok);
const n=a.name.toLowerCase();
if(/scrap|import|catalog/.test(n))return pick(CHATTER.proc_scrape);
if(/build|loop|deploy/.test(n))return pick(CHATTER.proc_build);
if(/dash|viewer|panel|cncp|token/.test(n))return pick(CHATTER.proc_dash);
if(/watch|monitor|hawk|keep|canary/.test(n))return pick(CHATTER.proc_watch);
if(/mail|george|inbox/.test(n))return pick(CHATTER.proc_mail);
if(/render|image|design|room/.test(n))return pick(CHATTER.proc_render);
return pick(CHATTER.proc);}
const pick=a=>a[(Math.random()*a.length)|0];
// ---- follow cam (auto-tour busiest chamber) ----
let followOn=false,followTgt=null,followTimer=0;
function toggleFollow(){followOn=!followOn;followTimer=0;syncFollowBtn();}
function syncFollowBtn(){document.getElementById('followBtn').style.background=followOn?'#2a2038':'#140f20';
document.getElementById('followBtn').style.borderColor=followOn?'#d4af6a':'#2a2338';}
function chamberScore(ch){let s=0;agents.forEach(a=>{if(a.chamber!==ch)return;
const st=statusOf(a);s+=st==='bad'?6:st==='work'?2:0.2;});return s;}
function updateFollow(){if(!followOn||!chambers.length)return;
if(followTimer--<=0){followTimer=260; // ~4.3s per stop
followTgt=chambers.reduce((b,c)=>chamberScore(c)>chamberScore(b)?c:b,chambers[0]);}
if(followTgt){const tz=Math.min(1.1,Math.min(innerWidth/(followTgt.w+260),innerHeight/(followTgt.h+260)));
const tx=followTgt.x+followTgt.w/2-innerWidth/2/tz, ty=followTgt.y+followTgt.h/2-innerHeight/2/tz;
cam.x+=(tx-cam.x)*.05;cam.y+=(ty-cam.y)*.05;cam.z+=(tz-cam.z)*.05;}}
// status → color/kind
function statusOf(a){
if(a.kind==='canary') return a.bad?'bad':'online';
if(a.status && a.status!=='online') return 'bad';
if(a.working) return 'work';
if(a.idle) return 'idle';
return 'online';
}
const COL={online:'#37d67a',work:'#5b8cff',idle:'#4a4360',bad:'#e5484d'};
// what-it's-doing icon from name/role
function iconFor(a){
const n=(a.name+' '+(a.doing||'')).toLowerCase();
if(a.kind==='officer')return '👑';
if(a.kind==='canary')return a.bad?'🚨':'🛡️';
if(a.kind==='session')return '🎮';
if(/scrap|import|catalog/.test(n))return '🕷️';
if(/build|loop|deploy/.test(n))return '⚙️';
if(/dash|viewer|panel|cncp/.test(n))return '📊';
if(/watch|monitor|hawk|keep/.test(n))return '👁️';
if(/mail|george|inbox/.test(n))return '✉️';
if(/render|image|design|room/.test(n))return '🎨';
return '🤖';
}
// ---- build world from /api/data ----
function build(d){
const offs=(d.officers&&d.officers.officers)||[];
const lf=(d.localFleet&&d.localFleet.procs)||[];
const km=(d.kamatera&&d.kamatera.procs)||[];
const cans=(d.canaries&&d.canaries.canaries)||[];
const ses=(d.sessions&&d.sessions.recent)||[];
commits=(d.activity&&d.activity.commits)||[];
// map each pm2 proc to an owning officer by keyword, else "Commons"
const officerFor=(name)=>{
const n=name.toLowerCase();
if(/dw|shopify|vendor|catalog|kravet|thibaut|scalamandre/.test(n))return 'vp-dw-commerce';
if(/insta|tiktok|social|marketing|reel/.test(n))return 'vp-dw-marketing';
if(/lawyer|doctor|directory|animals|ventura|corridor/.test(n))return 'vp-directories';
if(/security|firewall|secret|monitor/.test(n))return 'vp-security';
if(/wallpapersback|wallco|apartment|abramsego|pattern/.test(n))return 'vp-special-projects';
if(/cncp|pm2|keep|hawk|watch|dash|viewer|token/.test(n))return 'vp-operations';
return 'vp-operations';
};
// chambers = officers (+ a Sentries ring for canaries, + a Players nook for sessions)
const roster={};
offs.forEach(o=>roster[o.vp]={vp:o.vp,domain:o.domain,members:[]});
if(!roster['vp-operations'])roster['vp-operations']={vp:'vp-operations',domain:'Infra',members:[]};
const recent=new Set(commits.slice(0,12).map(c=>(c.repo||'').toLowerCase()));
[...lf.map(p=>({...p,loc:'Mac2'})),...km.map(p=>({...p,loc:'Kamatera'}))].forEach(p=>{
const vp=officerFor(p.name);
const working=(+p.cpu>3)|| [...recent].some(r=>p.name.toLowerCase().includes(r)||r.includes(p.name.toLowerCase().slice(0,6)));
(roster[vp]||roster['vp-operations']).members.push({
kind:'proc',name:p.name,status:p.status,cpu:+p.cpu||0,memMB:+p.memMB||0,
loc:p.loc,uptimeMs:+p.uptimeMs||0,restarts:p.restarts,working,idle:!working&&p.status==='online',
doing: working?('running · '+(p.cpu||0)+'% cpu'):(p.status==='online'?'idle':'DOWN')});
});
// canary sentries chamber
roster['__sentries']={vp:'Sentries',domain:'Canary watch — who watches the watchers',members:
cans.map(c=>({kind:'canary',name:c.name,verdict:c.verdict,bad:/FAIL|CRIT|DOWN/i.test(c.verdict||''),
ageMin:c.ageMin,stale:c.stale==='True'||c.stale===true,doing:(c.verdict||'?')+' · '+(c.ageMin||'?')+'m ago'}))};
// players (live sessions)
roster['__players']={vp:'Sessions',domain:'Live Claude sessions in flight',members:
ses.slice(0,8).map(s=>({kind:'session',name:(s.cwd||'').split('/').pop()||'session',
working:true,doing:'“'+(s.lastPrompt||'').slice(0,40)+'” · '+(s.msgCount||0)+' msgs',
branch:s.gitBranch,cwd:s.cwd,msgs:s.msgCount}))};
// layout chambers on a grid
const list=Object.values(roster).filter(r=>r.members.length);
const cols=Math.ceil(Math.sqrt(list.length));
const CW=460,CH=340,GAP=60;
chambers=list.map((r,i)=>{
const cx=(i%cols)*(CW+GAP), cy=Math.floor(i/cols)*(CH+GAP);
return {...r,x:cx,y:cy,w:CW,h:CH};
});
// place agents wandering inside their chamber
agents=[];
chambers.forEach(ch=>{
ch.members.forEach((m,j)=>{
const pad=54;
agents.push({...m,chamber:ch,
x:ch.x+pad+Math.random()*(ch.w-2*pad), y:ch.y+pad+Math.random()*(ch.h-2*pad),
vx:(Math.random()-.5)*.5, vy:(Math.random()-.5)*.5, ph:Math.random()*6.28, r:16});
});
});
// ---- alerts → spawn glitch monsters + explosions on NEW alerts ----
const alerts=agents.filter(a=>statusOf(a)==='bad');
const keyOf=a=>a.kind+':'+a.name;
const curKeys=new Set(alerts.map(keyOf));
// new alerts → explosion at that agent's chamber
alerts.forEach(a=>{if(!prevAlertKeys.has(keyOf(a))){burst(a.x,a.y,'#e5484d',26,3.2);burst(a.x,a.y,'#ffb020',14,2.2);sfx.boom();}});
// build monster list (one per active alert), engaged by the nearest sentry
const sentries=agents.filter(a=>a.kind==='canary'&&!a.bad);
monsters=alerts.map(a=>{
const prev=monsters.find(m=>m.key===keyOf(a));
const foe=sentries.length?sentries.reduce((b,s)=>Math.hypot(s.x-a.x,s.y-a.y)<Math.hypot(b.x-a.x,b.y-a.y)?s:b):null;
return prev? Object.assign(prev,{target:a,foe}):
{key:keyOf(a),target:a,foe,x:a.x+30,y:a.y-30,ph:Math.random()*6.28,hp:1};});
// cleared alerts → poof + tally
prevAlertKeys.forEach(k=>{if(!curKeys.has(k)){slain++;sfx.slain();}});
prevAlertKeys=curKeys;
S('sSlain',slain);
// hud
S('sChambers',chambers.length);S('sAgents',agents.length);
S('sWork',agents.filter(a=>statusOf(a)==='work').length);
S('sBad',agents.filter(a=>statusOf(a)==='bad').length);
S('sClock',new Date().toLocaleTimeString([],{hour:'numeric',minute:'2-digit'}));
// Ultron sits at the center of the whole map, wired to every chamber
const b=worldBounds();ultron.x=(b.x0+b.x1)/2;ultron.y=(b.y0+b.y1)/2;
if(!chambers._fit){resetView();chambers._fit=1;}
}
const S=(id,v)=>document.getElementById(id).textContent=v;
// ---- camera / interaction ----
function worldBounds(){let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;chambers.forEach(c=>{x0=Math.min(x0,c.x);y0=Math.min(y0,c.y);x1=Math.max(x1,c.x+c.w);y1=Math.max(y1,c.y+c.h)});return{x0,y0,x1,y1}}
function resetView(){if(!chambers.length)return;const b=worldBounds();const w=b.x1-b.x0,h=b.y1-b.y0;
cam.z=Math.min(innerWidth/(w+120),innerHeight/(h+160))*.95;cam.z=Math.max(.15,Math.min(cam.z,1.4));
cam.x=(b.x0+b.x1)/2 - innerWidth/2/cam.z; cam.y=(b.y0+b.y1)/2 - innerHeight/2/cam.z;}
function zoomBy(f){const cx=innerWidth/2,cy=innerHeight/2;const wx=cam.x+cx/cam.z,wy=cam.y+cy/cam.z;
cam.z=Math.max(.12,Math.min(cam.z*f,4));cam.x=wx-cx/cam.z;cam.y=wy-cy/cam.z;}
let drag=null;
cv.addEventListener('mousedown',e=>drag={x:e.clientX,y:e.clientY,cx:cam.x,cy:cam.y,moved:0});
addEventListener('mouseup',e=>{if(drag&&drag.moved<4)clickAt(e.clientX,e.clientY);drag=null;});
addEventListener('mousemove',e=>{
if(drag){cam.x=drag.cx-(e.clientX-drag.x)/cam.z;cam.y=drag.cy-(e.clientY-drag.y)/cam.z;drag.moved+=Math.abs(e.movementX)+Math.abs(e.movementY);}
else hoverAt(e.clientX,e.clientY);
});
cv.addEventListener('wheel',e=>{e.preventDefault();const f=e.deltaY<0?1.12:0.89;
const wx=cam.x+e.clientX/cam.z,wy=cam.y+e.clientY/cam.z;cam.z=Math.max(.12,Math.min(cam.z*f,4));
cam.x=wx-e.clientX/cam.z;cam.y=wy-e.clientY/cam.z;},{passive:false});
function screenToWorld(sx,sy){return{x:cam.x+sx/cam.z,y:cam.y+sy/cam.z}}
function agentAt(sx,sy){const w=screenToWorld(sx,sy);let best=null,bd=1e9;
agents.forEach(a=>{const d=Math.hypot(a.x-w.x,a.y-w.y);if(d<a.r+8&&d<bd){bd=d;best=a;}});return best;}
function chamberAt(sx,sy){const w=screenToWorld(sx,sy);
return chambers.find(c=>w.x>=c.x&&w.x<=c.x+c.w&&w.y>=c.y&&w.y<=c.y+c.h);}
function hoverAt(sx,sy){hover=agentAt(sx,sy);const tip=document.getElementById('tip');
if(hover){tip.style.display='block';tip.style.left=(sx+14)+'px';tip.style.top=(sy+14)+'px';
tip.innerHTML='<b>'+hover.name+'</b><br>'+(hover.doing||'');return;}
// chamber aggregate health
const ch=chamberAt(sx,sy);
if(ch){const mem=agents.filter(a=>a.chamber===ch);
const work=mem.filter(a=>statusOf(a)==='work').length,bad=mem.filter(a=>statusOf(a)==='bad').length;
const cpu=mem.filter(a=>a.kind==='proc').reduce((s,a)=>s+(a.cpu||0),0);
const hp=bad?'⚠ '+bad+' alert'+(bad>1?'s':''):'✓ all healthy';
tip.style.display='block';tip.style.left=(sx+14)+'px';tip.style.top=(sy+14)+'px';
tip.innerHTML='<b>'+(ch.vp||'').replace('__','')+'</b><br>'+mem.length+' agents · '+work+' working · '+
(cpu?cpu.toFixed(0)+'% cpu · ':'')+'<span style="color:'+(bad?'#e5484d':'#37d67a')+'">'+hp+'</span>';
}else tip.style.display='none';}
function clickAt(sx,sy){const a=agentAt(sx,sy);if(a){selected=a;openPanel(a);}else closePanel();}
// ---- detail panel (zoom-in on what an agent is doing) ----
function openPanel(a){
const st=statusOf(a),cls={online:'good',work:'work',idle:'idle',bad:'bad'}[st];
const role={officer:'Cabinet Officer',proc:'Process · '+(a.loc||''),canary:'Sentry / Canary',session:'Live Session'}[a.kind]||'Agent';
let kv='';
const add=(k,v)=>{if(v!==undefined&&v!==''&&v!==null)kv+='<div class="kv"><span>'+k+'</span><b>'+v+'</b></div>';};
if(a.kind==='proc'){add('Status',a.status);add('CPU',a.cpu+'%');add('Memory',a.memMB+' MB');
add('Uptime',a.uptimeMs?Math.round(a.uptimeMs/60000)+' min':'–');add('Restarts',a.restarts);add('Host',a.loc);}
if(a.kind==='canary'){add('Verdict',a.verdict);add('Last run',(a.ageMin||'?')+' min ago');add('Stale',a.stale?'yes ⚠':'no');}
if(a.kind==='session'){add('Branch',a.branch);add('Messages',a.msgs);add('Dir',(a.cwd||'').replace('/Users/stevestudio2/Projects/','~/'));}
if(a.kind==='officer'){add('Domain',a.domain);add('Reports',a.members?a.members.length:'–');}
document.getElementById('panelBody').innerHTML=
'<h2>'+iconFor(a)+' '+a.name+'</h2><div class="role">'+role+'</div>'+
'<span class="pill '+cls+'">'+st.toUpperCase()+'</span>'+
'<div class="doing"><div class="lbl">What it\'s doing</div>'+(a.doing||'—')+'</div>'+kv+
'<div class="kv"><span>Chamber</span><b>'+(a.chamber?a.chamber.vp:'–')+'</b></div>';
document.getElementById('panel').classList.add('open');
}
function closePanel(){selected=null;document.getElementById('panel').classList.remove('open');}
// ---- render loop ----
function draw(){
t+=1;updateFollow();
ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,W,H);
// ---- day/night backdrop (tied to real local time) ----
const now=new Date(),hr=now.getHours()+now.getMinutes()/60;
const day=Math.max(0,Math.sin((hr-6)/12*Math.PI)); // 0 night → 1 noon
const dusk=Math.max(0,1-Math.abs(hr-18)/3)+Math.max(0,1-Math.abs(hr-6.5)/2.5); // warm at dawn/dusk
const g=ctx.createLinearGradient(0,0,0,H);
const top=`rgb(${10+day*10+dusk*14|0},${8+day*7+dusk*4|0},${16+day*16|0})`;
const bot=`rgb(${8+day*4|0},${6+day*4|0},${12+day*8|0})`;
g.addColorStop(0,top);g.addColorStop(1,bot);ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
// torch-glow vignette at night
if(day<0.4){const vg=ctx.createRadialGradient(W/2,H/2,H*0.2,W/2,H/2,H*0.8);
vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,`rgba(0,0,0,${0.35*(1-day)})`);
ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);}
document.getElementById('sClock').textContent=(day>0.15?'☀ ':'🌙 ')+now.toLocaleTimeString([],{hour:'numeric',minute:'2-digit'});
ctx.setTransform(cam.z*DPR,0,0,cam.z*DPR,-cam.x*cam.z*DPR,-cam.y*cam.z*DPR);
// ---- Ultron orchestrator: edges to every chamber + directive/heartbeat packets ----
if(chambers.length){
ultron.ph+=0.04;
// edges (drawn under everything)
chambers.forEach(ch=>{const cx=ch.x+ch.w/2,cy=ch.y+ch.h/2;
const hot=chamberScore(ch)>3, alert=agents.some(a=>a.chamber===ch&&statusOf(a)==='bad');
ctx.strokeStyle=alert?'#e5484d40':hot?'#d4af6a55':'#3a305033';ctx.lineWidth=alert?2:1;
ctx.beginPath();ctx.moveTo(ultron.x,ultron.y);ctx.lineTo(cx,cy);ctx.stroke();});
// spawn packets: Ultron → busiest chamber (directive), chambers → Ultron (heartbeat)
if(t%22===0){const busy=chambers.reduce((a,c)=>chamberScore(c)>chamberScore(a)?c:a,chambers[0]);
packet(ultron.x,ultron.y,busy.x+busy.w/2,busy.y+busy.h/2,'#5b8cff');}
if(t%14===0){const c=chambers[(Math.random()*chambers.length)|0];
packet(c.x+c.w/2,c.y+c.h/2,ultron.x,ultron.y, agents.some(a=>a.chamber===c&&statusOf(a)==='bad')?'#e5484d':'#37d67a');}
// draw packets
packets.forEach(p=>{const e=p.tt<.5?2*p.tt*p.tt:1-Math.pow(-2*p.tt+2,2)/2; // ease
const x=p.ax+(p.bx-p.ax)*p.tt,y=p.ay+(p.by-p.ay)*p.tt;
ctx.fillStyle=p.color;ctx.globalAlpha=.9;ctx.beginPath();ctx.arc(x,y,3,0,6.28);ctx.fill();
ctx.globalAlpha=.3;ctx.beginPath();ctx.arc(x,y,6,0,6.28);ctx.fill();ctx.globalAlpha=1;});
updatePackets();
// Ultron core node
const pr=26+Math.sin(ultron.ph)*3;
ctx.beginPath();ctx.arc(ultron.x,ultron.y,pr+14,0,6.28);ctx.fillStyle='#d4af6a18';ctx.fill();
ctx.beginPath();ctx.arc(ultron.x,ultron.y,pr,0,6.28);ctx.fillStyle='#120d1e';ctx.fill();
ctx.lineWidth=3;ctx.strokeStyle='#d4af6a';ctx.stroke();
ctx.font='26px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('🧠',ultron.x,ultron.y+1);
ctx.fillStyle='#d4af6a';ctx.font='700 12px Avenir Next,sans-serif';ctx.fillText('ULTRON',ultron.x,ultron.y+pr+12);
ctx.fillStyle='#8a7fa8';ctx.font='10px Avenir Next,sans-serif';ctx.fillText('orchestrator · keeps the fleet busy',ultron.x,ultron.y+pr+26);ctx.textAlign='left';
}
// chambers
chambers.forEach(ch=>{
ctx.fillStyle='#14101e';ctx.strokeStyle='#2a2038';ctx.lineWidth=2;
roundRect(ch.x,ch.y,ch.w,ch.h,14);ctx.fill();ctx.stroke();
// floor tiles
ctx.strokeStyle='#191322';ctx.lineWidth=1;
for(let gx=ch.x+30;gx<ch.x+ch.w;gx+=38){ctx.beginPath();ctx.moveTo(gx,ch.y+30);ctx.lineTo(gx,ch.y+ch.h-14);ctx.stroke();}
for(let gy=ch.y+34;gy<ch.y+ch.h;gy+=38){ctx.beginPath();ctx.moveTo(ch.x+14,gy);ctx.lineTo(ch.x+ch.w-14,gy);ctx.stroke();}
// nameplate
ctx.fillStyle='#d4af6a';ctx.font='600 17px Avenir Next,sans-serif';ctx.textBaseline='top';
ctx.fillText((ch.vp||'').replace('vp-','').replace('__',''),ch.x+16,ch.y+12);
ctx.fillStyle='#8a7fa8';ctx.font='12px Avenir Next,sans-serif';
ctx.fillText((ch.domain||'').slice(0,54),ch.x+16,ch.y+32);
});
// handoff journeys: occasionally a working agent carries work to another chamber
if(t%40===0){const workers=agents.filter(a=>statusOf(a)==='work'&&!a.travel&&a.kind==='proc');
if(workers.length&&chambers.length>1&&Math.random()<0.6){
const a=workers[(Math.random()*workers.length)|0];
let dest=chambers[(Math.random()*chambers.length)|0];let g=0;
while(dest===a.chamber&&g++<5)dest=chambers[(Math.random()*chambers.length)|0];
a.travel={dest,home:a.chamber,phase:'go',hx:a.x,hy:a.y};}}
// agents (wander/travel + draw)
agents.forEach(a=>{
const ch=a.chamber,pad=40;
if(a.travel){
const tg=a.travel.phase==='go'?a.travel.dest:a.travel.home;
const tx=tg.x+tg.w/2, ty=tg.y+tg.h/2;
const dx=tx-a.x, dy=ty-a.y, dist=Math.hypot(dx,dy);
a.x+=dx/dist*3.2; a.y+=dy/dist*3.2;
if((t>>1)%2===0)particles.push({x:a.x,y:a.y,vx:0,vy:0,life:.5,color:'#5b8cff',r:2}); // trail
if(dist<26){
if(a.travel.phase==='go'){a.travel.phase='return';burst(tx,ty,'#5b8cff',10,1.6);sfx.deliver();} // deliver
else{a.chamber=a.travel.home;a.x=a.travel.hx;a.y=a.travel.hy;a.travel=null;}}
} else {
// wander within chamber
a.ph+=0.03;a.vx+=Math.cos(a.ph)*.03+(Math.random()-.5)*.04;a.vy+=Math.sin(a.ph*1.3)*.03+(Math.random()-.5)*.04;
a.vx*=.94;a.vy*=.94;a.x+=a.vx;a.y+=a.vy;
if(a.x<ch.x+pad){a.x=ch.x+pad;a.vx=Math.abs(a.vx);}if(a.x>ch.x+ch.w-pad){a.x=ch.x+ch.w-pad;a.vx=-Math.abs(a.vx);}
if(a.y<ch.y+pad+18){a.y=ch.y+pad+18;a.vy=Math.abs(a.vy);}if(a.y>ch.y+ch.h-pad){a.y=ch.y+ch.h-pad;a.vy=-Math.abs(a.vy);}
}
const st=statusOf(a),col=COL[st];
// status glow / working pulse
const pulse=st==='work'?(1+Math.sin(t*.12+a.ph)*.25):1;
ctx.beginPath();ctx.arc(a.x,a.y,a.r*1.7*pulse,0,6.28);
ctx.fillStyle=col+'22';ctx.fill();
// body
ctx.beginPath();ctx.arc(a.x,a.y,a.r,0,6.28);ctx.fillStyle='#0d0a16';ctx.fill();
ctx.lineWidth=2.5;ctx.strokeStyle=col;ctx.stroke();
if(a===selected||a===hover){ctx.lineWidth=2;ctx.strokeStyle='#d4af6a';ctx.beginPath();ctx.arc(a.x,a.y,a.r+5,0,6.28);ctx.stroke();}
// icon
ctx.font=(a.r+2)+'px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(iconFor(a),a.x,a.y+1);ctx.textAlign='left';
// working action bubble
if(st==='work'&&(t>>4)%2===0){ctx.font='11px sans-serif';ctx.fillStyle='#5b8cff';
ctx.fillText('•••',a.x+a.r,a.y-a.r);}
if(st==='bad'){ctx.font='12px sans-serif';ctx.fillText('!',a.x+a.r-2,a.y-a.r);}
// live sessions "type": blinking cursor + code particles floating up
if(a.kind==='session'){
if((t>>4)%2===0){ctx.fillStyle='#37d67a';ctx.font='bold 12px monospace';ctx.fillText('▌',a.x+a.r-1,a.y-a.r+2);}
if(t%12===0){particles.push({x:a.x+(Math.random()-.5)*14,y:a.y-a.r,vx:(Math.random()-.5)*.3,vy:-.8,life:1,color:'#37d67a',r:1.6});sfx.type();}
}
// label when zoomed in enough
if(cam.z>0.5){ctx.fillStyle='#8a7fa8';ctx.font='10px Avenir Next,sans-serif';ctx.textAlign='center';
ctx.fillText(a.name.slice(0,16),a.x,a.y+a.r+11);ctx.textAlign='left';}
// chatter speech bubbles (real commits + role lines) — only when zoomed in
if(!a.bubble && cam.z>0.42 && Math.random()<(st==='bad'?0.010:st==='work'?0.006:0.0018))
a.bubble={text:chatterFor(a),life:130};
if(a.bubble){a.bubble.life--;if(a.bubble.life<=0){a.bubble=null;}
else{const b=a.bubble,tw=ctx.measureText(b.text).width;ctx.font='11px Avenir Next,sans-serif';
const w=Math.min(220,ctx.measureText(b.text).width+16),bx=a.x-w/2,byy=a.y-a.r-26;
ctx.globalAlpha=Math.min(1,b.life/20);
ctx.fillStyle='#0d0a16ee';ctx.strokeStyle=st==='bad'?'#e5484d':'#3a3050';ctx.lineWidth=1;
roundRect(bx,byy,w,20,6);ctx.fill();ctx.stroke();
ctx.beginPath();ctx.moveTo(a.x-4,byy+20);ctx.lineTo(a.x+4,byy+20);ctx.lineTo(a.x,byy+26);ctx.closePath();ctx.fillStyle='#0d0a16ee';ctx.fill();
ctx.fillStyle=st==='bad'?'#e5a0a2':'#cdbfe6';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(b.text,a.x,byy+10);ctx.textAlign='left';ctx.globalAlpha=1;}}
});
// ---- monsters + combat ----
monsters.forEach(m=>{
const tg=m.target;m.ph+=0.08;m.x+=((tg.x+34)-m.x)*.06;m.y+=((tg.y-34)-m.y)*.06;
const by=m.y+Math.sin(m.ph)*4;
// a sentry engages: it advances on the monster and trades hit-sparks
if(m.foe){const f=m.foe;const dx=m.x-f.x,dy=by-f.y,d=Math.hypot(dx,dy);
if(d>60){f.x+=dx/d*1.6;f.y+=dy/d*1.6;} // charge in
else if(t%18===0){ // clash
const mx=(f.x+m.x)/2,my=(f.y+by)/2;burst(mx,my,'#ffd24a',6,2);burst(mx,my,'#e5484d',4,1.6);sfx.clash();}
// sword line
ctx.strokeStyle='#ffd24a55';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(f.x,f.y);ctx.lineTo(m.x,by);ctx.stroke();}
// menace aura + body
ctx.beginPath();ctx.arc(m.x,by,20,0,6.28);ctx.fillStyle='#e5484d22';ctx.fill();
ctx.font='26px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText('👾',m.x,by);
// threat label
ctx.fillStyle='#e5484d';ctx.font='700 10px Avenir Next,sans-serif';
ctx.fillText((tg.verdict||tg.status||'ALERT').toString().toUpperCase(),m.x,by-24);ctx.textAlign='left';
});
drawParticles();updateParticles();
if(t%3===0)drawMini();
requestAnimationFrame(draw);
}
function roundRect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);
ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
// ---- data ----
async function load(){try{const r=await fetch('/api/data',{headers:AUTH?{Authorization:AUTH}:{}});
const d=await r.json();build(d);}catch(e){console.error(e);}}
load();setInterval(load,15000);draw();
</script></body></html>