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.0
9 files changed
+84−9
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.15.1"+version = "0.16.0" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.15.1"+version = "0.16.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1@@ -1 +1 @@-0.15.1+0.16.0
bin/adom-shotlog⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.15.1",+ "version": "0.16.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 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.15.1",+ "version": "0.16.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 and on which surface, and ingests Adom Desktop child-window arrays via --meta.",
src/html/index.html+34@@ -308,6 +308,11 @@ header { background: rgba(0,184,176,.08); border: 1px solid rgba(0,184,176,.25); font-family: 'JetBrains Mono', monospace; font-size: 10.5px; line-height: 1.4; color: var(--accent-bright); }+.num-input { -webkit-appearance: none; appearance: none; width: 88px; height: 30px;+ background: var(--overlay); border: 1px solid var(--border); border-radius: 7px;+ color: var(--text); font: 600 13px 'JetBrains Mono', monospace; text-align: center;+ outline: none; }+.num-input:focus { border-color: rgba(0,184,176,.6); } .ai-chip .lbl { color: var(--text3); font-family: 'Satoshi', sans-serif; font-size: 9px; letter-spacing: .04em; text-transform: uppercase; margin-right: 6px; } @@ -400,6 +405,24 @@ header { <button data-v="pup">Pup</button> </div> </div>+ <div class="pref-row">+ <div class="pref-info">+ <div class="pref-name">Keep per channel</div>+ <div class="pref-desc">The janitor keeps only the newest N shots per channel on disk; older files are deleted as new ones arrive. Auto-capture channels like read-captures never grow unbounded.</div>+ <div class="ai-chip"><span class="lbl">or say to your AI</span>keep only the newest 100 shots per shotlog channel</div>+ </div>+ <input class="num-input" id="prefMaxShots" type="number" min="10" max="5000" step="10"+ data-tooltip="Newest shots kept per channel (10-5000). Applies as new shots arrive.">+ </div>+ <div class="pref-row">+ <div class="pref-info">+ <div class="pref-name">Prune idle channels after (days)</div>+ <div class="pref-desc">Channels with no new shots for this many days are deleted entirely (checked at startup and every 6 hours). 0 means never prune.</div>+ <div class="ai-chip"><span class="lbl">or say to your AI</span>never auto-delete idle shotlog channels</div>+ </div>+ <input class="num-input" id="prefRetention" type="number" min="0" max="3650" step="1"+ data-tooltip="Days of inactivity before a whole channel is pruned. 0 = never.">+ </div> </div> </div> <div class="toast" id="toast"></div>@@ -857,6 +880,8 @@ header { function renderPrefs(p){ hook.setAttribute('aria-checked', String(!!p.read_hook)); seg.querySelectorAll('button').forEach(b=>b.classList.toggle('on', b.dataset.v===p.default_canvas));+ $('prefMaxShots').value = p.max_shots ?? 200;+ $('prefRetention').value = p.retention_days ?? 30; } async function patch(body, okMsg){ try{@@ -873,6 +898,15 @@ header { const b=e.target.closest('button[data-v]'); if(!b) return; patch({default_canvas: b.dataset.v}, 'channels now default to '+b.dataset.v+'.'); });+ function wireNum(id, key, msg){+ const el=$(id);+ const commit=()=>{ const v=parseInt(el.value,10); if(Number.isNaN(v)) return;+ patch({[key]: v}, msg(v)); };+ el.addEventListener('change', commit);+ el.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); commit(); el.blur(); } });+ }+ wireNum('prefMaxShots','max_shots', v=>'keeping the newest '+v+' shots per channel.');+ wireNum('prefRetention','retention_days', v=> v===0?'idle channels are never pruned.':'idle channels prune after '+v+' days.'); $('prefsBtn').addEventListener('click', async ()=>{ try{ renderPrefs(await fetch('api/prefs').then(r=>r.json())); }catch{} dlg.classList.add('open');
src/server.rs+14@@ -281,6 +281,8 @@ async fn get_prefs(State(state): State<Arc<AppState>>) -> Json<crate::store::App struct PrefsPatch { default_canvas: Option<String>, read_hook: Option<bool>,+ max_shots: Option<usize>,+ retention_days: Option<u64>, } /// Patch global preferences from the viewer's Preferences dialog.@@ -298,6 +300,18 @@ async fn set_prefs( if let Some(h) = patch.read_hook { p.read_hook = h; }+ if let Some(n) = patch.max_shots {+ if !(10..=5000).contains(&n) {+ return Json(serde_json::json!({"ok": false, "error": "max_shots must be 10-5000"}));+ }+ p.max_shots = n;+ }+ if let Some(d) = patch.retention_days {+ if d > 3650 {+ return Json(serde_json::json!({"ok": false, "error": "retention_days must be 0-3650 (0 = never)"}));+ }+ p.retention_days = d;+ } state.store.set_app_prefs(p.clone()).await; Json(serde_json::json!({"ok": true, "prefs": p})) }
src/store.rs+31−4@@ -46,12 +46,14 @@ struct Channel { /// In-memory AND on-disk cap of shots per channel (SHOTLOG_MAX_SHOTS, default 200). /// When a channel exceeds it, the oldest shot is dropped AND its files deleted - /// without this the disk grew unbounded (3.4 GB before retention existed).+#[allow(dead_code)] fn max_shots() -> usize { std::env::var("SHOTLOG_MAX_SHOTS").ok().and_then(|v| v.parse().ok()).unwrap_or(200) } /// Days of inactivity before a whole channel is pruned (SHOTLOG_RETENTION_DAYS, /// default 30; 0 disables the sweep).+#[allow(dead_code)] fn retention_days() -> u64 { std::env::var("SHOTLOG_RETENTION_DAYS").ok().and_then(|v| v.parse().ok()).unwrap_or(30) }@@ -87,11 +89,22 @@ pub struct AppPrefs { /// becomes a no-op without touching hook registration. #[serde(default = "default_true")] pub read_hook: bool,+ /// Janitor: newest N shots kept per channel on disk (oldest deleted as new+ /// ones arrive). Env SHOTLOG_MAX_SHOTS overrides when set.+ #[serde(default = "default_max_shots")]+ pub max_shots: usize,+ /// Janitor: prune whole channels idle this many days (0 = never).+ /// Env SHOTLOG_RETENTION_DAYS overrides when set.+ #[serde(default = "default_retention_days")]+ pub retention_days: u64, } fn default_canvas() -> String { "webview".into() } fn default_true() -> bool { true }+fn default_max_shots() -> usize { 200 }+fn default_retention_days() -> u64 { 30 } impl Default for AppPrefs {- fn default() -> Self { Self { default_canvas: default_canvas(), read_hook: true } }+ fn default() -> Self { Self { default_canvas: default_canvas(), read_hook: true,+ max_shots: default_max_shots(), retention_days: default_retention_days() } } } /// The flag file the read-hook script checks; present = hook disabled.@@ -250,7 +263,7 @@ impl Store { // Keep only the newest max_shots() - and DELETE the older shots' // files, which previously lingered on disk forever.- let cap = max_shots();+ let cap = std::env::var("SHOTLOG_MAX_SHOTS").ok().and_then(|v| v.parse().ok()).unwrap_or(200); if entries.len() > cap { let trimmed: Vec<Entry> = entries.drain(..entries.len() - cap).collect(); for e in &trimmed { delete_entry_files(e); }@@ -342,7 +355,11 @@ impl Store { ch.next_id += 1; ch.entries.push(entry.clone());- while ch.entries.len() > max_shots() {+ let cap = std::env::var("SHOTLOG_MAX_SHOTS").ok().and_then(|v| v.parse().ok())+ .unwrap_or_else(|| { // prefs read is async; channels lock is held - use try_read+ self.app_prefs.try_read().map(|p| p.max_shots).unwrap_or(200)+ });+ while ch.entries.len() > cap { let old = ch.entries.remove(0); delete_entry_files(&old); }@@ -527,6 +544,16 @@ impl Store { self.app_prefs.read().await.clone() } + /// Janitor knobs: env vars override, otherwise the UI-editable prefs.+ pub async fn effective_max_shots(&self) -> usize {+ std::env::var("SHOTLOG_MAX_SHOTS").ok().and_then(|v| v.parse().ok())+ .unwrap_or(self.app_prefs.read().await.max_shots)+ }+ pub async fn effective_retention_days(&self) -> u64 {+ std::env::var("SHOTLOG_RETENTION_DAYS").ok().and_then(|v| v.parse().ok())+ .unwrap_or(self.app_prefs.read().await.retention_days)+ }+ /// Update prefs, persist, and sync the read-hook flag file. pub async fn set_app_prefs(&self, p: AppPrefs) { let flag = read_hook_off_flag();@@ -545,7 +572,7 @@ impl Store { /// delete the channel directory and drop it from the in-memory store. /// Returns the pruned channel names. Runs at startup and every 6 hours. pub async fn retention_sweep(&self) -> Vec<String> {- let days = retention_days();+ let days = self.effective_retention_days().await; if days == 0 { return Vec::new(); } let cutoff = std::time::SystemTime::now() - std::time::Duration::from_secs(days * 86_400); let mut pruned = Vec::new();