master
John Lauer v2.0.688 (insiders): Smoother taskbar tile text: every tile size is drawn at 4x and averaged down, so letters sit at ex… 80a3d14 1mo ago
//! pup-dashboard: every HTTP route that is not a verb. /health and /status (the chip contract ab
//! renders verbatim), the live dashboard page (the Node build's single-file page, embedded) with
//! its SSE snapshot feed, its action and image routes, /api/status (the LED row), /identity, and
//! the embedded page assets. Each route is one function; the binary only maps paths here.

pub mod assets_gen;

pub mod hydrogen;
use pup_core::{Caller};
use pup_verbs::Ctx;
use serde_json::{json, Value};
use std::io::Read;
use std::time::{Duration, Instant};

pub const DASHBOARD_HTML: &str = include_str!("../assets/dashboard.html");
pub const ICON_SVG: &str = include_str!("../assets/icon.svg");
pub const SAY_PHRASES: &str = include_str!("../assets/say-phrases.json");

pub enum Reply { Json(u16, Value), Bytes(u16, String, Vec<u8>, Option<String>), Stream(Box<dyn Read + Send>) }

fn q(query: &str, key: &str) -> Option<String> {
    query.split('&').find_map(|kv| { let mut it = kv.splitn(2, '='); if it.next()? == key { Some(url_decode(it.next().unwrap_or(""))) } else { None } })
}
fn url_decode(s: &str) -> String {
    let b = s.as_bytes(); let mut out = Vec::with_capacity(b.len()); let mut i = 0;
    while i < b.len() { if b[i] == b'%' && i + 2 < b.len() { if let Ok(v) = u8::from_str_radix(std::str::from_utf8(&b[i + 1..i + 3]).unwrap_or("zz"), 16) { out.push(v); i += 3; continue; } } if b[i] == b'+' { out.push(b' '); } else { out.push(b[i]); } i += 1; }
    String::from_utf8_lossy(&out).to_string()
}

fn session_rows(ctx: &Ctx) -> Vec<Value> {
    let active = ctx.sessions.active_id();
    ctx.sessions.all().iter().map(|s| json!({ "sessionId": s.id, "url": s.active().map(|t| t.url.clone()), "active": active.as_deref() == Some(&s.id), "ownerThread": s.owner, "view": Value::Null, "authed": Value::Null })).collect()
}

/// GET /health (Node: the /health branch): what the toolbar extension and ab's probes read.
pub fn health(ctx: &Ctx) -> Value {
    let st = &ctx.settings;
    json!({
        "status": "ok", "bridge": "puppeteer", "version": pup_core::version(), "shell": "rust", "port": ctx.port,
        "toolbar": { "enabled": st.get("toolbarExtension", "on") != "off", "ownerBadge": st.get("tbOwnerBadge", "on") != "off", "snipHotkey": st.get("tbSnipHotkey", "on") != "off", "floaty": st.get("tbFloatyPencil", "off") == "on" },
        "browserCount": ctx.browsers.lock().unwrap().len(),
        "sessionCount": ctx.sessions.count(), "activeSession": ctx.sessions.active_id(), "sessions": session_rows(ctx),
        "chrome": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()),
    })
}

