← back to AbramsEgo
feat(dungeon): chamber health-on-hover (agents/working/cpu/alerts per officer), real chatter bubbles (commit subjects + role-flavored lines), day/night backdrop tied to local time (dawn/dusk warmth + night torch vignette + ☀/🌙 clock), follow-cam auto-tour of the busiest chamber (🎥 toggle)
99d82e98cb36c68721bbfbc2a9e9dcb6e02b77ec · 2026-07-02 08:05:52 -0700 · Steve
Files touched
M data/local-fleet.jsonM public/dungeon.html
Diff
commit 99d82e98cb36c68721bbfbc2a9e9dcb6e02b77ec
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 2 08:05:52 2026 -0700
feat(dungeon): chamber health-on-hover (agents/working/cpu/alerts per officer), real chatter bubbles (commit subjects + role-flavored lines), day/night backdrop tied to local time (dawn/dusk warmth + night torch vignette + ☀/🌙 clock), follow-cam auto-tour of the busiest chamber (🎥 toggle)
---
data/local-fleet.json | 2 +-
public/dungeon.html | 92 ++++++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 88 insertions(+), 6 deletions(-)
diff --git a/data/local-fleet.json b/data/local-fleet.json
index 4b37e24..c611513 100644
--- a/data/local-fleet.json
+++ b/data/local-fleet.json
@@ -1 +1 @@
-{"fetchedAt":"2026-07-02T15:02:36.091Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0.2,"memMB":27,"restarts":0,"uptimeMs":2826994,"startedAt":"2026-07-02T14:15:29.097Z"},{"name":"abramsego","status":"online","cpu":0.7,"memMB":49,"restarts":10,"uptimeMs":901055,"startedAt":"2026-07-02T14:47:35.036Z"},{"name":"api-token-dashboard","status":"online","cpu":0.2,"memMB":25,"restarts":0,"uptimeMs":2705380,"startedAt":"2026-07-02T14:17:30.711Z"},{"name":"pattern-vault","status":"online","cpu":0.1,"memMB":26,"restarts":1,"uptimeMs":2194610,"startedAt":"2026-07-02T14:26:01.481Z"},{"name":"agent-cabinet","status":"online","cpu":0.1,"memMB":25,"restarts":0,"uptimeMs":2702656,"startedAt":"2026-07-02T14:17:33.435Z"},{"name":"astek-landing","status":"online","cpu":0.1,"memMB":38,"restarts":0,"uptimeMs":1199894,"startedAt":"2026-07-02T14:42:36.197Z"}]}
\ No newline at end of file
+{"fetchedAt":"2026-07-02T15:05:46.805Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0,"memMB":25,"restarts":0,"uptimeMs":3017708,"startedAt":"2026-07-02T14:15:29.097Z"},{"name":"abramsego","status":"online","cpu":8,"memMB":64,"restarts":11,"uptimeMs":180531,"startedAt":"2026-07-02T15:02:46.274Z"},{"name":"api-token-dashboard","status":"online","cpu":0,"memMB":25,"restarts":0,"uptimeMs":2896094,"startedAt":"2026-07-02T14:17:30.711Z"},{"name":"pattern-vault","status":"online","cpu":0,"memMB":65,"restarts":2,"uptimeMs":11205,"startedAt":"2026-07-02T15:05:35.600Z"},{"name":"agent-cabinet","status":"online","cpu":0,"memMB":25,"restarts":0,"uptimeMs":2893370,"startedAt":"2026-07-02T14:17:33.435Z"},{"name":"astek-landing","status":"online","cpu":0,"memMB":37,"restarts":0,"uptimeMs":1390608,"startedAt":"2026-07-02T14:42:36.197Z"}]}
\ No newline at end of file
diff --git a/public/dungeon.html b/public/dungeon.html
index f046c7d..1b89f9a 100644
--- a/public/dungeon.html
+++ b/public/dungeon.html
@@ -54,7 +54,7 @@ canvas{display:block;cursor:grab}canvas:active{cursor:grabbing}
<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="sfxBtn" onclick="toggleSfx()" title="sound">🔇</button><button onclick="zoomBy(1.2)">+</button><button onclick="zoomBy(0.83)">−</button><button onclick="resetView()">⌂</button></div>
+<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';}
@@ -102,7 +102,51 @@ function drawMini(){const w=mini.width=200,h=mini.height=132;mctx.clearRect(0,0,
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;});
+ 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){
@@ -135,7 +179,7 @@ function build(d){
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)||[];
+ 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();
@@ -234,9 +278,21 @@ cv.addEventListener('wheel',e=>{e.preventDefault();const f=e.deltaY<0?1.12:0.89;
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||'');}else tip.style.display='none';}
+ 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) ----
@@ -261,7 +317,21 @@ function closePanel(){selected=null;document.getElementById('panel').classList.r
// ---- render loop ----
function draw(){
- t+=1;ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,W,H);
+ 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);
// chambers
chambers.forEach(ch=>{
@@ -327,6 +397,18 @@ function draw(){
// 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=>{
← 08e18a2 feat(dungeon): sound FX (WebAudio synth — combat clash, deli
·
back to AbramsEgo
·
fix(dungeon): add missing </script></body></html> — untermin 8a165c0 →