← Commit history

Dashboard brought into adom-ui-design compliance (John: 'did you follow our ui design skills when making your app? what does it say about headers and icons?' - honest answer was no, so now it does). HEADER (skill section 5): replaced the pill-soup row with the canonical 44px single-row three-zone header - brand zone left (16px monochrome currentColor icon + 'pup · live windows' in Familjen Grotesk 600 accent, with the app-info POPOVER on 600ms hover / 220ms hide grace carrying version, platform, bridge uptime, deps, and the clickable wiki link), 1px hairline, subject zone center (subject name + muted JetBrains Mono meta line with windows/tabs/icons/LED/updated, hidden below 720px), icon-button actions right (flash-all / clear / close-all as monochrome stroke SVGs with tooltips + aria-labels). The VERSION IS OUT OF THE ROW (the skill explicitly bans it inline; popover only). Filter + search moved to a toolbar bar below. ICONS (section 9): 'monochrome white, no exceptions... and the favicon' - the teal favicon violated the master rule, redrawn as #e6edf3 strokes on the dark ground tile, same two-windows+pulse concept. COLOR: the whole stylesheet used #12e0c4, a WRONG retyped teal (the skill's own cautionary tale is a one-digit teal error reaching 148 files) - every accent is now the real #00b8b1, on-accent #05221f, grounds/borders/text on the token values, body text at --text-body not max-contrast white. FONTS: brand faces loaded (Familjen Grotesk headings, Satoshi body, JetBrains Mono for the data/meta line) instead of the system stack. FEEDBACK (section 3): every dashboard action now answers back with a bottom-center toast (success green / error red sticky-8s / info) reporting what actually happened including the clear-flashes swept count, and the POST /ui/toast narration hook is built so the AI can talk over the live app (delivered via the SSE snapshot). Ralph grew 5 compliance checks: header exactly 44px, version NOT in the header row, favicon monochrome (accent-hex grep), /ui/toast answers, toasts render.