/// GET /status: the status-chip contract (led/summary/tooltip verbatim in ab's UI).
pub fn status(ctx: &Ctx) -> Value {
    let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());
    let v = pup_core::version();
    let n = ctx.sessions.count();
    let (led, summary, tooltip) = if rd["installPhase"] == json!("failed") {
        ("red", "Chrome install failed".to_string(), format!("Puppeteer bridge v{v} (rust)\nChrome for Testing install FAILED: {}\nRetry pup_prewarm (usually a network/proxy or disk issue).", rd["lastError"].as_str().unwrap_or("?")))
    } else if rd["installing"] == json!(true) {
        let pct = rd["installProgressPct"].as_u64().unwrap_or(0);
        let unpack = rd["installPhase"] == json!("unpacking");
        ("yellow", if unpack { "Unpacking Chrome".to_string() } else { format!("Installing Chrome {pct}%") }, format!("Puppeteer bridge v{v} (rust)\n{} — pup not ready yet. Poll pup_readiness until ready.", if unpack { "Unpacking Chrome for Testing".to_string() } else { format!("Downloading Chrome for Testing ({pct}%)") }))
    } else if rd["ready"] != json!(true) {
        ("yellow", "No browser yet".to_string(), format!("Puppeteer bridge v{v} (rust)\nNo Chrome/Edge detected and Chrome for Testing not cached — pup will fetch CfT on first use (or call pup_prewarm)."))
    } else {
        let kind = rd["browserKind"].as_str().unwrap_or("?"); let src = rd["browserSource"].as_str().unwrap_or("?");
        ("green", format!("{n} window{} · {kind}", if n == 1 { "" } else { "s" }), format!("Puppeteer bridge v{v} (rust) — ready\nDrives {kind} ({src}) · {n} session(s){}", if rd["chromeForTestingInstalled"] == json!(true) { format!("\nCfT {} cached", rd["chromeBuildId"].as_str().unwrap_or("")) } else { String::new() }))
    };
    let list: Vec<Value> = ctx.sessions.all().iter().map(|s| json!({ "sessionId": s.id, "url": s.active().map(|t| t.url.clone()), "active": ctx.sessions.active_id().as_deref() == Some(&s.id) })).collect();
    json!({
        "ok": true, "bridge": "puppeteer", "version": v, "shell": "rust", "ready": rd["ready"], "warming": false,
        "led": led, "summary": summary, "tooltip": tooltip,
        "sessionCount": n, "activeSession": ctx.sessions.active_id(), "sessions": list, "chrome": rd,
        "adVersion": ctx.ad_version.lock().unwrap().clone(), "uptimeSec": ctx.started.elapsed().as_secs(),
        "pupshotUrl": format!("http://127.0.0.1:{}/dashboard", ctx.port),
    })
}

