app
Shotlog - Screenshot Log
Public Made by Adomby adom
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 Bridge child-window arrays via --meta.
← Commit history
Publish 0.16.1
7 files changed
+67−27
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.16.0"+version = "0.16.1" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.16.0"+version = "0.16.1" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1@@ -1 +1 @@-0.16.0+0.16.1
bin/adom-shotlog⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.16.0",+ "version": "0.16.1", "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.16.0",+ "version": "0.16.1", "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/server.rs+62−22@@ -374,10 +374,22 @@ async fn canvas_close( let surface = req.surface.clone(); let v = tokio::task::spawn_blocking(move || { if req.surface == "webview" {- let ok = std::process::Command::new("adom-cli")- .args(["hydrogen", "workspace", "remove-tab", "--name", &format!("Shots: {}", req.channel)])- .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())- .status().map(|s| s.success()).unwrap_or(false);+ let name = format!("Shots: {}", req.channel);+ let out = std::process::Command::new("adom-cli")+ .args(["hydrogen", "workspace", "remove-tab", "--name", &name])+ .output();+ let mut ok = out.as_ref().map(|o| o.status.success()+ && !String::from_utf8_lossy(&o.stdout).contains("\"error\"")).unwrap_or(false);+ // Last tab in its pane: Hydrogen requires closing the whole pane.+ if !ok {+ let text = out.map(|o| format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))).unwrap_or_default();+ if text.contains("Cannot remove all tabs") {+ ok = std::process::Command::new("adom-cli")+ .args(["hydrogen", "workspace", "remove-tab", "--name", &name, "--mode", "all"])+ .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())+ .status().map(|s| s.success()).unwrap_or(false);+ }+ } serde_json::json!({"ok": ok, "surface": "webview"}) } else if req.surface == "pup" { let ok = crate::open::close_pup_tab(&req.channel);@@ -565,20 +577,37 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], /// lands with nobody watching. Runs via a self-subprocess (open_pup can exit). /// If pup genuinely cannot open, falls back to a webview - never invisible. fn force_open_pup(channel: &str) {- if !debounce_ok(&format!("openpup:{channel}"), 60) {+ // Debounce longer than the whole retry window so attempts never overlap.+ if !debounce_ok(&format!("openpup:{channel}"), 240) { return; } let channel = channel.to_string(); std::thread::spawn(move || {+ // PATIENT: right after a laptop reboot Adom Desktop takes a minute to+ // reconnect. Retry pup before surrendering to the webview fallback -+ // the user's pref says pup, so give pup a real chance to come up.+ let tries: u32 = std::env::var("SHOTLOG_PUP_RETRY_N").ok().and_then(|v| v.parse().ok()).unwrap_or(6);+ let wait_s: u64 = std::env::var("SHOTLOG_PUP_RETRY_SECS").ok().and_then(|v| v.parse().ok()).unwrap_or(20); 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 {- // pup unavailable right now - fall back so the shot isn't invisible.- crate::open::open_webview(&channel, None, None, 8820, true, 0.2);+ for attempt in 1..=tries {+ 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 {+ if attempt > 1 {+ println!("OK: pup force-open for '{channel}' succeeded on attempt {attempt} (Adom Desktop is back).");+ }+ return;+ }+ println!("WARN: pup force-open for '{channel}' attempt {attempt}/{tries} failed (Adom Desktop offline / reconnecting?){}",+ if attempt < tries { format!(" - retrying in {wait_s}s") } else { String::new() });+ if attempt < tries {+ std::thread::sleep(std::time::Duration::from_secs(wait_s));+ } }+ println!("WARN: pup never came up for '{channel}' - falling back to a webview so the shots aren't invisible (channel pref unchanged).");+ crate::open::open_webview(&channel, None, None, 8820, true, 0.2); }); } @@ -1138,7 +1167,13 @@ async fn ws_connection(mut socket: WebSocket, channel: String, state: Arc<AppSta return; // reconnected - just a blip } let ch2 = ch.clone();- let tab_gone = tokio::task::spawn_blocking(move || {+ // Three-way verdict - flip ONLY on DEFINITIVE evidence of a user close:+ // Some(true) = AD reachable AND (session alive with the tab absent,+ // OR session_not_found while AD is up) -> user closed it+ // Some(false) = tab still there -> blip/reload+ // None = AD offline / error / can't tell -> a REBOOT looks+ // exactly like this; never mistake it for a choice.+ let verdict: Option<bool> = tokio::task::spawn_blocking(move || { let mut cmd = std::process::Command::new("adom-desktop"); if let Ok(t) = std::env::var("SHOTLOG_AD_TARGET") { cmd.args(["--target", &t]);@@ -1146,20 +1181,25 @@ async fn ws_connection(mut socket: WebSocket, channel: String, state: Arc<AppSta match cmd.args(["browser_list_tabs", "{\"sessionId\":\"shotlog\"}"]).output() { Ok(o) => { let out = String::from_utf8_lossy(&o.stdout).to_string();- match serde_json::from_str::<serde_json::Value>(&out) {- Ok(v) => !v.get("tabs").and_then(|t| t.as_array())- .map(|tabs| tabs.iter().any(|t| t.get("url").and_then(|u| u.as_str())- .map(|u| u.contains(&format!("/log/{ch2}/"))).unwrap_or(false)))- .unwrap_or(false),- Err(_) => true, // session itself is gone -> tabs gone+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(&out) {+ if let Some(tabs) = v.get("tabs").and_then(|t| t.as_array()) {+ return Some(!tabs.iter().any(|t| t.get("url").and_then(|u| u.as_str())+ .map(|u| u.contains(&format!("/log/{ch2}/"))).unwrap_or(false)));+ }+ if out.contains("session_not_found") {+ // AD answered; the whole pup window is gone while+ // the laptop is up -> the user closed the window.+ return Some(true);+ } }+ None // no-clients / ambiguous / unparseable -> uncertain }- Err(_) => false, // cannot probe: never flip on uncertainty+ Err(_) => None, } }) .await- .unwrap_or(false);- if tab_gone && !store.viewer_infos(&ch).await.iter().any(|i| i.surface == "pup") {+ .unwrap_or(None);+ if verdict == Some(true) && !store.viewer_infos(&ch).await.iter().any(|i| i.surface == "pup") { store.set_canvas_pref(&ch, "webview").await; println!("OK: pup tab for '{ch}' was closed by the user - canvas preference reverted to webview (next auto-open uses the slim panel)."); }