← Commit history
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).&#10;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.&#10;While green the server knows you're watching and&#10;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;