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.27.0
11 files changed
+95−17
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.26.2"+version = "0.27.0" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.26.2"+version = "0.27.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
README.md+1@@ -45,6 +45,7 @@ adom-shotlog open -c my-project --skinny # a ~20% right-side column - It **auto-updates live** over a WebSocket: the moment any thread injects a shot, the card appears at the top of that channel, no refresh, no polling. - **One panel, shotlog's own tabs**: the viewer is a single app that manages a tab per channel itself (read-captures pinned first as the global auto-capture feed). New shots flash their tab orange, show an unseen badge, and (by default) auto-switch to the updated channel - all inside the app, no workspace tab juggling. - If a shot lands in a channel with **nobody watching**, the broker auto-opens the slim 20% webview for it, so a shot is never logged invisibly. (Opt out with `SHOTLOG_NO_FORCE_OPEN=1`.)+- The **clear** button wipes EVERY channel (all the per-thread tabs that build up) and reports exactly what came back: `cleared N shots across M threads - freed X GB of disk`. Safe for running AIs: a channel auto-recreates the moment any thread injects to it again. - The layout is **fully fluid** at that skinny width: the header wraps to rows, the child-window thumbnails reflow, and the outer page never shows a horizontal scrollbar. ### Phone
VERSION+1−1@@ -1 +1 @@-0.26.2+0.27.0
bin/adom-shotlog⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+15@@ -416,3 +416,18 @@ ctrl_tx as {type:"prefs",prefs} and every open page applies them live (APREFS was cached from boot before - an API prefs change didn't reach open pages until reload; that staleness invalidated a live test once). renderPrefs uses ===true for default-false toggles (!==false is wrong once a default flips).++## Clear = EVERYTHING (0.27.0, John): shots + thread tabs, with a freed report+The viewer's clear button now calls DELETE /api/clear-everything: wipes every+channel (entries + files + remove_dir_all + the in-memory Channel), so all+thread tabs disappear. Toast reports 'cleared N shots across M threads -+freed X GB/MB/KB' (bytes summed per deleted file, byte-exact vs du). The+broker broadcasts {type:"cleared-all"} on ctrl_tx: every OTHER open page+resets its tab strip (window._wipeLocalAll) + gets an informational toast;+the initiating page suppresses the broadcast via window._selfCleared so its+detailed toast isn't stomped. SAFETY INVARIANT (verified live): channels are+created on demand, so an AI injecting into a wiped channel name seconds later+just auto-recreates it (fresh dir, ids restart at 1, tab reappears via the+WS entry handler's ensureChan). Per-channel /api/clear-all still exists for+programmatic use. NEVER test clear-everything against the live 8820 broker -+it nukes John's real channels; use a throwaway broker + data dir.
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.26.2",+ "version": "0.27.0", "type": "app", "title": "Shotlog - Screenshot Log", "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 Bridge child-window arrays via --meta.",
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.26.2",+ "version": "0.27.0", "type": "app", "title": "Shotlog - Screenshot Log", "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 Bridge child-window arrays via --meta.",
src/html/index.html+30−12@@ -540,7 +540,7 @@ body.compact .card-popups .ph { padding-top: 4px; } <button class="hdr-btn icon-only" id="toTopBtn" data-tooltip="Scroll to newest (top)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg></button> <button class="hdr-btn icon-only" id="filterBtn" data-tooltip="Filter shots: child-window arrays, capture endpoint, safe/full pairs, direct vs auto-captured."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/></svg></button> <button class="hdr-btn icon-only" id="prefsBtn" data-tooltip="Settings: the auto-capture read-hook, and whether channels default to webview or pup."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></button>- <button class="hdr-btn danger" id="clearBtn" data-tooltip="Delete ALL screenshots in this channel (cannot be undone)">clear</button>+ <button class="hdr-btn danger" id="clearBtn" data-tooltip="Delete ALL screenshots in EVERY channel and remove all the thread tabs (cannot be undone). Safe for running AIs: a channel auto-recreates the moment any thread injects to it again.">clear</button> <span class="ver-pill" id="verPill" tabindex="0" data-tooltip="Loading version…">v…</span> <span class="ws-led" id="wsLed" data-tooltip="WebSocket: grey = disconnected, green = live. While green the server knows you're watching and won't force-open a duplicate tab on new shots."></span> </div>@@ -1638,6 +1638,15 @@ body.compact .card-popups .ph { padding-top: 4px; } document.body.classList.toggle('compact', APREFS.compact!==false); return; }+ if (msg.type==='cleared-all') {+ // Someone wiped everything. If it was THIS page's clear button, it+ // already wiped locally and showed the detailed shots/threads/size+ // toast - don't stomp it. Other pages reset their tabs here.+ if (window._selfCleared) { window._selfCleared=false; return; }+ if (window._wipeLocalAll) window._wipeLocalAll();+ showToast('all shotlog threads were cleared', 5000);+ return;+ } if (msg.type==='show' && msg.channel) { // Consolidation: an AI asked to "open" a channel while this window is // already up - switch OUR tab instead of letting a second surface@@ -1689,21 +1698,30 @@ body.compact .card-popups .ph { padding-top: 4px; } (function initClear(){ const btn=$('clearBtn'); let armed=false, t=null; const reset=()=>{ armed=false; btn.classList.remove('arm'); btn.textContent='clear'; clearTimeout(t); };+ const fmtBytes=b=> b>=1024*1024*1024 ? (b/(1024*1024*1024)).toFixed(2)+' GB'+ : b>=1024*1024 ? (b/(1024*1024)).toFixed(1)+' MB'+ : b>0 ? Math.max(1,Math.round(b/1024))+' KB'+ : '0 KB';+ // Drop every local tab + entry cache and land on an empty read-captures.+ // Used by the button AND by the broker's cleared-all broadcast (so every+ // open page resets, not just the one that clicked).+ window._wipeLocalAll=function(){+ chans.clear(); ensureChan('read-captures');+ activate('read-captures');+ }; btn.addEventListener('click', async ()=>{- if(!armed){ armed=true; btn.classList.add('arm'); btn.textContent='sure? clears all';+ if(!armed){ armed=true; btn.classList.add('arm'); btn.textContent='sure? clears ALL threads'; t=setTimeout(reset,3000); return; } reset();- entries=[]; renderAll(); try{- const r=await fetch('api/clear-all?channel='+encodeURIComponent(channel),{method:'DELETE'}).then(x=>x.json());- const b=r.bytes_freed||0;- // Human-readable: GB once it's a gigabyte-scale cleanup, else MB/KB.- const size = b>=1024*1024*1024 ? (b/(1024*1024*1024)).toFixed(2)+' GB'- : b>=1024*1024 ? (b/(1024*1024)).toFixed(1)+' MB'- : b>0 ? Math.max(1,Math.round(b/1024))+' KB'- : '0 KB';- showToast('cleared '+(r.cleared??0)+' shot'+((r.cleared??0)===1?'':'s')+' - freed '+size+' of disk', 7000);- }catch{ showToast('cleared'); }+ // EVERYTHING: all channels, all shots, all thread tabs. Channels+ // auto-recreate on the next inject, so this never breaks a live AI.+ window._selfCleared=true; // we show the detailed toast; skip the broadcast one+ const r=await fetch('api/clear-everything',{method:'DELETE'}).then(x=>x.json());+ window._wipeLocalAll();+ const n=r.shots_cleared??0, ch=r.channels_deleted??0;+ showToast('cleared '+n+' shot'+(n===1?'':'s')+' across '+ch+' thread'+(ch===1?'':'s')+' - freed '+fmtBytes(r.bytes_freed||0)+' of disk', 8000);+ }catch{ showToast('clear failed - broker unreachable?'); } }); })();
src/server.rs+15@@ -191,6 +191,7 @@ pub fn create_router(state: Arc<AppState>) -> Router { .route("/api/reveal", post(reveal_in_vscode)) .route("/api/clear-log", delete(clear_log)) .route("/api/clear-all", delete(clear_all))+ .route("/api/clear-everything", delete(clear_everything)) .route("/api/delete", delete(delete_entry)) .route("/ws/{channel}", get(ws_handler)) .route("/shots/{channel}/{filename}", get(serve_image))@@ -1450,6 +1451,20 @@ async fn clear_all( Json(serde_json::json!({ "ok": true, "cleared": count, "files_deleted": true, "bytes_freed": bytes_freed })) } +/// The viewer's clear button: wipe EVERY channel (shots + files + the thread+/// tabs themselves). Channels auto-recreate on the next inject, so this is+/// always safe for in-flight AI threads.+async fn clear_everything(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {+ let (channels_deleted, shots_cleared, bytes_freed) = state.store.clear_everything().await;+ println!("{} CLEAR-EVERYTHING: {channels_deleted} thread tab(s), {shots_cleared} shot(s), {bytes_freed} bytes freed.", ts());+ // Every open page resets its tab strip (not just the one that clicked).+ let _ = state.store.ctrl_tx.send(serde_json::json!({"type": "cleared-all"}).to_string());+ Json(serde_json::json!({+ "ok": true, "channels_deleted": channels_deleted,+ "shots_cleared": shots_cleared, "bytes_freed": bytes_freed,+ }))+}+ #[derive(Deserialize)] struct DeleteQuery { channel: Option<String>,
src/store.rs+29@@ -607,6 +607,35 @@ impl Store { (count, bytes_freed) } + /// Nuke EVERYTHING: every channel's shots, files, and the channel itself,+ /// so the viewer's thread tabs all disappear. Safe by design: channels are+ /// created on demand, so an AI injecting to a wiped channel name 5 seconds+ /// later just auto-recreates it (fresh dir, ids restart at 1).+ /// Returns (channels_deleted, shots_cleared, bytes_freed).+ pub async fn clear_everything(&self) -> (usize, usize, u64) {+ let mut channels = self.channels.write().await;+ let names: Vec<String> = channels.keys().cloned().collect();+ let mut shots = 0usize;+ let mut bytes: u64 = 0;+ for name in &names {+ if let Some(ch) = channels.get(name) { shots += ch.entries.len(); }+ let dir = self.base_dir.join(name);+ if dir.is_dir() {+ // Sum EVERY file (safe PNGs, __full pairs, __popup children,+ // .json sidecars) so the freed-space report is byte-exact,+ // then remove the whole directory.+ if let Ok(rd) = fs::read_dir(&dir) {+ for e in rd.flatten() {+ if let Ok(md) = e.metadata() { if md.is_file() { bytes += md.len(); } }+ }+ }+ let _ = fs::remove_dir_all(&dir);+ }+ channels.remove(name);+ }+ (names.len(), shots, bytes)+ }+ /// List all channel names. pub async fn channel_names(&self) -> Vec<String> { let channels = self.channels.read().await;