[object Object]

← back to AbramsEgo

style: apply design-research top items (androoagi/norma patterns)

63d36862d24fc320a65346832c1c0856387d3556 · 2026-07-01 18:45:57 -0700 · Steve

- top metrics bar: segmented 10-cell gauges + bracketed [CPU]-style labels,
  cell fill keeps good/warn/bad threshold colors
- stable per-agent golden-angle hue chips (agentHue) on local fleet, Kamatera
  fleet, activity repos, sub-agent run projects
- [ PANEL NAME ] boxed terminal-style headers on every card (h2::before/after)
- click-to-focus: panel glows, siblings dim; Esc or re-click clears
- memo: items 1-4 marked ✅, item 5 noted server-side-blocked

Hard rules spot-checked live (Chromium): 177 🕓 chips, 5 sort+density bars,
cost labels intact, 0 pageerrors on index + landing.

Files touched

Diff

commit 63d36862d24fc320a65346832c1c0856387d3556
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 18:45:57 2026 -0700

    style: apply design-research top items (androoagi/norma patterns)
    
    - top metrics bar: segmented 10-cell gauges + bracketed [CPU]-style labels,
      cell fill keeps good/warn/bad threshold colors
    - stable per-agent golden-angle hue chips (agentHue) on local fleet, Kamatera
      fleet, activity repos, sub-agent run projects
    - [ PANEL NAME ] boxed terminal-style headers on every card (h2::before/after)
    - click-to-focus: panel glows, siblings dim; Esc or re-click clears
    - memo: items 1-4 marked ✅, item 5 noted server-side-blocked
    
    Hard rules spot-checked live (Chromium): 177 🕓 chips, 5 sort+density bars,
    cost labels intact, 0 pageerrors on index + landing.
---
 memos/design-research.md | 26 ++++++++++++++------
 public/index.html        | 63 +++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 69 insertions(+), 20 deletions(-)

diff --git a/memos/design-research.md b/memos/design-research.md
index 6944866..b52d855 100644
--- a/memos/design-research.md
+++ b/memos/design-research.md
@@ -280,14 +280,24 @@ are tracked per message so you always know what is working"*).
 
 ## Apply next (updated 2026-07-01, task 31 — supersedes the task-30 list)
 
-1. Segmented 10-cell gauges + bracketed monospace labels in the top metrics
-   bar (keep threshold colors).
-2. Per-agent golden-angle hue chips in Fleet health, reused everywhere that
-   agent appears.
-3. `[ PANEL NAME ]` boxed headers with a per-panel summary metrics row
-   (count / healthy% / live N).
-4. Focus state: click card → glow + dim siblings.
-5. One-stat caption line on session/sub-agent cards (model · tokens · $).
+1. ✅ Segmented 10-cell gauges + bracketed monospace labels in the top metrics
+   bar (keep threshold colors). *(task 40 — CPU/RAM/SWAP/DISK now render a
+   10-cell `.seg` gauge under the value, labels bracketed `[CPU]`, cell fill
+   inherits the good/warn/bad threshold class.)*
+2. ✅ Per-agent golden-angle hue chips in Fleet health, reused everywhere that
+   agent appears. *(task 40 — `agentHue()` name-hash × 137.508° → stable hsl
+   chip on local-fleet rows, Kamatera-fleet rows, activity commit repos, and
+   sub-agent run projects.)*
+3. ✅ `[ PANEL NAME ]` boxed headers with a per-panel summary metrics row
+   (count / healthy% / live N). *(task 40 — CSS `h2::before/::after` brackets
+   frame every panel header edge-to-edge; the per-panel summary metrics rows
+   already lived in each h2's muted span.)*
+4. ✅ Focus state: click card → glow + dim siblings. *(task 40 — click a panel
+   → accent glow, siblings dim to .45; click again or Esc clears; clicks on
+   links/inputs/sliders inside panels are ignored.)*
+5. One-stat caption line on session/sub-agent cards (model · tokens · $) —
+   *needs the sessions collector to expose model+tokens per card; server-side,
+   not done in task 40's frontend pass.*
 6. Slide-in + source-panel pulse on new activity-feed items.
 7. Build-loop **per-task manifest JSON** (status / derived_state /
    current_blocker / lifecycle events / commit) written by build-loop.sh,
diff --git a/public/index.html b/public/index.html
index 04b5ca9..2bba8ce 100644
--- a/public/index.html
+++ b/public/index.html
@@ -28,6 +28,19 @@
     border-radius:12px;padding:14px 16px;min-height:60px}
   .card h2{margin:0 0 10px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);
     display:flex;align-items:center;gap:8px}