/// The live snapshot the dashboard page renders (Node: dashboardSnapshot), same field names.
pub fn snapshot(ctx: &Ctx) -> Value {
    let now = pup_core::now_ms();
    let mut wins: Vec<Value> = Vec::new();
    let mut buttons: Vec<Value> = Vec::new();
    for s in ctx.sessions.all() {
        let states = ctx.tabstate.lock().unwrap();
        let tabs: Vec<Value> = s.tabs.iter().map(|t| { let st = states.get(&t.cdp_session); json!({ "tabId": t.tab_id, "url": t.url, "title": t.title, "fav": t.fav, "active": s.active().map(|a| a.tab_id == t.tab_id).unwrap_or(false), "crashed": false, "crashReason": Value::Null, "opener": "agent", "errorCount": st.map(|x| x.errors.len()).unwrap_or(0), "fav": Value::Null }) }).collect();
        let errors: Vec<Value> = s.tabs.iter().flat_map(|t| states.get(&t.cdp_session).map(|x| x.errors.clone()).unwrap_or_default()).collect();
        drop(states);
        let badge_file = ctx.host.temp_dir().join("pup-badge-debug").join(format!("{}.png", pup_session::sanitize_sid(&s.id)));
        let painted = badge_file.exists();
        let overlay_led = s.overlay_led.clone().unwrap_or_else(|| if s.app_id.is_none() { "red".into() } else if painted { "green".into() } else { "amber".into() });
        let url = s.active().map(|t| t.url.clone()).unwrap_or_default();
        wins.push(json!({
            "sessionId": s.id, "profile": s.profile, "owner": s.owner, "ownerSource": if s.owner.is_some() { "caller-identity" } else { Value::Null.as_str().unwrap_or("none") }, "ownerThread": s.owner, "ownerContainer": s.owner_container, "lastThread": s.owner,
            "foreground": s.foreground, "foregroundReason": s.last_note, "foregroundAt": Value::Null, "foregroundVerb": Value::Null, "background": if s.foreground { "foreground" } else { "backgrounded" },
            "highFps": false, "highFpsMinutesLeft": 0, "openedAt": s.created_ms, "uptimeMs": now.saturating_sub(s.created_ms), "tabCount": s.tabs.len(), "tabs": tabs,
            "overlay": s.overlay_desc.clone().unwrap_or_else(|| if painted { "page-favicon".into() } else { "none".into() }), "overlayWhy": s.overlay_why.clone().unwrap_or_else(|| json!({ "at": if painted { now } else { 0 }, "why": if painted { "painted through ab desktop_taskbar" } else { "not painted yet" } })),
            "_curAppId": s.app_id, "wiki": { "authed": Value::Null, "view": Value::Null, "isWiki": pup_identity::is_adom_url(&url) },
            "identityPending": false, "identityPendingMs": 0, "overlayLed": overlay_led, "hwnd": s.hwnd, "emulatedViewport": s.emulated, "lostBrowser": pup_verbs::browser_for(ctx, &s.profile).is_none(),
            "lastAgentUpdate": s.last_agent_update_ms, "errorCount": errors.len(), "recentErrors": errors.iter().rev().take(5).map(|e| json!({ "type": e["type"], "text": e["text"].as_str().unwrap_or("").chars().take(200).collect::<String>() })).collect::<Vec<_>>(), "flashCount": 0, "url": url,
        }));
        let (key, label, base_icon) = match &s.app_id { Some(a) => (format!("aumid:{a}"), s.id.clone(), format!("/dashboard/stampicon?session={}", s.id)), None => (format!("chrome-process:{}", s.profile), format!("Chrome ({})", s.profile), format!("/dashboard/baseicon?profile={}", s.profile)) };
        if let Some(b) = buttons.iter_mut().find(|b| b["key"] == json!(key)) { b["windows"].as_array_mut().unwrap().push(json!(s.id)); if b["led"] == json!("green") && overlay_led != "green" { b["led"] = json!(overlay_led); } }
        else { buttons.push(json!({ "key": key, "label": label, "kind": "process", "baseIcon": base_icon, "grouping": ctx.settings.get("taskbarGrouping", "split"), "windows": [s.id], "badgeSession": s.id, "badgeAt": now, "badgeVia": "newest-painted", "badgeLed": overlay_led, "led": overlay_led, "identityPending": 0, "ledWhy": match overlay_led.as_str() { "green" => "every window wears its real page-favicon overlay — dashboard matches the taskbar", "amber" => "stamped, overlay not painted yet (or the page had no favicon)", _ => "a window under this button is not identity-stamped yet" } })); }
    }
    wins.sort_by(|a, b| b["openedAt"].as_u64().cmp(&a["openedAt"].as_u64()));
    let mut threads: Vec<String> = wins.iter().filter_map(|w| w["owner"].as_str().map(|s| s.to_string())).collect(); threads.sort(); threads.dedup();
    let mut profiles = serde_json::Map::new();
    for w in &wins { let p = w["profile"].as_str().unwrap_or("").to_string(); let n = profiles.get(&p).and_then(|v| v.as_u64()).unwrap_or(0); profiles.insert(p, json!(n + 1)); }
    let toasts: Vec<Value> = ctx.toasts.lock().unwrap().iter().filter(|t| now.saturating_sub(t["at"].as_u64().unwrap_or(0)) < 15000).cloned().collect();
    json!({
        "ts": now,
        "bridge": { "version": pup_core::version(), "shell": "rust", "platform": std::env::consts::OS, "uptimeMs": ctx.started.elapsed().as_millis() as u64, "depsOk": true, "minimalTouch": ctx.settings.get("minimalTouch", "on") == "on", "flashedPending": 0, "settings": pup_verbs::settings::snapshot(ctx), "win": { "inProcess": true, "why": Value::Null }, "loops": [], "abEnumLastMin": 0 },
        "counts": { "windows": wins.len(), "tabs": wins.iter().map(|w| w["tabCount"].as_u64().unwrap_or(0)).sum::<u64>(), "browsers": ctx.browsers.lock().unwrap().len(), "threads": threads.len() },
        "threads": threads, "profiles": profiles, "taskbarButtons": buttons, "untrackedWindows": [], "toasts": toasts, "regen": Value::Null, "windows": wins,
    })
}

