← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.23.8"+version = "0.23.9" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.23.8"+version = "0.23.9" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1
@@ -1 +1 @@-0.23.8+0.23.9
bin/adom-shotlog
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.23.8",+  "version": "0.23.9",   "type": "app",   "title": "Adom Shotlog",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Desktop child-window arrays via --meta.",
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.23.8",+  "version": "0.23.9",   "type": "app",   "title": "Adom Shotlog",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Desktop child-window arrays via --meta.",
src/open.rs+1−1
@@ -292,7 +292,7 @@ pub fn open_pup(channel: &str, port: u16, swap: bool) {  /// Does the broker currently see ANY pup viewer connected (the real proof a /// pup page is alive)? Minimal HTTP GET, no external deps.-fn broker_has_pup_viewer(port: u16) -> bool {+pub fn broker_has_pup_viewer(port: u16) -> bool {     use std::io::{Read, Write};     let Ok(mut st) = std::net::TcpStream::connect(("127.0.0.1", port)) else { return false };     let _ = st.set_read_timeout(Some(std::time::Duration::from_secs(3)));
src/server.rs+55
@@ -375,6 +375,15 @@ async fn set_prefs(         if c != "pup" && c != "webview" {             return Json(serde_json::json!({"ok": false, "error": "default_canvas must be pup|webview"}));         }+        if c != p.default_canvas {+            // The human just made a NEW explicit choice - stale in-memory+            // click-notes must not override it ("all channels adhere to the+            // GLOBAL default"; notes only exist for clicks made SINCE).+            let cleared = state.store.clear_all_canvas_prefs().await;+            if cleared > 0 {+                println!("{} DECISION default canvas changed to {c} - cleared {cleared} in-memory canvas note(s).", ts());+            }+        }         p.default_canvas = c;     }     if let Some(h) = patch.read_hook {@@ -670,6 +679,15 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo],     let has_webview = infos.iter().any(|i| i.surface == "webview");     if has_webview {         bring_webview_forward(channel, port, alerts);+        // The webview FALLBACK is a lifeboat, not a destination. If the human's+        // canvas says pup and the only thing connected is a webview (classic:+        // AD was offline, we fell back, AD came back), each new shot buys one+        // cooled-down attempt to bring pup up - and once pup CONNECTS, the+        // fallback webview closes. Staged like the UI switch: nothing closes+        // until the destination is live.+        if pref == "pup" && !has_pup {+            recover_pup_from_fallback(channel, port);+        }         return;     }     if infos.is_empty() && std::env::var("SHOTLOG_NO_FORCE_OPEN").is_err() {@@ -722,6 +740,43 @@ fn force_open_pup(channel: &str, fallback_ratio: f64) {     }); } +/// Recover from the webview fallback once pup is reachable again: try pup once+/// (cooldown 5 min), wait for a REAL pup viewer connect, then close the+/// fallback webview. Never closes anything while pup isn't live.+fn recover_pup_from_fallback(channel: &str, port: u16) {+    if !debounce_ok("puprecover", 300) {+        return;+    }+    let channel = channel.to_string();+    std::thread::spawn(move || {+        println!("{} DECISION default canvas is pup but only a webview is connected (fallback residue) - retrying pup.", ts());+        let exe = std::env::current_exe().unwrap_or_else(|_| "adom-shotlog".into());+        let ok = std::process::Command::new(&exe)+            .args(["open", "-c", &channel, "--pup"])+            .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())+            .status().map(|s| s.success()).unwrap_or(false);+        if !ok {+            println!("{} DECISION pup still unreachable - staying on the fallback webview.", ts());+            return;+        }+        for _ in 0..15 {+            std::thread::sleep(std::time::Duration::from_secs(2));+            if crate::open::broker_has_pup_viewer(port) {+                println!("{} DECISION pup is back and CONNECTED - closing the fallback webview (it was never a choice).", ts());+                let out = std::process::Command::new("adom-cli")+                    .args(["hydrogen", "workspace", "remove-tab", "--name", "Shotlog"])+                    .output();+                let closed = out.map(|o| o.status.success()).unwrap_or(false);+                if !closed {+                    println!("{} WARN: fallback webview tab close failed - pup is live, human may see both surfaces.", ts());+                }+                return;+            }+        }+        println!("{} DECISION pup open reported ok but no pup viewer connected within 30s - leaving the webview alone.", ts());+    });+}+ /// Orange taskbar flash on the pup window hosting a viewer. Debounced per session /// so a burst of injects flashes once, not strobe. fn flash_pup_session(session: &str) {
src/store.rs+10
@@ -607,6 +607,16 @@ impl Store {         self.canvas_prefs.write().await.remove(channel);     } +    /// Forget EVERY in-memory canvas note. Used when the human changes the+    /// global default: the newest explicit intent wins, so stale click-notes+    /// must never override it.+    pub async fn clear_all_canvas_prefs(&self) -> usize {+        let mut m = self.canvas_prefs.write().await;+        let n = m.len();+        m.clear();+        n+    }+     pub async fn canvas_pref(&self, channel: &str) -> Option<String> {         self.canvas_prefs.read().await.get(channel).cloned()     }