[object Object]

← back to AbramsEgo

feat: The Dungeon — top-down game view of the agent fleet (officers=chambers, procs=workers, canaries=sentries, sessions=players; wandering avatars, status glow, click-to-zoom shows what each agent is doing) + /dungeon route

799648dcefa04e104229c369829c229e6f4ca403 · 2026-07-02 07:43:21 -0700 · Steve

Files touched

Diff

commit 799648dcefa04e104229c369829c229e6f4ca403
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 2 07:43:21 2026 -0700

    feat: The Dungeon — top-down game view of the agent fleet (officers=chambers, procs=workers, canaries=sentries, sessions=players; wandering avatars, status glow, click-to-zoom shows what each agent is doing) + /dungeon route
---
 data/local-fleet.json |   2 +-
 public/dungeon.html   | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js             |   5 +
 3 files changed, 262 insertions(+), 1 deletion(-)

diff --git a/data/local-fleet.json b/data/local-fleet.json
index 2d2a409..7f00e85 100644
--- a/data/local-fleet.json
+++ b/data/local-fleet.json
@@ -1 +1 @@
-{"fetchedAt":"2026-07-02T14:13:08.168Z","procs":[{"name":"api-token-dashboard","status":"online","cpu":0,"memMB":39,"restarts":null,"uptimeMs":54946000,"startedAt":"2026-07-01T22:57:22.168Z"},{"name":"abramsego","status":"stopped","cpu":null,"memMB":null,"restarts":null,"uptimeMs":null,"startedAt":null}]}
\ No newline at end of file
+{"fetchedAt":"2026-07-02T14:43:11.913Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0,"memMB":27,"restarts":0,"uptimeMs":1662816,"startedAt":"2026-07-02T14:15:29.097Z"},{"name":"abramsego","status":"online","cpu":0,"memMB":75,"restarts":9,"uptimeMs":1253,"startedAt":"2026-07-02T14:43:10.660Z"},{"name":"api-token-dashboard","status":"online","cpu":0,"memMB":29,"restarts":0,"uptimeMs":1541202,"startedAt":"2026-07-02T14:17:30.711Z"},{"name":"pattern-vault","status":"online","cpu":0,"memMB":31,"restarts":1,"uptimeMs":1030432,"startedAt":"2026-07-02T14:26:01.481Z"},{"name":"agent-cabinet","status":"online","cpu":0,"memMB":30,"restarts":0,"uptimeMs":1538478,"startedAt":"2026-07-02T14:17:33.435Z"},{"name":"astek-landing","status":"online","cpu":0,"memMB":97,"restarts":0,"uptimeMs":35716,"startedAt":"2026-07-02T14:42:36.197Z"}]}
\ No newline at end of file
diff --git a/public/dungeon.html b/public/dungeon.html
new file mode 100644
index 0000000..b152151
--- /dev/null
+++ b/public/dungeon.html
@@ -0,0 +1,256 @@
+<!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}
+</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" 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>
+<div id="zoombar"><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;
+const AUTH=''; // same-origin, cookie/basic-auth already present
+
+// 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)||[];
+  const 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});
+    });
+  });
+  // 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'}));
+  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 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||'');}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;ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,W,H);
+  ctx.setTransform(cam.z*DPR,0,0,cam.z*DPR,-cam.x*cam.z*DPR,-cam.y*cam.z*DPR);
+  // 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);
+  });
+  // agents (wander + draw)
+  agents.forEach(a=>{
+    // 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;
+    const ch=a.chamber,pad=40;
+    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);}
+    // 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';}
+  });
+  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();
diff --git a/server.js b/server.js
index 9c2700c..ef0524d 100644
--- a/server.js
+++ b/server.js
@@ -1076,6 +1076,11 @@ app.post('/api/chat', async (req, res) => {
 const LANDING_FILE = path.join(__dirname, 'public', 'landing.html');
 app.get('/landing', (req, res) => res.sendFile(LANDING_FILE));
 
+// --- The Dungeon: top-down game view of the whole agent fleet ---------------
+// Each officer = a chamber, pm2 procs = workers, canaries = sentries, live
+// sessions = players; avatars wander + click-to-zoom shows what each is doing.
+app.get('/dungeon', (req, res) => res.sendFile(path.join(__dirname, 'public', 'dungeon.html')));
+
 // --- billable_work engine (engine 3): sellable Fleet Health Report ----------
 // The dashboard's own snapshot rendered as a polished, self-contained HTML
 // artifact a customer would pay for. /api/report/fleet-health = real data,

← 0abb0a5 auto-save: 2026-07-02T07:15:18 (1 files) — data/local-fleet.  ·  back to AbramsEgo  ·  auto-save: 2026-07-02T07:46:12 (1 files) — data/local-fleet. 97535d2 →