/// The SSE feed: a snapshot every 2 s for as long as the client reads (Node: /dashboard/events).
struct SseReader { ctx: std::sync::Arc<Ctx>, buf: Vec<u8>, pos: usize, last: Option<Instant> }
impl Read for SseReader {
    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
        if self.pos >= self.buf.len() {
            if let Some(t) = self.last { let el = t.elapsed(); if el < Duration::from_secs(2) { std::thread::sleep(Duration::from_secs(2) - el); } }
            let payload = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| snapshot(&self.ctx))) { Ok(v) => v.to_string(), Err(_) => json!({ "error": "snapshot panicked", "ts": pup_core::now_ms() }).to_string() };
            self.buf = format!("data: {payload}\n\n").into_bytes(); self.pos = 0; self.last = Some(Instant::now());
        }
        let n = (self.buf.len() - self.pos).min(out.len());
        out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]); self.pos += n;
        Ok(n)
    }
}

fn png_1x1() -> Vec<u8> { pup_cdp::base64_decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==").unwrap_or_default() }

/// LED row for the dock card (Node: /api/status).
pub fn api_status(ctx: &Ctx) -> Value {
    let rd = ctx.installer.readiness(&*ctx.host, ctx.prefer_cft());
    let failed = rd["installPhase"] == json!("failed"); let installing = rd["installing"] == json!(true); let cft = rd["chromeForTestingInstalled"] == json!(true);
    let ready = rd["ready"] == json!(true);
    let leds = vec![
        json!({ "key": "chrome", "label": "Chrome for Testing", "state": if failed { "fail" } else if cft { "ok" } else { "warn" }, "detail": if failed { format!("install FAILED: {}", rd["lastError"].as_str().unwrap_or("?")) } else if installing { format!("downloading ({}%)", rd["installProgressPct"].as_u64().unwrap_or(0)) } else if cft { format!("installed, build {}", rd["chromeBuildId"].as_str().unwrap_or("")) } else { "not installed yet".into() }, "short": if cft { "installed" } else if installing { "installing" } else { "not installed" }, "_hint": if cft { Value::Null } else { json!("Run pup_prewarm to install and warm Chrome for Testing before the first window.") } }),
        json!({ "key": "bridge", "label": "Bridge", "state": "ok", "detail": format!("pup {} (rust) answering on port {}", pup_core::version(), ctx.port), "short": "answering", "_hint": Value::Null }),
        json!({ "key": "profile", "label": "Profile", "state": if ready { "ok" } else { "warn" }, "detail": format!("{} window(s) on the durable adom-you profile", ctx.sessions.for_profile("adom-you").len()), "short": if ready { "adom-you" } else { "waiting" }, "_hint": Value::Null }),
    ];
    json!({ "ok": true, "leds": leds, "version": pup_core::version(), "shell": "rust", "windows": ctx.sessions.count(), "sayPhrases": serde_json::from_str::<Value>(SAY_PHRASES).unwrap_or(json!([])) })
}

/// /identity: who the desktop user is (from ~/.adom/session.json + carbon), cached 10 min.
pub fn identity(ctx: &Ctx) -> Value {
    static CACHE: std::sync::Mutex<Option<(Instant, Value)>> = std::sync::Mutex::new(None);
    if let Some((t, v)) = CACHE.lock().unwrap().as_ref() { if t.elapsed() < Duration::from_secs(600) { return v.clone(); } }
    let sess: Value = std::fs::read_to_string(ctx.host.state_dir().join("session.json")).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or(json!({}));
    let token = sess["token"].as_str().or(sess["session_token"].as_str()).or(sess["access_token"].as_str()).or(sess["session"]["token"].as_str()).map(|s| s.to_string());
    let (mut user, mut orgs) = (Value::Null, Vec::new());
    if let Some(t) = &token {
        let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(6)).build();
        if let Ok(r) = agent.get("https://carbon.adom.inc/user").set("Authorization", &format!("Bearer {t}")).call() { user = r.into_json().unwrap_or(Value::Null); }
        if let Ok(r) = agent.get("https://carbon.adom.inc/user/orgs").set("Authorization", &format!("Bearer {t}")).call() { let j: Value = r.into_json().unwrap_or(json!([])); orgs = j.as_array().cloned().or_else(|| j["orgs"].as_array().cloned()).unwrap_or_default(); }
    }
    let u = if user.is_object() { user } else { sess["user"].clone() };
    let v = json!({ "ok": true, "displayName": u["display_name"].as_str().or(u["displayName"].as_str()).or(u["name"].as_str()), "username": u["name"].as_str().or(u["username"].as_str()), "email": u["email"], "avatarUrl": u["avatar_url"].as_str().or(u["avatarUrl"].as_str()), "avatarB64": ctx.settings.all().get("identityAvatarB64").cloned().unwrap_or(Value::Null), "isAdmin": u["is_admin"].as_bool().or(u["isAdmin"].as_bool()).unwrap_or(false), "orgs": orgs, "signedIn": token.is_some() });
    *CACHE.lock().unwrap() = Some((Instant::now(), v.clone()));
    v
}

