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.6.0
11 files changed
+139−51
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.5.0"+version = "0.6.0" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.5.0"+version = "0.6.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
SKILL.md+11−6@@ -132,18 +132,23 @@ Injecting a shot is not the job — the human *seeing* it is. shotlog tells you whether a live viewer is connected, and on which **surface** (webview / pup / mobile / browser), **in the inject response itself** — no guessing, no extra turns: -- **Viewer connected:** `OK: 1 live viewer(s) connected to 'ch' [pup]`. Connected- ≠ guaranteed visible: if that surface might be a background tab or minimized- window, confirm it's the human's foreground surface.+- **On-screen:** `OK: 1 of 1 viewer(s) ON-SCREEN [pup] — a human can see this shot.`+- **Open but backgrounded:** `WARN: 1 viewer(s) connected … but ALL are+ BACKGROUNDED / minimized — the human is NOT looking right now.` Bring the pup+ window / webview tab / phone to the FOREGROUND, then re-check. - **Nobody watching:** `WARN: NO live viewer is connected — the human is NOT seeing this shot.` A viewer is force-opened as a fallback, but **do not assume** it worked. Open the viewer on the human's actual surface and re-check. -**Gate programmatically instead of burning turns** — `shotlog viewers` exits `0`-when a human is watching, `3` when nobody is:+The signal is real: each viewer holds a WebSocket open and reports its **surface**+(webview/pup/mobile/browser) AND whether it's actually **on-screen** (Page+Visibility) vs a background tab / minimized window.++**Gate programmatically instead of burning turns** — `shotlog viewers` exit codes:+`0` = a human has it on-screen, `4` = connected but all backgrounded, `3` = nobody: ```bash-shotlog viewers -c my-project # exit 0 = seen, 3 = unseen; prints surfaces+shotlog viewers -c my-project # 0=on-screen, 4=backgrounded, 3=none; prints surfaces shotlog viewers -c my-project --url "$URL" # also HTTP-checks the endpoint you opened ```
VERSION+1−1@@ -1 +1 @@-0.5.0+0.6.0
bin/adom-shotlog⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.5.0",+ "version": "0.6.0", "type": "app", "title": "Adom Shotlog", "description": "See the shots your AI never showed you \u2014 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 + on which surface, and ingests Adom Desktop owned-popup arrays via --meta.",
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.5.0",+ "version": "0.6.0", "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 + on which surface, and ingests Adom Desktop owned-popup arrays via --meta.",
src/html/index.html+11−2@@ -484,13 +484,22 @@ header { }).catch(e=>{ feed.innerHTML='<div class="empty">load failed: '+esc(e.message)+'</div>'; }); // ── WebSocket live updates ({type:'entry', entry}) ───────────────────────+ let activeWs = null;+ // Report on-screen state whenever this tab/window is shown or hidden, so the+ // server can tell "watching" from "open but backgrounded/minimized".+ document.addEventListener('visibilitychange', () => {+ try { if (activeWs && activeWs.readyState===1)+ activeWs.send(JSON.stringify({type:'vis', visible: document.visibilityState==='visible'})); } catch {}+ }); function connectWs() { const proto = location.protocol==='https:'?'wss:':'ws:'; const baseHref = document.querySelector('base')?.href.replace(location.origin,'') || '/'; const ws = new WebSocket(`${proto}//${location.host}${baseHref}ws/${encodeURIComponent(channel)}`); ws.onopen = () => { wsLed.classList.add('live'); wsLed.setAttribute('data-tooltip','WebSocket live — the server knows you are watching.');- // Tell the server WHICH surface is watching, so `inject`/`viewers` can report it.- try { ws.send(JSON.stringify({type:'hello', surface:detectSurface()})); } catch {} };+ activeWs = ws;+ // Tell the server WHICH surface is watching + whether it's on-screen right now,+ // so `inject`/`viewers` can distinguish "watching" from "open but backgrounded".+ try { ws.send(JSON.stringify({type:'hello', surface:detectSurface(), visible: document.visibilityState==='visible'})); } catch {} }; ws.onmessage = ev => { try { const msg = JSON.parse(ev.data);
src/main.rs+42−20@@ -140,9 +140,10 @@ enum Commands { port: u16, }, /// Verify a HUMAN is actually watching a channel's viewer (live WebSocket- /// presence). Exits 0 if ≥1 viewer is connected, 3 if none — so an AI can- /// gate on it without a round of guessing. Prints the connected surfaces- /// (webview / pup / mobile / browser).+ /// presence + Page Visibility). Exit 0 = ≥1 viewer ON-SCREEN, 4 = viewer(s)+ /// connected but ALL backgrounded/minimized, 3 = nobody connected — so an AI+ /// can gate precisely without guessing. Prints the surfaces (webview / pup /+ /// mobile / browser). Viewers { /// Channel name #[arg(short, long)]@@ -442,19 +443,30 @@ fn run_inject( } } // Presence check — is the human ACTUALLY seeing this, and where?- if let Some(viewers) = val.get("viewers").and_then(|v| v.as_u64()) {- let surfaces: Vec<String> = val.get("surfaces").and_then(|v| v.as_array())+ let getvec = |key: &str| -> Vec<String> {+ val.get(key).and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())- .unwrap_or_default();- if viewers > 0 {- let surf = if surfaces.is_empty() { String::new() } else { format!(" [{}]", surface_label(&surfaces)) };- ok(&format!("{viewers} live viewer(s) connected to '{channel}'{surf}"));- hint("Are you sure the human is seeing the shotlog? Is it open in a webview or pup? Have you verified it's truly showing?");- hint("Connected ≠ necessarily visible: if the surface could be a background tab / minimized window, confirm it's the human's foreground surface.");+ .unwrap_or_default()+ };+ if let Some(viewers) = val.get("viewers").and_then(|v| v.as_u64()) {+ let surfaces = getvec("surfaces");+ let visible = val.get("visible_viewers").and_then(|v| v.as_u64()).unwrap_or(0);+ let vis_surf = getvec("visible_surfaces");+ // The hint the human asked to ride on EVERY inject.+ let verify = "Are you sure the human is seeing the shotlog? Is it open in a webview or pup? Have you verified it's truly showing?";+ if visible > 0 {+ let s = if vis_surf.is_empty() { String::new() } else { format!(" [{}]", surface_label(&vis_surf)) };+ ok(&format!("{visible} of {viewers} viewer(s) ON-SCREEN{s} — a human can see this shot."));+ hint(verify);+ } else if viewers > 0 {+ let s = if surfaces.is_empty() { String::new() } else { format!(" [{}]", surface_label(&surfaces)) };+ warn(&format!("{viewers} viewer(s) connected to '{channel}'{s} but ALL are BACKGROUNDED / minimized — the human is NOT looking right now."));+ hint(verify);+ hint(&format!("Bring the viewer to the FOREGROUND (the pup window / webview tab / phone), then re-check: `shotlog viewers -c {channel}`.")); } else { warn(&format!("NO live viewer is connected to '{channel}' — the human is NOT seeing this shot."));- hint("Are you sure the human is seeing the shotlog? Is it open in a webview or pup? Have you verified it's truly showing?");- hint(&format!("Open it, then RE-CHECK before continuing: `shotlog open -c {channel}` (Hydrogen webview) — or load the viewer in pup / on the phone — then `shotlog viewers -c {channel}` (exits non-zero until a viewer is live)."));+ hint(verify);+ hint(&format!("Open it, then RE-CHECK before continuing: `shotlog open -c {channel}` (Hydrogen webview) — or load the viewer in pup / on the phone — then `shotlog viewers -c {channel}`.")); hint("A viewer was force-opened as a fallback, but do NOT assume the human sees it — verify their actual surface."); } }@@ -535,9 +547,14 @@ fn run_viewers(channel: &str, port: u16, url: Option<&str>) { } }; let viewers = val["viewers"].as_u64().unwrap_or(0);- let surfaces: Vec<String> = val["surfaces"].as_array()- .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())- .unwrap_or_default();+ let visible = val["visible"].as_u64().unwrap_or(0);+ let getvec = |key: &str| -> Vec<String> {+ val[key].as_array()+ .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())+ .unwrap_or_default()+ };+ let surfaces = getvec("surfaces");+ let vis_surf = getvec("visible_surfaces"); // Optional: the AI passes the endpoint it opened; we confirm it's reachable. if let Some(u) = url {@@ -558,11 +575,16 @@ fn run_viewers(channel: &str, port: u16, url: Option<&str>) { } } - if viewers > 0 {- let surf = if surfaces.is_empty() { String::new() } else { format!(" [{}]", surface_label(&surfaces)) };- ok(&format!("{viewers} live viewer(s) watching '{channel}'{surf} — a human can see it."));- hint("Connected ≠ necessarily visible: if a surface could be a background tab / minimized window, confirm it's foregrounded before trusting it.");+ // Three states: on-screen (0), open-but-backgrounded (4), nobody (3).+ if visible > 0 {+ let surf = if vis_surf.is_empty() { String::new() } else { format!(" [{}]", surface_label(&vis_surf)) };+ ok(&format!("{visible} of {viewers} viewer(s) ON-SCREEN{surf} watching '{channel}' — a human can see it.")); std::process::exit(0);+ } else if viewers > 0 {+ let surf = if surfaces.is_empty() { String::new() } else { format!(" [{}]", surface_label(&surfaces)) };+ warn(&format!("{viewers} viewer(s) connected to '{channel}'{surf} but ALL are BACKGROUNDED / minimized — the human isn't looking right now."));+ hint(&format!("Bring the viewer to the FOREGROUND (pup window / webview tab / phone), then re-run `shotlog viewers -c {channel}`."));+ std::process::exit(4); } else { warn(&format!("NO live viewer connected to '{channel}' — nothing you inject here is being seen.")); hint(&format!("Open it: `shotlog open -c {channel}` (Hydrogen webview), or load the viewer in pup / on the phone, then re-run `shotlog viewers -c {channel}`."));
src/server.rs+39−12@@ -138,13 +138,18 @@ async fn viewer_status( Query(q): Query<ChannelQuery>, ) -> Json<serde_json::Value> { let channel = q.channel.unwrap_or_else(|| "default".into());- let viewers = state.store.viewer_count(&channel).await;- let surfaces = state.store.viewer_surfaces(&channel).await;+ let infos = state.store.viewer_infos(&channel).await;+ let viewers = infos.len();+ let surfaces: Vec<String> = infos.iter().map(|i| i.surface.clone()).collect();+ let visible: usize = infos.iter().filter(|i| i.visible).count();+ let visible_surfaces: Vec<String> = infos.iter().filter(|i| i.visible).map(|i| i.surface.clone()).collect(); Json(serde_json::json!({ "channel": channel, "viewers": viewers, "has_viewers": viewers > 0, "surfaces": surfaces,+ "visible": visible,+ "visible_surfaces": visible_surfaces, })) } @@ -265,6 +270,10 @@ struct InjectResponse { /// can immediately tell whether the human is actually seeing this shot. viewers: usize, surfaces: Vec<String>,+ /// Of those, how many are actually ON-SCREEN (Page Visibility) vs a+ /// background tab / minimized window, and which surfaces those are.+ visible_viewers: usize,+ visible_surfaces: Vec<String>, } async fn post_shots(@@ -325,9 +334,12 @@ async fn post_shots( .await; // Presence at inject time — the caller uses this to know if the human is- // actually watching (and on which surface) without a round of guessing.- let viewers = state.store.viewer_count(&req.channel).await;- let surfaces = state.store.viewer_surfaces(&req.channel).await;+ // actually watching (and on which surface / on-screen) without guessing.+ let infos = state.store.viewer_infos(&req.channel).await;+ let viewers = infos.len();+ let surfaces: Vec<String> = infos.iter().map(|i| i.surface.clone()).collect();+ let visible_viewers = infos.iter().filter(|i| i.visible).count();+ let visible_surfaces: Vec<String> = infos.iter().filter(|i| i.visible).map(|i| i.surface.clone()).collect(); // If nobody is watching this channel, force-open a viewer so the user // actually sees the shot instead of it landing invisibly in the log.@@ -348,6 +360,8 @@ async fn post_shots( original_h: entry.orig_h, viewers, surfaces,+ visible_viewers,+ visible_surfaces, })) } @@ -401,8 +415,11 @@ async fn post_paste( .await; - let viewers = state.store.viewer_count(&req.channel).await;- let surfaces = state.store.viewer_surfaces(&req.channel).await;+ let infos = state.store.viewer_infos(&req.channel).await;+ let viewers = infos.len();+ let surfaces: Vec<String> = infos.iter().map(|i| i.surface.clone()).collect();+ let visible_viewers = infos.iter().filter(|i| i.visible).count();+ let visible_surfaces: Vec<String> = infos.iter().filter(|i| i.visible).map(|i| i.surface.clone()).collect(); Ok(Json(InjectResponse { ok: true,@@ -417,6 +434,8 @@ async fn post_paste( original_h: if was_resized { Some(orig_h) } else { None }, viewers, surfaces,+ visible_viewers,+ visible_surfaces, })) } @@ -554,7 +573,7 @@ async fn ws_connection(mut socket: WebSocket, channel: String, state: Arc<AppSta let vid = state .next_viewer_id .fetch_add(1, std::sync::atomic::Ordering::Relaxed);- state.store.register_viewer(&channel, vid, "browser".into()).await;+ state.store.register_viewer(&channel, vid, "browser".into(), true).await; loop { tokio::select! {@@ -577,12 +596,20 @@ async fn ws_connection(mut socket: WebSocket, channel: String, state: Arc<AppSta match msg { Some(Ok(Message::Close(_))) | None => break, Some(Ok(Message::Text(t))) => {- // {type:'hello', surface:'pup'|'webview'|'mobile'|'browser'}+ // {type:'hello', surface, visible} on connect; {type:'vis', visible}+ // whenever the tab is shown/hidden (Page Visibility). if let Ok(v) = serde_json::from_str::<serde_json::Value>(&t) {- if v.get("type").and_then(|x| x.as_str()) == Some("hello") {- if let Some(s) = v.get("surface").and_then(|x| x.as_str()) {- state.store.register_viewer(&channel, vid, s.to_string()).await;+ match v.get("type").and_then(|x| x.as_str()) {+ Some("hello") => {+ let surface = v.get("surface").and_then(|x| x.as_str()).unwrap_or("browser").to_string();+ let visible = v.get("visible").and_then(|x| x.as_bool()).unwrap_or(true);+ state.store.register_viewer(&channel, vid, surface, visible).await; }+ Some("vis") => {+ let visible = v.get("visible").and_then(|x| x.as_bool()).unwrap_or(true);+ state.store.set_viewer_visibility(&channel, vid, visible).await;+ }+ _ => {} } } }
src/store.rs+31−6@@ -45,12 +45,20 @@ struct Channel { const MAX_ENTRIES: usize = 200; +/// One connected viewer: which surface it's on, and whether that surface is+/// actually on-screen (Page Visibility) vs a background tab / minimized window.+#[derive(Clone)]+pub struct ViewerInfo {+ pub surface: String,+ pub visible: bool,+}+ pub struct Store { channels: RwLock<HashMap<String, Channel>>, base_dir: PathBuf,- /// Live viewer presence: channel -> (connection id -> surface label).+ /// Live viewer presence: channel -> (connection id -> viewer info). /// Surface is what the viewer reported on connect: webview | pup | mobile | browser.- presence: RwLock<HashMap<String, HashMap<u64, String>>>,+ presence: RwLock<HashMap<String, HashMap<u64, ViewerInfo>>>, } impl Store {@@ -277,10 +285,21 @@ impl Store { channels.get(channel_name).map(|ch| ch.tx.receiver_count()).unwrap_or(0) } - /// Register a connected viewer with the surface it reported (webview/pup/mobile/browser).- pub async fn register_viewer(&self, channel_name: &str, id: u64, surface: String) {+ /// Register (or update) a connected viewer with the surface it reported+ /// (webview/pup/mobile/browser) and whether it's currently on-screen.+ pub async fn register_viewer(&self, channel_name: &str, id: u64, surface: String, visible: bool) {+ let mut p = self.presence.write().await;+ p.entry(channel_name.to_string())+ .or_default()+ .insert(id, ViewerInfo { surface, visible });+ }++ /// Update just the on-screen (Page Visibility) state of an existing viewer.+ pub async fn set_viewer_visibility(&self, channel_name: &str, id: u64, visible: bool) { let mut p = self.presence.write().await;- p.entry(channel_name.to_string()).or_default().insert(id, surface);+ if let Some(v) = p.get_mut(channel_name).and_then(|m| m.get_mut(&id)) {+ v.visible = visible;+ } } /// Drop a viewer on disconnect.@@ -292,10 +311,16 @@ impl Store { } } + /// Full presence for a channel — one entry per connected viewer.+ pub async fn viewer_infos(&self, channel_name: &str) -> Vec<ViewerInfo> {+ let p = self.presence.read().await;+ p.get(channel_name).map(|m| m.values().cloned().collect()).unwrap_or_default()+ }+ /// Surfaces of the live viewers for a channel, e.g. ["pup","mobile"]. pub async fn viewer_surfaces(&self, channel_name: &str) -> Vec<String> { let p = self.presence.read().await;- p.get(channel_name).map(|m| m.values().cloned().collect()).unwrap_or_default()+ p.get(channel_name).map(|m| m.values().map(|v| v.surface.clone()).collect()).unwrap_or_default() } /// Get a broadcast receiver for a channel's SSE stream.