+  /* androoagi "[ OUTPUT TERMINAL ]" boxed-label panel headers */
+  .card h2::before{content:'[';color:#3b4f68;font-weight:400}
+  .card h2::after{content:']';color:#3b4f68;font-weight:400;margin-left:auto}
+  /* androoagi segmented 10-cell mini-gauges (top metrics bar) */
+  .seg{display:inline-flex;gap:1.5px;margin-top:2px}
+  .seg i{width:5px;height:7px;border-radius:1px;background:#ffffff14}
+  .seg i.on{background:currentColor}
+  /* stable per-agent hue chip (golden-angle) — same entity, same color, every panel */
+  .hue{display:inline-block;width:7px;height:7px;border-radius:2px;margin-right:5px;flex-shrink:0;vertical-align:baseline}
+  /* androoagi focus cue: clicked panel glows, siblings dim */
+  main.focused section.card{opacity:.45;transition:opacity .15s ease}
+  main.focused section.card.focus{opacity:1;border-color:#4d9fff88;
+    box-shadow:0 0 0 1px #4d9fff33,0 0 26px -8px #4d9fff99}
   .span12{grid-column:span 12}.span8{grid-column:span 8}.span7{grid-column:span 7}.span6{grid-column:span 6}
   .span5{grid-column:span 5}.span4{grid-column:span 4}.span3{grid-column:span 3}
   @media(max-width:1000px){.span8,.span7,.span6,.span5,.span4,.span3{grid-column:span 12}}
@@ -283,16 +296,21 @@ function feedGrid(name, items){ const g=GRIDS[name]; if(!g) return; g.items=Arra
 async function load(){
   let d; try{ d = await (await fetch('/api/data')).json(); }catch(e){ return; }
   if(!d || !d.builtAt) return;
-  // metrics bar
+  // metrics bar — segmented 10-cell gauges + bracketed labels (androoagi);
+  // cell fill keeps the threshold colors (green must still mean healthy)
   const s=d.system||{};
+  const gauge=(pct)=>{ if(pct==null) return '';
+    const on=Math.max(0,Math.min(10,Math.round(pct/10)));
+    return `<span class="seg">${Array.from({length:10},(_,i)=>`<i${i<on?' class="on"':''}></i>`).join('')}</span>`; };
+  const swapMB=(s.swap && s.swap.usedMB!=null)?s.swap.usedMB:null;
   $('metrics').innerHTML = [
-    ['CPU', (s.loadPct??'—')+'%', cls(s.loadPct,60,85)],
-    ['RAM', (s.memUsedPct??'—')+'%', cls(s.memUsedPct,70,88)],
-    ['SWAP', (s.swap && s.swap.usedMB!=null) ? (s.swap.usedMB>=1024 ? (s.swap.usedMB/1024).toFixed(1)+'G' : Math.round(s.swap.usedMB)+'M') : '—', (s.swap && s.swap.usedMB!=null) ? cls(s.swap.usedMB, 2048, 8192) : ''],
-    ['DISK', (s.diskUsedPct??'—')+'%', cls(s.diskUsedPct,80,90)],
-    ['NODE', s.node||'—',''],
-    ['UP', fmtDur(s.dashboardUptimeSec)||'—',''],
-  ].map(([k,v,c])=>`<div class="metric"><div class="v ${c}">${v}</div><div class="k">${k}</div></div>`).join('');
+    ['CPU', (s.loadPct??'—')+'%', cls(s.loadPct,60,85), s.loadPct],
+    ['RAM', (s.memUsedPct??'—')+'%', cls(s.memUsedPct,70,88), s.memUsedPct],
+    ['SWAP', swapMB!=null ? (swapMB>=1024 ? (swapMB/1024).toFixed(1)+'G' : Math.round(swapMB)+'M') : '—', swapMB!=null ? cls(swapMB, 2048, 8192) : '', swapMB!=null?Math.min(100,swapMB/8192*100):null],
+    ['DISK', (s.diskUsedPct??'—')+'%', cls(s.diskUsedPct,80,90), s.diskUsedPct],
+    ['NODE', s.node||'—','',null],
+    ['UP', fmtDur(s.dashboardUptimeSec)||'—','',null],
+  ].map(([k,v,c,g])=>`<div class="metric ${c}"><div class="v">${v}</div>${gauge(g)}<div class="k">[${k}]</div></div>`).join('');
   $('refresh').textContent = 'snapshot '+new Date(d.builtAt).toLocaleTimeString()+' · auto 15s · $0 (local)'+(d.version?' · v'+d.version:'');
 
   // ---- P&L ----
@@ -340,7 +358,7 @@ async function load(){
   $('kam-up').innerHTML=`<span class="${cls(100-(k.up/(k.total||1)*100),20,50)}">${k.up??'—'}</span><span class="dim" style="font-size:16px">/${k.total??'—'} up</span> `+(whenChip(k.fetchedAt)||'')+(k.note?` <span class="warn" style="font-size:11px" title="${esc(k.error||'')}">⚠ ${esc(k.note)}</span>`:'');
   renderKamFleet(k);
   $('loc-up').innerHTML=`<span class="good">${l.up??'—'}</span><span class="dim" style="font-size:16px">/${l.total??'—'} up</span>`+(l.note?` <span class="warn" style="font-size:11px" title="${esc(l.error||'')}">⚠ ${esc(l.note)}</span>`:'');
-  $('loc-list').innerHTML=(l.procs||[]).map(pr=>lrow(`${statusDot(pr.status)}${esc(pr.name)}`,`${pr.memMB!=null?pr.memMB+'M':''}`,whenChip(pr.startedAt))).join('')||`<span class="muted">${esc(l.note||'no local pm2 procs')}</span>`;
+  $('loc-list').innerHTML=(l.procs||[]).map(pr=>lrow(`${statusDot(pr.status)}${hueChip(pr.name)}${esc(pr.name)}`,`${pr.memMB!=null?pr.memMB+'M':''}`,whenChip(pr.startedAt))).join('')||`<span class="muted">${esc(l.note||'no local pm2 procs')}</span>`;
 
   // ---- canaries ---- (sort + density + infinite scroll like every DW grid)
   const cans=(d.canaries&&d.canaries.canaries)||[];
@@ -471,7 +489,7 @@ function renderUsage(d){
       {k:'model',label:'Model A→Z',   cmp:(a,b)=>(a.model||'').localeCompare(b.model||'')},
     ],
     render:(r)=>`<div class="subcard" title="${esc(r.startedAt||r.updatedAt||'')}">
-      <div class="subcard-top"><span class="subcard-proj">${esc(r.project||'?')}</span>`+
+      <div class="subcard-top"><span class="subcard-proj">${hueChip(r.project)}${esc(r.project||'?')}</span>`+
       `<span class="pill">${esc(r.model||'unknown')}</span></div>`+
       `<div class="subcard-task">${esc(r.task||'')}</div>`+
       `<div class="subcard-when">${whenChip(r.startedAt,r.updatedAt)||'🕓 —'}</div></div>`,
@@ -494,7 +512,7 @@ function renderActivity(d){
       {k:'repo',label:'Repo A→Z',cmp:(x,y)=>(x.repo||'').localeCompare(y.repo||'')||tsOf(y.date)-tsOf(x.date)},
     ],
     render:(cm)=>`<div class="subcard" title="${esc(cm.date||'')}">
-      <div class="subcard-top"><span class="subcard-proj">${esc(cm.repo||'?')}</span>`+
+      <div class="subcard-top"><span class="subcard-proj">${hueChip(cm.repo)}${esc(cm.repo||'?')}</span>`+
       `<span class="pill">${esc(cm.hash||'')}</span></div>`+
       `<div class="subcard-task">${esc(cm.subject||'')}</div>`+
       `<div class="subcard-when">${whenChip(cm.date)||'🕓 —'}${cm.author?' · '+esc(cm.author):''}</div></div>`,
@@ -578,13 +596,34 @@ function renderKamFleet(k){
       {k:'restarts',label:'Restarts ↓', cmp:(a,b)=>(b.restarts||0)-(a.restarts||0)},
       {k:'status',  label:'Status',     cmp:(a,b)=>srank(a)-srank(b)||(a.name||'').localeCompare(b.name||'')},
     ],
-    render:(pr)=>lrow(`${statusDot(pr.status)}${esc(pr.name)}`,`${pr.restarts??''}↺`,whenChip(pr.startedAt)),
+    render:(pr)=>lrow(`${statusDot(pr.status)}${hueChip(pr.name)}${esc(pr.name)}`,`${pr.restarts??''}↺`,whenChip(pr.startedAt)),
   });
   // empty text can change per snapshot (unreachable note vs default)
   GRIDS['kamfleet'].cfg.empty=(k.note||'kamatera unreachable');
   feedGrid('kamfleet', k.procs||[]);
 }
 function statusDot(st){const c=st==='online'?'var(--good)':st?'var(--bad)':'var(--dim)';return `<span class="dot" style="background:${c}"></span>`;}
+// stable per-agent identity color: name hash spun around the golden angle
+// (137.508°, same trick as /color) so every process/repo keeps one hue everywhere
+function agentHue(name){let h=0;const s=String(name||'');for(let i=0;i<s.length;i++)h=(h*31+s.charCodeAt(i))>>>0;return Math.round((h%997)*137.508)%360;}
+function hueChip(name){return `<span class="hue" style="background:hsl(${agentHue(name)},62%,56%)"></span>`;}
+// click a panel → it glows, siblings dim (click again or Esc to clear);
+// clicks on links/controls inside a panel are left alone
+document.getElementById('main').addEventListener('click',(e)=>{
+  const card=e.target.closest('section.card'); if(!card) return;
+  if(e.target.closest('a,select,input,button,textarea,label')) return;
+  const main=document.getElementById('main');
+  const was=card.classList.contains('focus');
+  main.querySelectorAll('section.card.focus').forEach(c=>c.classList.remove('focus'));
+  main.classList.toggle('focused',!was);
+  if(!was) card.classList.add('focus');
+});
+document.addEventListener('keydown',(e)=>{
+  if(e.key!=='Escape') return;
+  const main=document.getElementById('main');
+  main.classList.remove('focused');
+  main.querySelectorAll('section.card.focus').forEach(c=>c.classList.remove('focus'));
+});
 function fmtDur(s){if(s==null)return null;if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m';return Math.floor(s/3600)+'h';}
 function esc(x){return (x==null?'':String(x)).replace(/[<>&]/g,m=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[m]));}
 

← 1c59231 auto-save: 2026-07-01T18:38:06 (8 files) — data/local-fleet.  ·  back to AbramsEgo  ·  auto-save: 2026-07-01T19:08:14 (10 files) — build-queue/task fb9b33f →