fn action(ctx: &Ctx, caller: &Caller, body: &str) -> Value {
    let a: Value = serde_json::from_str(body).unwrap_or(json!({}));
    let act = a["action"].as_str().unwrap_or("");
    let user = Caller { thread: "user-dashboard".into(), reason: format!("dashboard action {act}"), ..caller.clone() };
    match act {
        "close-all" => { let ids: Vec<String> = ctx.sessions.all().iter().map(|s| s.id.clone()).collect(); for id in &ids { let _ = pup_verbs::close_window(ctx, &user, &json!({ "window": id, "takeover": true })); } json!({ "ok": true, "closed": ids.len() }) }
        "close-window" => { let sid = a["sessionId"].as_str().unwrap_or(""); let r = pup_verbs::close_window(ctx, &user, &json!({ "window": sid, "takeover": true })); json!({ "ok": r["ok"] == json!(true), "closed": sid }) }
        "close-tab" => { let r = pup_verbs::tabs::close_tab(ctx, &user, &json!({ "window": a["sessionId"], "tabId": a["tabId"], "takeover": true })); json!({ "ok": r["ok"] == json!(true), "closed": a["tabId"] }) }
        "set-setting" => { let r = pup_verbs::settings::configure(ctx, &json!({ a["key"].as_str().unwrap_or("").to_string(): a["value"].clone() })); json!({ "ok": r["rejected"].as_array().map(|x| x.is_empty()).unwrap_or(false), "settings": r["settings"], "rejected": r["rejected"] }) }
        "test-caption" => { let ms: u64 = ctx.settings.get("fgCaptionMs", "3500").parse().unwrap_or(3500); let _ = ctx.ab.call("desktop_caption", json!({ "text": "pup: this is what a foreground caption looks like", "duration": ms, "expiresInMs": ms, "position": "bottom", "size": "medium", "id": "pup-test-caption" }), &user, Duration::from_secs(6)); json!({ "ok": true, "ms": ms }) }
        "import-scan" | "import-run" => json!({ "ok": false, "error": "browser-login import is not in the Rust build yet (Phase 3)" }),
        _ => json!({ "ok": false, "error": format!("unknown action {act}") }),
    }
}

fn tile_for(ctx: &Ctx, sid: &str) -> Option<Vec<u8>> {
    let s = ctx.sessions.get(sid)?;
    let name = s.owner.clone().unwrap_or(s.id.clone());
    let tile = ctx.host.temp_dir().join("pup-thread-tiles").join(pup_identity::TILE_GEN).join(format!("{}.ico", pup_identity::thread_slug(&name)));
    std::fs::read(tile).ok()
}

