← Commit history

Overlay correctness + LED match indicator + real pup favicon (John's audit). FOUR fixes: (1) Ripped out the dead AUMID code I wrongly added in 1.9.235/236 - we stopped using AUMID; minimal-touch stamps none (verified zero Adom.Pup* registry keys, every session curAppId=null), so grouping is now purely one taskbar button per Chrome PROCESS with that browser exe's real icon as the base, no phantom AUMID branch. (2) THE favicon bug John kept hitting: applyAppOverlay set _appOverlay=true even when it fell to the GENERIC icon (favicon fetch timed out, e.g. during a post-restart enforcement paint), and never retried - so a window that could show github's real favicon stayed stuck on the generic Adom mark forever. Now the badge tracks generic-vs-real (_overlayIsGeneric), and the 15s health sweep UPGRADES a stuck-generic badge to the real page favicon the moment it becomes fetchable (90s cadence), in addition to healing bare windows. (3) LED indicator (John: 'put an led indicator indicating the check for overlay icons match came back green vs red'): every window, every taskbar button, and a header rollup now show a green/amber/red LED - green = real page-favicon overlay painted and the exact bytes are on disk (dashboard badge == what Windows was handed), amber = painted but legit generic fallback (page truly has no favicon), red = bare/unpainted (taskbar would be wrong). The strip badge now mirrors the FOREGROUND window's overlay (what Windows actually fronts) else newest-painted, and says which. (4) Real pup dashboard favicon designed against adom-ui-design (exact brand tokens accent #00b8b1 / on-accent #05221f / bg #0d1117 / border #30363d): two overlapping browser-window cards with an SSE heartbeat pulse = live windows monitoring, distinct from the plain Adom leaf mark, crisp at 16px.

John Lauer ·c8b9834daf ·2mo ago ·parent 4b4e6c8
4 files changed +90−52
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.236",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.236/adom-bridge-puppeteer-v1.9.236.zip",-  "sha256": "f81df210f3e88771d0141ea550a89cb5cc10c3ab91265233024cb61d338cfba8",-  "size": 458434,+  "version": "1.9.237",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.237/adom-bridge-puppeteer-v1.9.237.zip",+  "sha256": "281b4416874e57d3b295a2852f6304a5123cdd8ad512249c50451ba87ef63458",+  "size": 460221,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.236+1.9.237
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.236",+  "version": "1.9.237",   "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+84−46
@@ -1047,7 +1047,6 @@ function saveSessionFile(sessionId, profileName, url) {     // biggest "pup windows don't behave / keep vanishing" driver (John, 2026-07-29). The flag is set     // by a granted raise OR by pup OBSERVING the user foreground the window themselves (taskbar click).     foreground: !!((sessions.get(sessionId) || {})._foreground),-    curAppId: ((sessions.get(sessionId) || {})._curAppId) || null,     // v1.9.197 BUG 4 (Bills thread): owner was NEVER written here, so every bridge restart erased it     // and browser_list_windows reported owner:null for every session — silently disabling the     // session_owned_by_another_thread protection that owner is supposed to back.@@ -1713,7 +1712,6 @@ async function recoverSessions() {       // park below yanked a window the user was actively using to the bottom (John, 2026-07-29).       if (sf.foreground) { session._foreground = true; session._userActiveAt = Date.now(); }       if (sf.owner) session.owner = sf.owner;   // v1.9.197 BUG 4: restore ownership across restarts-      if (sf.curAppId) session._curAppId = sf.curAppId;   // v1.9.236: stamped AUMID survives restart (drives real-taskbar grouping)       // v1.9.202 (John's icon audit): a recovered window came back with NO taskbar badge. Under       // minimalTouch the overlay is applied only on OPEN and NAVIGATE, and the re-stamp sweeps that       // used to cover recovery are gated off — so every bridge restart left surviving windows bare@@ -4231,6 +4229,14 @@ async function applyAppOverlay(session, sessionId) {       at: Date.now(),     };     try { session._overlayDesc = 'favicon+adom:' + (src && src.href && src.href !== 'data:inline' ? new URL(src.href).hostname : 'inline-icon'); } catch (e) { session._overlayDesc = 'favicon+adom'; }+    // v1.9.237 (John: "why are the overlay icons the standard adom favicon when you have an entire set+    // of rules on picking the favicon from your first or foreground tab and you seem to not be doing+    // that"). A GENERIC fallback (favicon fetch failed/timed out, e.g. during a post-restart paint) is+    // NOT a finished state - the page usually HAS a favicon that becomes fetchable a moment later. Flag+    // it so the health-sweep enforcement re-attempts and UPGRADES the badge to the real favicon. Only a+    // page that genuinely has no favicon anywhere stays generic (correct, e.g. a bare checkout page).+    session._overlayIsGeneric = !!(src && /^generic:/.test(src.href || ''));+    session._overlayGenericAt = session._overlayIsGeneric ? Date.now() : 0;     return true;   } catch (e) { return false; } }@@ -7804,7 +7810,17 @@ async function dashboardSnapshot() {       tabs,       overlay: sess._overlayDesc || null,       overlayWhy: sess._overlayWhy || null,-      _curAppId: sess._curAppId || null,+      // v1.9.237 (John: "put an led indicator indicating the check for overlay icons match came back+      // green vs red"). Per-window overlay health, the source of truth the LED rolls up:+      //   green  = pup painted a REAL page favicon and the exact bytes are on disk to serve the badge+      //   amber  = painted, but the page had no favicon so it's the legit generic fallback+      //   red    = not painted yet / paint failed → the taskbar button would be bare or wrong+      overlayLed: (() => {+        const painted = !!sess._appOverlay;+        const hasBytes = (() => { try { return fs.existsSync(path.join(require('os').tmpdir(), 'pup-badge-debug', sid + '.png')); } catch (e) { return false; } })();+        if (!painted || !hasBytes) return 'red';+        return sess._overlayIsGeneric ? 'amber' : 'green';+      })(),       hwnd: sess._hwnd || null,       emulatedViewport: sess._emulatedViewport || null,       lostBrowser: !!sess._lostBrowser,@@ -7830,30 +7846,33 @@ async function dashboardSnapshot() {     // hwnd/session invented a button per window and the dashboard showed more rows than the real     // taskbar had icons. Key by PROFILE (= one Chrome process = one button), which is why the header     // count "chromes N" equals the number of real taskbar icons.-    // v1.9.235 (John's on-glass audit): a window that STILL CARRIES a per-session AUMID (stamped-    // before minimal-touch, kept until close) has its OWN real taskbar button with the category tile-    // as its base icon - grouping those into the process bucket made the dashboard show fewer-    // buttons than the real taskbar. Honour the stamp when it exists; only unstamped windows share-    // the browser-process button.-    if (w._curAppId && w._curAppId !== 'Adom.Pup') {-      key = w._curAppId; label = w.sessionId;-      const catM = /\.([a-z-]+)\.i\d+$/.exec(w._curAppId);-      var baseIcon = '/dashboard/caticon?cat=' + encodeURIComponent((catM && catM[1]) || 'default');-      var kind = 'aumid';-    } else if (!minimalTouch() && grouping === 'grouped') {-      key = 'Adom.Pup'; label = 'Adom Pup (grouped)';-      baseIcon = '/dashboard/caticon?cat=default'; kind = 'aumid';-    } else {-      key = 'chrome-process:' + w.profile; label = 'Chrome (' + w.profile + ')';-      baseIcon = '/dashboard/baseicon?profile=' + encodeURIComponent(w.profile); kind = 'process';-    }-    if (!buttons.has(key)) buttons.set(key, { key, label, kind, baseIcon, grouping: minimalTouch() ? 'minimal-touch' : grouping, windows: [], badgeSession: null, badgeAt: 0 });+    // v1.9.237 (John: 'we stopped using aumid so what are you talking about?'). Correct: minimal-touch+    // stamps NO AUMID (verified: zero Adom.Pup* registry keys, every session curAppId=null), so there+    // is exactly ONE real taskbar button per Chrome PROCESS. Group by process; the button's base icon+    // is that browser exe's real icon. No AUMID branch - it was dead code chasing a deleted mechanism.+    key = 'chrome-process:' + w.profile;+    label = 'Chrome (' + w.profile + ')';+    const baseIcon = '/dashboard/baseicon?profile=' + encodeURIComponent(w.profile);+    if (!buttons.has(key)) buttons.set(key, { key, label, kind: 'process', baseIcon, grouping: minimalTouch() ? 'minimal-touch' : grouping, windows: [], badgeSession: null, badgeAt: 0, badgeVia: null });     const btn = buttons.get(key);     btn.windows.push(w.sessionId);-    // The real button wears whatever pup painted LAST - mirror that, and show NO overlay when-    // nothing is currently painted (a bare Chrome button must render bare here too).+    // Windows shows the overlay of whichever window is FOREMOST in the group. Mirror that: prefer the+    // foreground window's badge; otherwise the newest-painted. Record which, so the LED can say whether+    // the dashboard badge matches what the taskbar button is actually wearing.     const paintedAt = (w.overlay && !String(w.overlay).startsWith('none') && w.overlayWhy && w.overlayWhy.at) || 0;-    if (paintedAt > btn.badgeAt) { btn.badgeAt = paintedAt; btn.badgeSession = w.sessionId; }+    if (w.foreground) { btn.badgeSession = w.sessionId; btn.badgeAt = Infinity; btn.badgeVia = 'foreground window'; btn.badgeLed = w.overlayLed; }+    else if (paintedAt > btn.badgeAt) { btn.badgeAt = paintedAt; btn.badgeSession = w.sessionId; btn.badgeVia = 'newest-painted (no window foreground)'; btn.badgeLed = w.overlayLed; }+  }+  // v1.9.237: button LED = green only if EVERY window under it wears a real-favicon overlay (a single+  // bare/wrong window makes the whole taskbar button suspect); red if any window is bare; amber if all+  // painted but some are the generic fallback. This is the "do the dashboard overlays match the taskbar"+  // check John asked to see as a green/red light.+  for (const btn of buttons.values()) {+    const leds = btn.windows.map(sid => { const w = wins.find(x => x.sessionId === sid); return w ? w.overlayLed : 'red'; });+    btn.led = leds.includes('red') ? 'red' : leds.includes('amber') ? 'amber' : 'green';+    btn.ledWhy = leds.includes('red') ? 'a window under this button has NO overlay painted (taskbar would be bare/stale)'+      : leds.includes('amber') ? 'all painted, but a window fell back to the generic icon (page had no favicon)'+      : 'every window wears its real page-favicon overlay — dashboard matches the taskbar';   }   const threads = [...new Set(wins.map(w => w.owner || w.ownerThread).filter(Boolean))].sort();   const profiles = {};@@ -7915,6 +7934,11 @@ main{padding:14px 18px} .tog.leaf{visibility:hidden} .ico{width:22px;height:22px;flex:0 0 22px;object-fit:contain;border-radius:5px;background:#0f1720} .ico.sm{width:16px;height:16px;flex:0 0 16px}+.led{display:inline-block;width:9px;height:9px;border-radius:50%;flex:0 0 9px;vertical-align:middle;box-shadow:0 0 0 1px rgba(0,0,0,.4) inset}+.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} .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}@@ -7946,6 +7970,7 @@ footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px}   <span class="pill">taskbar icons <b id="cB">0</b></span>   <span class="pill">v<b id="cV">?</b></span>   <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>@@ -7968,6 +7993,11 @@ function render(){   _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('#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');+  const roll=bl.includes('red')?'red':bl.includes('amber')?'amber':(bl.length?'green':'red');+  _q('#cLedDot').className='led '+roll;+  _q('#cLedTxt').textContent=roll==='green'?'MATCH':roll==='amber'?'generic':'MISMATCH';   _q('#age').textContent='updated '+new Date(d.ts).toLocaleTimeString();   const sel=_q('#f'),cur=sel.value;   if(sel.options.length-1!==d.threads.length){sel.innerHTML='<option value="">all AI threads</option>'+d.threads.map(t=>'<option>'+esc(t)+'</option>').join('');sel.value=cur;}@@ -7983,9 +8013,10 @@ function render(){   // button must render bare here too, otherwise the mimic lies).   _q('#taskbar').innerHTML='<span class="lbl">TASKBAR</span>'+groups.map((g,i)=>{     const fg=g.wins.some(w=>w.foreground);-    return '<div class="tbi'+(fg?' fg':'')+'" data-jump="g'+i+'" title="'+esc(g.label)+'">'+    return '<div class="tbi'+(fg?' fg':'')+'" data-jump="g'+i+'" title="'+esc(g.label)+' — '+esc(g.ledWhy||'')+'">'       +'<img class="base" src="'+esc(g.baseIcon||'')+'" data-fallback="hide">'       +(g.badgeSession?'<img class="ov" src="'+badge(g.badgeSession)+'" data-fallback="hide">':'')+      +'<span class="led '+esc(g.led||'red')+'" title="'+esc(g.ledWhy||'')+'"></span>'       +(g.wins.length>1?'<span class="n">'+g.wins.length+'</span>':'')       +(fg?'<span class="u"></span>':'')+'</div>';   }).join('');@@ -7997,8 +8028,9 @@ function render(){       +'<div class="row"><span class="tog" data-toggle="'+gid+'">'+(gcoll?'▸':'▾')+'</span>'         +'<span class="icostack"><img class="ico" src="'+esc(g.baseIcon||'')+'" data-fallback="hide">'         +(g.badgeSession?'<img class="icoov" src="'+badge(g.badgeSession)+'" data-fallback="hide">':'')+'</span>'+        +'<span class="led '+esc(g.led||'red')+'" title="overlay match: '+esc(g.ledWhy||'')+'"></span>'         +'<span class="nm">'+esc(g.label)+'</span>'-        +'<span class="meta">'+g.wins.length+' window'+(g.wins.length>1?'s':'')+' &middot; '+esc(g.grouping)+'</span>'+        +'<span class="meta">'+g.wins.length+' window'+(g.wins.length>1?'s':'')+' &middot; '+esc(g.grouping)+' &middot; badge from '+esc(g.badgeVia||'?')+'</span>'         +'<span class="acts"><button class="btn sm" data-act="flash-group" data-ids="'+esc(g.windows.join(','))+'">flash</button>'           +'<button class="btn sm" data-act="unflash-group" data-ids="'+esc(g.windows.join(','))+'">clear</button></span></div>'       +'<div class="tw'+(gcoll?' hidden':'')+'">'+g.wins.map(w=>{@@ -8006,6 +8038,7 @@ function render(){         const crash=w.tabs.some(t=>t.crashed)||w.lostBrowser;         return '<div class="win">'           +'<div class="row"><span class="tog" data-toggle="'+wid+'">'+(wcoll?'▸':'▾')+'</span>'+            +'<span class="led '+esc(w.overlayLed||'red')+'" title="'+(w.overlayLed==='green'?'real page favicon painted — matches taskbar':w.overlayLed==='amber'?'painted, generic fallback (page has no favicon)':'NOT painted — taskbar would be bare/stale')+'"></span>'             +'<img class="ico" src="'+badge(w.sessionId)+'" data-fallback="hide">'             +'<span class="nm">'+esc(w.sessionId)+'</span>'             +'<span class="tag '+(w.foreground?'fg':'')+'">'+(w.foreground?'FG':'bg')+'</span>'@@ -8020,7 +8053,7 @@ function render(){               +'<button class="btn sm danger" data-act="close-window" data-sid="'+esc(w.sessionId)+'" data-confirm="Close '+esc(w.sessionId)+'?">close</button></span></div>'           +'<div class="'+(wcoll?'hidden':'')+'">'           +(w.foreground&&w.foregroundReason?'<div class="why"><b>why foreground</b>'+esc(w.foregroundReason)+'</div>':'')-          +(w.overlayWhy?'<div class="why"><b>how this icon was chosen</b>tab '+esc(w.overlayWhy.tabId||'?')+', '+esc(w.overlayWhy.tabPickedBy)+'<br>source: '+esc(w.overlayWhy.source||'none')+'<br>via: '+esc(w.overlayWhy.fetchedVia)+'<br>painted by: '+esc(w.overlayWhy.paintedBy)+'</div>':'')+          +(w.overlayWhy?'<div class="why"><b>how this icon was chosen</b><span class="led '+esc(w.overlayLed||'red')+'"></span> '+(w.overlayLed==='green'?'real page favicon':w.overlayLed==='amber'?'generic fallback (no favicon on the page)':'not painted')+'<br>tab '+esc(w.overlayWhy.tabId||'?')+', '+esc(w.overlayWhy.tabPickedBy)+'<br>source: '+esc(w.overlayWhy.source||'none')+'<br>via: '+esc(w.overlayWhy.fetchedVia)+'<br>painted by: '+esc(w.overlayWhy.paintedBy)+'</div>':'')           +'<div class="tabs">'+w.tabs.map(t=>             '<div class="row"><span class="tog leaf">•</span>'             +'<img class="ico sm" src="'+fav(w.sessionId,t.tabId)+'" data-fallback="hide">'@@ -8158,12 +8191,20 @@ const server = http.createServer(async (req, res) => {       // asset (not a data: URI) so pup's favicon pipeline fetches it exactly like any other app icon,       // which also means the dashboard window's own taskbar badge is produced by the same code path       // it is built to display.+      // v1.9.237 (John: "why have you not created your own pup favicon yet using our adom ui design+      // skills?"). Designed against adom-ui-design: exact brand tokens — accent #00b8b1, on-accent+      // #05221f (the token for marks on a teal fill), bg #0d1117, border #30363d. Concept = two+      // 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.       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 width="32" height="32" rx="7" fill="#12e0c4"/>'-        + '<circle cx="12" cy="12.5" r="4.1" fill="#06282a"/>'-        + '<circle cx="20.5" cy="20" r="4.1" fill="#06282a"/>'-        + '<path d="M14.6 15.1 L18 17.6" stroke="#06282a" stroke-width="2.1" stroke-linecap="round"/>'+        + '<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"/>'         + '</svg>');       return;     }@@ -8221,18 +8262,6 @@ const server = http.createServer(async (req, res) => {         res.end(buf); return;       } catch (e) { res.writeHead(404); res.end(); return; }     }-    if (pathname === '/dashboard/caticon') {-      // Base icon for an AUMID-stamped button: the pup category .ico that AUMID was registered with.-      try {-        const cat = (url.searchParams.get('cat') || 'default').replace(/[^a-z0-9-]/gi, '');-        let f = path.join(__dirname, 'icons', `pup-cat-${cat}9.ico`);-        if (!fs.existsSync(f)) f = path.join(__dirname, 'icons', 'pup-cat-default9.ico');-        const inner = icoLargestPng(fs.readFileSync(f));-        if (!inner) { res.writeHead(404); res.end(); return; }-        res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });-        res.end(inner); return;-      } catch (e) { res.writeHead(404); res.end(); return; }-    }     if (pathname === '/dashboard/badge') {       // Serve the exact overlay PNG pup last composed for a session, so the dashboard shows the REAL       // badge rather than a re-render that might differ from what Windows was handed.@@ -12028,12 +12057,21 @@ server.listen(PORT, BIND_HOST, async () => {     // gently instead of hammered.     try {       for (const [sid2, s2] of [...sessions]) {-        if (s2._appOverlay) continue;+        // Two things to heal: a BARE window (no overlay at all), and a window stuck on the GENERIC+        // fallback that should be wearing its real page favicon. The generic case is retried on a+        // slower cadence (favicon may legitimately never resolve) but still upgrades the moment it can.+        const bare = !s2._appOverlay;+        const stuckGeneric = s2._appOverlay && s2._overlayIsGeneric;+        if (!bare && !stuckGeneric) continue;         const last = s2._overlayEnforceAt || 0;-        if (Date.now() - last < 60000) continue;+        const cooldown = bare ? 60000 : 90000;+        if (Date.now() - last < cooldown) continue;         s2._overlayEnforceAt = Date.now();+        const wasGeneric = stuckGeneric;         applyAppOverlay(s2, sid2).then(okp => {-          if (okp) console.log(`[overlay] "${sid2}" ENFORCED (was bare) — every pup window must wear its overlay`);+          if (!okp) return;+          if (wasGeneric && !s2._overlayIsGeneric) console.log(`[overlay] "${sid2}" UPGRADED generic → real favicon (health sweep re-fetched it)`);+          else if (bare) console.log(`[overlay] "${sid2}" ENFORCED (was bare) — every pup window must wear its overlay`);         }).catch(() => {});       }     } catch (e) {}