John Lauer ·e279ea421e ·2mo ago ·parent f3c47eb
4 files changed +116−39
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.249",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.249/adom-bridge-puppeteer-v1.9.249.zip",-  "sha256": "aa745c81ed7c86fcd56639fdda1415e6a2244dc3ceabddbdc6c33b9201dd764c",-  "size": 468134,+  "version": "1.9.250",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.250/adom-bridge-puppeteer-v1.9.250.zip",+  "sha256": "e8b772a315c19e723338e9c535b802ebc8d5223d7b72f91e354ab4c066698a55",+  "size": 470283,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.249+1.9.250
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.249",+  "version": "1.9.250",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+110−33
@@ -4003,6 +4003,7 @@ function icoLargestPng(buf) { // icon can be detected and skipped for a lighter sibling. Returns the full acquisition STATE so the // dashboard can show, per tab, exactly how the icon was obtained. const _tabFavMeta = new Map();   // page-url -> { meta, at }+const _dashToasts = []; let _dashToastId = 0;   // /ui/toast narration queue, delivered over the SSE async function enumerateFaviconCandidates(page) {   try {     return await withTimeout(page.evaluate(() => {@@ -8147,6 +8148,7 @@ async function dashboardSnapshot() {     threads, profiles,     taskbarButtons: [...buttons.values()],     untrackedWindows: untracked,+    toasts: _dashToasts.filter(t => Date.now() - t.at < 15000),     windows: wins.sort((a, b) => (b.openedAt || 0) - (a.openedAt || 0)),   }; }@@ -8159,17 +8161,34 @@ const DASHBOARD_HTML = `<!doctype html><html lang="en"><head> <title>pup live windows</title> <link rel="icon" type="image/svg+xml" href="/dashboard/icon.svg?v=${BRIDGE_VERSION}"> <style>-*{box-sizing:border-box} html,body{margin:0;background:#0b1116;color:#e6edf3;font:14px/1.5 "Segoe UI",system-ui,sans-serif}-a{color:#12e0c4}-header{position:sticky;top:0;z-index:10;background:#0d141b;border-bottom:1px solid #1d2833;padding:12px 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}-h1{font-size:15px;margin:0;font-weight:650}+@import url('https://adom.inc/fonts/fonts.css');+*{box-sizing:border-box} html,body{margin:0;background:#0d1117;color:#c9d1d9;font:14px/1.5 Satoshi,"Segoe UI",system-ui,sans-serif}+a{color:#64abff}+header{position:sticky;top:0;z-index:10;background:#161b22;border-bottom:1px solid #30363d;height:44px;padding:0 12px;display:flex;gap:10px;align-items:center}+.brand{position:relative;display:flex;align-items:center;gap:7px;color:#00b8b1;font-family:"Familjen Grotesk",Satoshi,sans-serif;font-weight:600;font-size:14px;cursor:default}+.brand .bicon{width:16px;height:16px;color:#e6edf3}+.brand .bsub{color:#8b949e;font-weight:500}+.vsep{width:1px;height:22px;background:#30363d;flex:0 0 1px}+.subject{display:flex;align-items:baseline;gap:10px;min-width:0;flex:1 1 auto;overflow:hidden}+.sname{font-weight:700;color:#e6edf3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}+.smeta{color:#8b949e;font-size:12px;font-family:"JetBrains Mono",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}+.smeta b{color:#c9d1d9;font-weight:500}+@media(max-width:720px){.smeta{display:none}}+.acts2{margin-left:auto;display:flex;gap:6px}+.ibtn{width:30px;height:30px;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid #30363d;border-radius:8px;color:#e6edf3;cursor:pointer;transition:background .15s ease}+.ibtn:hover{background:#1c2128;border-color:#00b8b1}+.ibtn.danger:hover{border-color:#f85149;color:#f85149}+.ibtn svg{width:16px;height:16px}+.pop{position:fixed;z-index:99999;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:10px 12px;font-size:12px;color:#c9d1d9;font-weight:400;font-family:Satoshi,sans-serif;display:none;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:auto;line-height:1.7}+.pop.show{display:block}+#bar{display:flex;gap:10px;align-items:center;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d;flex-wrap:wrap} .dot{width:8px;height:8px;border-radius:50%;background:#3fb950;display:inline-block;margin-right:6px} .dot.stale{background:#d29922} .pill{background:#111b23;border:1px solid #1d2833;border-radius:999px;padding:3px 10px;font-size:12px;color:#9fb0c0} .pill b{color:#e6edf3;font-weight:600} select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-radius:6px;padding:5px 9px;font-size:13px} .btn{background:#16222c;border:1px solid #2a3a47;color:#cfe0ea;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}-.btn:hover{background:#1d2b36;border-color:#12e0c4}+.btn:hover{background:#1d2b36;border-color:#00b8b1} .btn.sm{padding:2px 8px;font-size:11px} .btn.xs{padding:0 6px;font-size:11px;line-height:16px} .btn.danger{border-color:#5c2b2b;color:#ff9d95}@@ -8177,16 +8196,21 @@ select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-ra .sub{margin-left:auto;color:#7d8f9f;font-size:12px}  /* taskbar strip: mimic of the real Windows taskbar */-#taskbar{position:sticky;top:49px;z-index:9;display:flex;gap:6px;align-items:center;padding:8px 18px;background:#0a0f14;border-bottom:1px solid #1d2833;overflow-x:auto}+#toasts{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column-reverse;gap:8px;align-items:center}+.toast{background:#21262d;border:1px solid #30363d;border-left:3px solid #00b8b1;color:#c9d1d9;border-radius:8px;padding:8px 14px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.5);transition:opacity .3s ease}+.toast.success{border-left-color:#3fb950}+.toast.error{border-left-color:#f85149}+.toast.out{opacity:0}+#taskbar{position:sticky;top:44px;z-index:9;display:flex;gap:6px;align-items:center;padding:8px 18px;background:#0a0f14;border-bottom:1px solid #1d2833;overflow-x:auto} #taskbar .lbl{color:#5c6c7a;font-size:11px;margin-right:6px;flex:0 0 auto} .tbi{position:relative;width:48px;height:42px;border-radius:8px;background:#0f1720;border:1px solid #1d2833;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto} .tbi.ghost{color:#5c6c7c;font-size:18px;font-weight:700;border-style:dashed;border-color:#7c3a3a}-.tbi:hover{border-color:#12e0c4;background:#12202a}-.tbi.fg{border-color:#12e0c4}+.tbi:hover{border-color:#00b8b1;background:#12202a}+.tbi.fg{border-color:#00b8b1} .tbi img.base{width:30px;height:30px;object-fit:contain} .tbi img.ov{position:absolute;top:1px;right:1px;width:17px;height:17px;object-fit:contain;border-radius:4px}-.tbi .n{position:absolute;bottom:-6px;left:-6px;background:#12e0c4;color:#06282a;font-size:10px;font-weight:700;border-radius:8px;min-width:16px;height:16px;line-height:16px;text-align:center;padding:0 3px}-.tbi .u{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);width:16px;height:3px;border-radius:2px;background:#12e0c4}+.tbi .n{position:absolute;bottom:-6px;left:-6px;background:#00b8b1;color:#05221f;font-size:10px;font-weight:700;border-radius:8px;min-width:16px;height:16px;line-height:16px;text-align:center;padding:0 3px}+.tbi .u{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);width:16px;height:3px;border-radius:2px;background:#00b8b1}  /* tree */ main{padding:14px 18px}@@ -8211,7 +8235,7 @@ main{padding:14px 18px} .led.green{background:#22e07a;box-shadow:0 0 6px 1px rgba(34,224,122,.65)} .led.amber{background:#f0b429;box-shadow:0 0 6px 1px rgba(240,180,41,.5)} .led.red{background:#ff5b6a;box-shadow:0 0 6px 1px rgba(255,91,106,.6)}-.tbi .led{position:absolute;bottom:-5px;right:-5px;box-shadow:0 0 0 2px #0b1116}+.tbi .led{position:absolute;bottom:-5px;right:-5px;box-shadow:0 0 0 2px #0d1117} .icostack{position:relative;flex:0 0 22px;width:22px;height:22px} .icostack .ico{position:absolute;inset:0} .icostack .icoov{position:absolute;top:-3px;right:-3px;width:13px;height:13px;object-fit:contain;border-radius:3px}@@ -8219,7 +8243,7 @@ main{padding:14px 18px} .nm.dim{font-weight:400;color:#c3d0da} .meta{color:#7d8f9f;font-size:12px} .tag{font-size:11px;border-radius:5px;padding:1px 7px;border:1px solid #23303c;color:#9fb0c0}-.tag.fg{background:#12e0c41a;border-color:#12e0c4;color:#12e0c4}+.tag.fg{background:#00b8b11a;border-color:#00b8b1;color:#00b8b1} .tag.hf{background:#d299221a;border-color:#d29922;color:#e3b341} .tag.warn{background:#f851491a;border-color:#f85149;color:#ff7b72} .tag.vp{background:#a371f71a;border-color:#a371f7;color:#c297ff}@@ -8230,27 +8254,39 @@ main{padding:14px 18px} .win{margin:2px 0 2px 20px;border-left:2px solid #1a2530} .win>.row .nm{font-size:13px} .tabs{margin:0 0 4px 44px}-.why{margin:4px 0 8px 44px;padding:6px 9px;background:#0b1116;border:1px solid #1d2833;border-radius:6px;color:#8fa3b3;font-size:11px;line-height:1.6}-.why b{display:block;color:#12e0c4;margin-bottom:2px}+.why{margin:4px 0 8px 44px;padding:6px 9px;background:#0d1117;border:1px solid #1d2833;border-radius:6px;color:#8fa3b3;font-size:11px;line-height:1.6}+.why b{display:block;color:#00b8b1;margin-bottom:2px} .empty{color:#7d8f9f;padding:40px;text-align:center} .hidden{display:none} footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px} </style></head><body> <header>-  <h1><span class="dot" id="live"></span>pup live windows</h1>-  <span class="pill">windows <b id="cW">0</b></span>-  <span class="pill">tabs <b id="cT">0</b></span>-  <span class="pill">taskbar icons <b id="cB">0</b></span>-  <span class="pill">v<b id="cV">?</b></span>+  <div class="brand" id="brand">+    <svg class="bicon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="8" width="13" height="12" rx="2"/><rect x="10" y="4" width="11" height="8" rx="2" opacity=".45"/><path d="M5 15h2l1.4-3 2 5 1.6-2h2"/></svg>+    <span class="bname">pup</span><span class="bsub">&middot; live windows</span>+  </div>+  <div class="pop" id="pop">+    <div>version <b id="cV">?</b></div>+    <div id="popMeta">connecting</div>+    <div><a href="https://wiki.adom.inc/adom/adom-desktop-puppeteer-bridge" target="_blank" rel="noopener">wiki: adom-desktop-puppeteer-bridge</a></div>+  </div>+  <div class="vsep"></div>+  <div class="subject">+    <span class="sname"><span class="dot" id="live"></span><span id="subjName">all windows</span></span>+    <span class="smeta"><b id="cW">0</b> windows &middot; <b id="cT">0</b> tabs &middot; <b id="cB">0</b> icons &middot; <span id="cLed" title="green: every taskbar button wears its real page-favicon overlay and no untracked pup windows exist. red: a window is bare or unmodeled."><span class="led red" id="cLedDot"></span> <b id="cLedTxt">?</b></span> &middot; <span id="age"></span></span>+  </div>+  <div class="acts2">+    <button class="ibtn" data-act="flash-all" title="flash every pup taskbar button" aria-label="flash all"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></button>+    <button class="ibtn" data-act="unflash-all" title="clear every flash" aria-label="clear flashes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>+    <button class="ibtn danger" data-act="close-all" data-confirm="Close ALL pup windows?" title="close every pup window" aria-label="close all windows"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>+  </div>+</header>+<div id="bar">   <span class="pill" id="cM" style="display:none">minimal-touch</span>-  <span class="pill" id="cLed" title="green = every taskbar button wears its real page-favicon overlay (dashboard matches the taskbar); red = a window is bare"><span class="led red" id="cLedDot"></span> overlays <b id="cLedTxt">?</b></span>   <select id="f"><option value="">all AI threads</option></select>   <input id="q" placeholder="filter url / session..." size="20">-  <button class="btn" data-act="flash-all">flash all</button>-  <button class="btn" data-act="unflash-all">clear flashes</button>-  <button class="btn danger" data-act="close-all" data-confirm="Close ALL pup windows?">close all</button>-  <span class="sub" id="age"></span>-</header>+</div>+<div id="toasts"></div> <div id="taskbar"></div> <main id="m"><div class="empty">connecting...</div></main> <footer id="ft"></footer>@@ -8271,7 +8307,7 @@ function favTags(f){   if(f.mode&&f.mode!=='default') bits.push('<span class="ft '+(f.mode==='dark'?'k-dark':f.mode==='light'?'k-light':'')+'">'+esc(f.mode)+'-mode</span>');   if(f.transparent===true) bits.push('<span class="ft'+(f.darkDominant?' k-warn':'')+'">transparent'+(f.darkDominant?' · dark':'')+'</span>');   else if(f.transparent===false) bits.push('<span class="ft">opaque</span>');-  const tip=['kind: '+(f.kind||'?'),'format: '+(f.format||'?'),'color-scheme: '+(f.mode||'?'),'transparent: '+(f.transparent===null?'n/a':f.transparent),f.luminance!=null?'luminance: '+f.luminance+(f.darkDominant?' (dark)':''):'',f.via?'fetched via: '+f.via:'',f.candidates!=null?'candidates seen: '+f.candidates:'',f.href&&!/^generic|^app-provided/.test(f.href)?'src: '+f.href:'',f.note?'⚠ '+f.note:''].filter(Boolean).join(String.fromCharCode(10));+  const tip=['kind: '+(f.kind||'?'),'format: '+(f.format||'?'),'color-scheme: '+(f.mode||'?'),'transparent: '+(f.transparent===null?'n/a':f.transparent),f.luminance!=null?'luminance: '+f.luminance+(f.darkDominant?' (dark)':''):'',f.via?'fetched via: '+f.via:'',f.candidates!=null?'candidates seen: '+f.candidates:'',f.href&&!/^generic|^app-provided/.test(f.href)?'src: '+f.href:'',f.note?'warning: '+f.note:''].filter(Boolean).join(String.fromCharCode(10));   return '<span class="fttray" title="'+esc(tip)+'">'+bits.join('')+'</span>'; } @@ -8279,6 +8315,9 @@ function render(){   if(!SNAP)return; const d=SNAP;   _q('#cW').textContent=d.counts.windows; _q('#cT').textContent=d.counts.tabs;   _q('#cB').textContent=(d.taskbarButtons||[]).length; _q('#cV').textContent=d.bridge.version;+  _q('#popMeta').textContent=d.bridge.platform+' \u00b7 bridge up '+ago(d.bridge.uptimeMs)+' \u00b7 deps '+(d.bridge.depsOk?'ok':'missing');+  _q('#subjName').textContent=(_q('#f').value||'all windows');+  for(const t of (d.toasts||[])){ if(t.id>SEENTOAST){SEENTOAST=t.id;toast(t.msg,t.type);} }   _q('#cM').style.display=d.bridge.minimalTouch?'':'none';   // header overlay-match rollup: red if ANY button bare, amber if any generic, else green   const bl=(d.taskbarButtons||[]).map(b=>b.led||'red');@@ -8361,13 +8400,37 @@ function render(){   }).join(''):'<div class="empty">no pup windows open'+(th||q?' matching this filter':'')+'</div>';   _q('#ft').textContent='bridge up '+ago(d.bridge.uptimeMs)+' &middot; '+d.bridge.platform+' &middot; deps '+(d.bridge.depsOk?'ok':'MISSING'); }-async function act(p){try{await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});}catch(e){}}+function toast(msg,type){+  const t=document.createElement('div');t.className='toast '+(type||'info');t.textContent=msg;+  _q('#toasts').appendChild(t);+  setTimeout(()=>{t.classList.add('out');setTimeout(()=>t.remove(),350)},type==='error'?8000:4000);+}+const ACTMSG={'flash':'flashed','unflash':'flash cleared on','flash-all':'flashed every pup taskbar button','unflash-all':'cleared all flashes','close-all':'closed all pup windows','close-window':'closed','close-tab':'closed tab in','fg':'bringing to front:','bg':'sent to background:'};+async function act(p){+  try{+    const r=await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});+    const j=await r.json().catch(()=>({}));+    const what=(ACTMSG[p.action]||p.action)+(p.sessionId?' '+p.sessionId:'');+    if(j.ok) toast(what+(p.action==='unflash-all'&&j.swept!=null?' ('+j.swept+' windows swept)':''), p.action.indexOf('close')===0?'success':'info');+    else toast(what+' failed'+(j.error?': '+j.error:''),'error');+    return j;+  }catch(e){toast(p.action+' failed: '+e.message,'error');}+}+let SEENTOAST=0;+(function(){+  const b=_q('#brand'),pop=_q('#pop');let show=null,hide=null;+  function place(){const r=b.getBoundingClientRect();pop.style.left=Math.max(8,r.left)+'px';pop.style.top=(r.bottom+8)+'px';}+  b.addEventListener('mouseenter',()=>{clearTimeout(hide);show=setTimeout(()=>{place();pop.classList.add('show');},600);});+  b.addEventListener('mouseleave',()=>{clearTimeout(show);hide=setTimeout(()=>pop.classList.remove('show'),220);});+  pop.addEventListener('mouseenter',()=>clearTimeout(hide));+  pop.addEventListener('mouseleave',()=>{hide=setTimeout(()=>pop.classList.remove('show'),220);});+})(); document.addEventListener('error',ev=>{const i=ev.target;if(i&&i.tagName==='IMG'&&i.dataset&&i.dataset.fallback==='hide')i.style.visibility='hidden';},true); document.addEventListener('click',ev=>{   const tog=ev.target.closest('[data-toggle]');   if(tog){const id=tog.dataset.toggle;if(COLL.has(id))COLL.delete(id);else COLL.add(id);render();return;}   const jmp=ev.target.closest('[data-jump]');-  if(jmp){const t=document.getElementById(jmp.dataset.jump);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});t.style.transition='box-shadow .3s';t.style.boxShadow='0 0 0 2px #12e0c4';setTimeout(()=>t.style.boxShadow='',900);}return;}+  if(jmp){const t=document.getElementById(jmp.dataset.jump);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});t.style.transition='box-shadow .3s';t.style.boxShadow='0 0 0 2px #00b8b1';setTimeout(()=>t.style.boxShadow='',900);}return;}   const b=ev.target.closest('[data-act]');if(!b)return;   const a=b.dataset.act,c=b.dataset.confirm;if(c&&!confirm(c))return;   if(a==='flash-group'||a==='unflash-group'){(b.dataset.ids||'').split(',').filter(Boolean).forEach(sid=>act({action:a==='flash-group'?'flash':'unflash',sessionId:sid}));return;}@@ -8434,6 +8497,18 @@ const server = http.createServer(async (req, res) => {       req.on('close', () => { alive = false; clearInterval(iv); });       return;     }+    if ((pathname === '/ui/toast' || pathname === '/dashboard/toast') && req.method === 'POST') {+      // adom-ui-design section 3: the toast hook, built in so the AI can narrate over the live app+      // ("Running test 3 of 8") through the same surface the app uses for its own feedback.+      try {+        const body = JSON.parse(await readBody(req) || '{}');+        const msg = String(body.msg || body.message || '').slice(0, 200);+        if (!msg) { sendJSON(res, { ok: false, error: 'msg required' }); return; }+        _dashToasts.push({ id: ++_dashToastId, msg, type: ['success', 'error', 'info'].includes(body.type) ? body.type : 'info', at: Date.now() });+        while (_dashToasts.length > 20) _dashToasts.shift();+        sendJSON(res, { ok: true }); return;+      } catch (e) { sendJSON(res, { ok: false, error: e.message }); return; }+    }     if (pathname === '/dashboard/action' && req.method === 'POST') {       // v1.9.226 (John): make the dashboard actionable, not just observational — close all / close one       // window / close one tab, and flash a taskbar button orange (or clear it), per-button and for@@ -8529,14 +8604,16 @@ const server = http.createServer(async (req, res) => {       // overlapping browser-window cards (the live "cluster of windows" pup drives) with an SSE       // heartbeat pulse across the front one (live monitoring). Bold fills + negative-space marks so       // nothing hairlines at 16px, and it reads as its OWN app, distinct from the plain Adom leaf mark.+      // v1.9.250 (adom-ui-design section 9, John: "what does it say about headers and icons?"):+      // "Icons are monochrome white. No exceptions... and the favicon." The teal version violated the+      // master rule (brand color belongs only in marketing art). Same two-windows+pulse concept,+      // #e6edf3 marks on the dark ground tile.       res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=3600' });       res.end('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">'         + '<rect x="1" y="1" width="30" height="30" rx="7" fill="#0d1117" stroke="#30363d" stroke-width="1"/>'-        + '<rect x="14" y="6" width="13" height="10" rx="2.2" fill="#00b8b1" fill-opacity="0.5"/>'-        + '<rect x="5" y="11" width="17" height="15" rx="2.6" fill="#00b8b1"/>'-        + '<circle cx="8" cy="14.2" r="1.15" fill="#05221f"/>'-        + '<circle cx="11.3" cy="14.2" r="1.15" fill="#05221f"/>'-        + '<path d="M7 19 H10.5 L12 15.5 L14 23 L16 19 H20" fill="none" stroke="#05221f" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>'+        + '<rect x="13" y="5" width="14" height="11" rx="2.2" fill="none" stroke="#e6edf3" stroke-width="1.6" opacity="0.45"/>'+        + '<rect x="5" y="11" width="17" height="15" rx="2.6" fill="none" stroke="#e6edf3" stroke-width="1.8"/>'+        + '<path d="M7.5 19 H10 L11.5 15.5 L13.5 22.5 L15.5 19 H19" fill="none" stroke="#e6edf3" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>'         + '</svg>');       return;     }