/// One function per route; the binary only maps method+path here. None = 404.
pub fn route(ctx: &std::sync::Arc<Ctx>, caller: &Caller, method: &str, path: &str, query: &str, body: &str) -> Option<Reply> {
    let png = |b: Vec<u8>, cache: &str| Some(Reply::Bytes(200, "image/png".into(), b, Some(cache.into())));
    match (method, path) {
        ("GET", "/") | ("GET", "/dashboard") | ("GET", "/dashboard/") => {
            let html = DASHBOARD_HTML.replace("${BRIDGE_VERSION}", pup_core::version()).replace("${JSON.stringify(SAY_PHRASES)}", SAY_PHRASES);
            Some(Reply::Bytes(200, "text/html; charset=utf-8".into(), html.into_bytes(), Some("no-store".into())))
        }
        ("GET", "/dashboard/events") => Some(Reply::Stream(Box::new(SseReader { ctx: ctx.clone(), buf: Vec::new(), pos: 0, last: None }))),
        ("GET", "/dashboard/icon.svg") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))),
        ("GET", "/favicon.ico") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))),
        ("GET", "/dashboard/newbie-panel.js") => Some(Reply::Bytes(200, "application/javascript".into(), assets_gen::get("newbie-panel.js").map(|b| b.to_vec()).unwrap_or_default(), Some("public, max-age=600".into()))),
        ("GET", "/dashboard/example") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); assets_gen::get(&format!("icons/settings-ex/{n}.png")).map(|b| Reply::Bytes(200, "image/png".into(), b.to_vec(), Some("no-store".into()))) }
        // 2.0.687: the page sends n=, the route read name=, so every Settings sample badge was a broken image
        ("GET", "/dashboard/sample-favicon") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); let (file, ct) = match n.as_str() { "wiki" => ("wiki.svg", "image/svg+xml"), "ti" => ("ti.ico", "image/x-icon"), "mouser" => ("mouser.ico", "image/x-icon"), "digikey" => ("digikey.svg", "image/svg+xml"), _ => return None }; assets_gen::get(&format!("icons/settings-samples/{file}")).map(|b| Reply::Bytes(200, ct.into(), b.to_vec(), Some("public, max-age=3600".into()))) }
        ("GET", "/dashboard/sample-cat") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); pup_identity::ICONS.iter().find(|(name, _)| *name == n || name.trim_end_matches(".ico") == n.trim_end_matches(".ico")).map(|(_, b)| Reply::Bytes(200, "image/x-icon".into(), b.to_vec(), Some("public, max-age=3600".into()))) }
        ("GET", "/dashboard/stampicon") => { let sid = q(query, "session").unwrap_or_default(); match tile_for(ctx, &sid) { Some(b) => Some(Reply::Bytes(200, "image/x-icon".into(), b, Some("public, max-age=60".into()))), None => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[1].1.to_vec(), Some("public, max-age=60".into()))) } }
        ("GET", "/dashboard/badge") | ("GET", "/dashboard/bestbadge") => { let sid = q(query, "session").unwrap_or_default(); let f = ctx.host.temp_dir().join("pup-badge-debug").join(format!("{}.png", pup_session::sanitize_sid(&sid))); match std::fs::read(f) { Ok(b) => png(b, "public, max-age=30"), Err(_) => png(png_1x1(), "no-store") } }
        ("GET", "/dashboard/baseicon") => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[0].1.to_vec(), Some("public, max-age=3600".into()))),
        ("GET", "/dashboard/favicon") => {
            let sid = q(query, "session").unwrap_or_default();
            let s = ctx.sessions.get(&sid)?; let t = q(query, "tab").and_then(|id| s.tab_by_id(&id).cloned()).or_else(|| s.active().cloned())?;
            let b = pup_verbs::browser_for(ctx, &s.profile)?;
            let js = r#"(async () => { const l = document.querySelector('link[rel~="icon"], link[rel="shortcut icon"]'); const href = (l && l.href) || (location.origin + '/favicon.ico'); try { const r = await fetch(href, { credentials: 'include' }); if (!r.ok) return null; const bl = await r.blob(); return await new Promise(res => { const fr = new FileReader(); fr.onload = () => res(fr.result); fr.onerror = () => res(null); fr.readAsDataURL(bl); }); } catch (e) { return null; } })()"#;
            let generic = || Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=60".into())));
            let Some(data) = b.cdp.evaluate(&t.cdp_session, js, Duration::from_secs(8)).ok().and_then(|v| v.as_str().map(|s| s.to_string())) else { return generic() };
            let Some((mime, b64)) = data.trim_start_matches("data:").split_once(";base64,") else { return generic() };
            match pup_cdp::base64_decode(b64) { Ok(bytes) if !bytes.is_empty() => Some(Reply::Bytes(200, mime.to_string(), bytes, Some("public, max-age=60".into()))), _ => generic() }
        }
        ("GET", "/dashboard/apply-progress") => Some(Reply::Json(200, json!({ "ok": true, "apply": Value::Null }))),
        ("POST", "/dashboard/toast") | ("POST", "/ui/toast") => {
            let b: Value = serde_json::from_str(body).unwrap_or(json!({}));
            let msg = b["msg"].as_str().or(b["message"].as_str()).unwrap_or("").chars().take(200).collect::<String>();
            if msg.is_empty() { return Some(Reply::Json(200, json!({ "ok": false, "error": "msg required" }))); }
            let mut t = ctx.toasts.lock().unwrap(); let id = t.len() as u64 + 1;
            t.push(json!({ "id": id, "msg": msg, "type": if ["success", "error", "info"].contains(&b["type"].as_str().unwrap_or("")) { b["type"].clone() } else { json!("info") }, "at": pup_core::now_ms() }));
            while t.len() > 20 { t.remove(0); }
            Some(Reply::Json(200, json!({ "ok": true })))
        }
        ("POST", "/dashboard/action") => Some(Reply::Json(200, action(ctx, caller, body))),
        ("GET", "/api/status") => Some(Reply::Json(200, api_status(ctx))),
        ("GET", "/identity") => Some(Reply::Json(200, identity(ctx))),
        // 2.0.534: the identity extension paints the user's avatar on its toolbar button (John: "you should be
        // showing my avatar up in your button"). The bytes come through pup so the extension needs no host
        // permission for wherever the avatar lives; 10-minute cache like identity() itself.
        ("GET", "/identity/avatar") => {
            static AV: std::sync::Mutex<Option<(Instant, String, Vec<u8>)>> = std::sync::Mutex::new(None);
            if let Some((t, m, b)) = AV.lock().unwrap().as_ref() { if t.elapsed() < Duration::from_secs(600) { return Some(Reply::Bytes(200, m.clone(), b.clone(), Some("public, max-age=600".into()))); } }
            let id = identity(ctx);
            let mut got: Option<(String, Vec<u8>)> = None;
            if let Some(b64) = id["avatarB64"].as_str() {
                let raw = b64.split("base64,").last().unwrap_or(b64);
                if let Ok(bytes) = pup_cdp::base64_decode(raw) { got = Some(("image/png".into(), bytes)); }
            }
            if got.is_none() {
                if let Some(u) = id["avatarUrl"].as_str() {
                    let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(8)).redirects(4).build();
                    if let Ok(r) = agent.get(u).call() {
                        let mime = r.header("Content-Type").unwrap_or("image/png").split(';').next().unwrap_or("image/png").to_string();
                        let mut b = Vec::new(); let _ = r.into_reader().take(2 * 1024 * 1024).read_to_end(&mut b);
                        if !b.is_empty() { got = Some((mime, b)); }
                    }
                }
            }
            match got {
                Some((m, b)) => { *AV.lock().unwrap() = Some((Instant::now(), m.clone(), b.clone())); Some(Reply::Bytes(200, m, b, Some("public, max-age=600".into()))) }
                None => Some(Reply::Json(404, json!({ "ok": false, "error": "no avatar for this identity" }))),
            }
        }
        _ => hydrogen::route(method, path, query, body),
    }
}