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.25.2
10 files changed
+92−20
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.25.1"+version = "0.25.2" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.25.1"+version = "0.25.2" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1@@ -1 +1 @@-0.25.1+0.25.2
bin/adom-shotlog⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+10@@ -381,3 +381,13 @@ Hydrogen·self). Kept INTERNAL source keys (ad-window/abe/hd) so existing shots still filter; only labels changed. The read-hook path match also updated: screenshots/adom-desktop/ -> adom-bridge/. "Preferences" dialog -> "Settings" (ah calls it that). Env var SHOTLOG_AD_TARGET kept (internal, not user-facing).++## Settings steppers + on/off toggles (0.25.2, John)+Number inputs got −/+ steppers (.num-step buttons, data-target + data-dir;+delegated click adjusts by the input's step, clamps min/max, dispatches a+change event to reuse wireNum's commit). Steps: width 5, keep 25, prune 5.+Keep-per-channel and prune-idle got on/off switches: OFF stores 0, ON restores+the remembered last value (lastKeep/lastPrune in JS). max_shots=0 now means+UNLIMITED - the janitor guards `cap > 0` at BOTH enforcement points (recovery+trim + add_entry trim) and the prefs handler accepts 0 or 10-5000. retention=0+already meant "never". The .pref-control.off class greys + disables the field.
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.25.1",+ "version": "0.25.2", "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.25.1",+ "version": "0.25.2", "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+70−11@@ -448,13 +448,26 @@ header { the human guessing what they're changing. */ .num-wrap { display: inline-flex; align-items: center; gap: 6px; height: 30px; background: var(--overlay); border: 1px solid var(--border); border-radius: 7px;- padding: 0 10px; flex-shrink: 0; }+ padding: 0 4px 0 10px; flex-shrink: 0; } .num-wrap:focus-within { border-color: rgba(0,184,176,.6); } .num-wrap .num-input { -webkit-appearance: none; appearance: textfield; width: 46px; background: transparent; border: none; outline: none; color: var(--text); font: 600 13px 'JetBrains Mono', monospace; text-align: right; } .num-wrap .num-input::-webkit-inner-spin-button, .num-wrap .num-input::-webkit-outer-spin-button { -webkit-appearance: none; } .num-wrap .unit { color: var(--text3); font: 500 11px 'Satoshi', sans-serif; }+/* Up/down stepper buttons on number inputs */+.num-wrap .num-step { -webkit-appearance: none; appearance: none; flex-shrink: 0;+ width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center;+ background: var(--elevated); color: var(--text2); border: 1px solid var(--border);+ border-radius: 5px; cursor: pointer; font: 600 15px/1 'Satoshi', sans-serif; padding: 0;+ transition: background .12s, color .12s, border-color .12s; }+.num-wrap .num-step:first-child { margin-right: 2px; }+.num-wrap .num-step:hover { background: rgba(0,184,176,.16); color: var(--accent-bright); border-color: rgba(0,184,176,.5); }+.num-wrap .num-step:active { transform: translateY(.5px); }+/* A setting that pairs an on/off switch with its number field */+.pref-control { display: inline-flex; align-items: center; gap: 10px; flex-shrink: 0; }+/* When the switch is OFF, grey + disable the number field */+.pref-control.off .num-wrap { opacity: .4; pointer-events: none; } .ai-chip .lbl { color: var(--text3); font-family: 'Satoshi', sans-serif; font-size: 9px; letter-spacing: .04em; text-transform: uppercase; margin-right: 6px; } @@ -582,28 +595,40 @@ header { <div class="ai-chip"><span class="lbl">or say to your AI</span>make the shotlog webview panel 25% wide</div> </div> <span class="num-wrap" data-tooltip="Percent of the workspace width for the skinny shotlog panel (10-50). Seeds a new panel; only re-applied on every open when auto-size is ON.">+ <button class="num-step" data-target="prefWidth" data-dir="-1" aria-label="decrease" tabindex="-1">−</button> <input class="num-input" id="prefWidth" type="number" min="10" max="50" step="5"><span class="unit">%</span>+ <button class="num-step" data-target="prefWidth" data-dir="1" aria-label="increase" tabindex="-1">+</button> </span> </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="pref-desc">The janitor keeps only the newest N shots per channel on disk; older files are deleted as new ones arrive. Turn the switch OFF to keep every shot (unlimited on disk).</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>- <span class="num-wrap" data-tooltip="Newest shots kept per channel (10-5000). Applies as new shots arrive.">- <input class="num-input" id="prefMaxShots" type="number" min="10" max="5000" step="10"><span class="unit">shots</span>- </span>+ <div class="pref-control">+ <button class="toggle" id="prefKeepToggle" role="switch" aria-checked="true" data-tooltip="On: cap the shots kept per channel. Off: keep everything (unlimited)."></button>+ <span class="num-wrap" data-tooltip="Newest shots kept per channel (10-5000). Turn the switch off to keep unlimited.">+ <button class="num-step" data-target="prefMaxShots" data-dir="-1" aria-label="decrease" tabindex="-1">−</button>+ <input class="num-input" id="prefMaxShots" type="number" min="10" max="5000" step="25"><span class="unit">shots</span>+ <button class="num-step" data-target="prefMaxShots" data-dir="1" aria-label="increase" tabindex="-1">+</button>+ </span>+ </div> </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="pref-desc">Channels with no new shots for this many days are deleted entirely (checked at startup and every 6 hours). Turn the switch OFF to never prune.</div> <div class="ai-chip"><span class="lbl">or say to your AI</span>never auto-delete idle shotlog channels</div> </div>- <span class="num-wrap" data-tooltip="Days of inactivity before a whole channel is pruned. 0 = never.">- <input class="num-input" id="prefRetention" type="number" min="0" max="3650" step="1"><span class="unit">days</span>- </span>+ <div class="pref-control">+ <button class="toggle" id="prefPruneToggle" role="switch" aria-checked="true" data-tooltip="On: prune idle channels after N days. Off: never prune."></button>+ <span class="num-wrap" data-tooltip="Days of inactivity before a whole channel is pruned. Turn the switch off to never prune.">+ <button class="num-step" data-target="prefRetention" data-dir="-1" aria-label="decrease" tabindex="-1">−</button>+ <input class="num-input" id="prefRetention" type="number" min="1" max="3650" step="5"><span class="unit">days</span>+ <button class="num-step" data-target="prefRetention" data-dir="1" aria-label="increase" tabindex="-1">+</button>+ </span>+ </div> </div> </div> </div>@@ -1403,6 +1428,9 @@ header { $('prefsClose').addEventListener('click',close); dlg.addEventListener('click',e=>{ if(e.target===dlg) close(); }); document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&dlg.classList.contains('open')) close(); });+ // Remember the last positive value so toggling a setting OFF (which stores 0)+ // then back ON restores what the human had, not a bare default.+ let lastKeep=200, lastPrune=30; function renderPrefs(p){ hook.setAttribute('aria-checked', String(!!p.read_hook)); $('prefAlerts').setAttribute('aria-checked', String(p.update_alerts!==false));@@ -1411,8 +1439,16 @@ header { APREFS=p; seg.querySelectorAll('button').forEach(b=>b.classList.toggle('on', b.dataset.v===p.default_canvas)); $('prefWidth').value = p.webview_width ?? 20;- $('prefMaxShots').value = p.max_shots ?? 200;- $('prefRetention').value = p.retention_days ?? 30;+ // Keep per channel: max_shots 0 = OFF (unlimited).+ const mk = p.max_shots ?? 200; if(mk>0) lastKeep=mk;+ $('prefKeepToggle').setAttribute('aria-checked', String(mk>0));+ $('prefMaxShots').closest('.pref-control').classList.toggle('off', mk===0);+ $('prefMaxShots').value = mk>0 ? mk : lastKeep;+ // Prune idle: retention_days 0 = OFF (never).+ const rd = p.retention_days ?? 30; if(rd>0) lastPrune=rd;+ $('prefPruneToggle').setAttribute('aria-checked', String(rd>0));+ $('prefRetention').closest('.pref-control').classList.toggle('off', rd===0);+ $('prefRetention').value = rd>0 ? rd : lastPrune; } async function patch(body, okMsg){ try{@@ -1453,6 +1489,29 @@ header { wireNum('prefWidth','webview_width', v=>'webview panel width is now '+v+'% for new opens.'); 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.');+ // Up/down steppers: adjust the target input by its step, clamp, commit.+ dlg.addEventListener('click', e=>{+ const b=e.target.closest('.num-step'); if(!b) return;+ const el=$(b.dataset.target); if(!el) return;+ if(el.closest('.pref-control')?.classList.contains('off')) return; // disabled while switch is off+ const step=parseInt(el.step,10)||1, dir=parseInt(b.dataset.dir,10)||0;+ const min=parseInt(el.min,10), max=parseInt(el.max,10);+ let v=(parseInt(el.value,10)||min)+dir*step;+ v=Math.max(min, Math.min(max, v));+ if(String(v)!==el.value){ el.value=v; el.dispatchEvent(new Event('change')); }+ });+ // Keep-per-channel switch: OFF => max_shots 0 (unlimited); ON => restore last.+ $('prefKeepToggle').addEventListener('click',()=>{+ const on=$('prefKeepToggle').getAttribute('aria-checked')==='true';+ if(on) patch({max_shots:0}, 'keep-per-channel OFF: keeping every shot (unlimited on disk).');+ else patch({max_shots:lastKeep}, 'keep-per-channel ON: newest '+lastKeep+' shots per channel.');+ });+ // Prune-idle switch: OFF => retention_days 0 (never); ON => restore last.+ $('prefPruneToggle').addEventListener('click',()=>{+ const on=$('prefPruneToggle').getAttribute('aria-checked')==='true';+ if(on) patch({retention_days:0}, 'prune OFF: idle channels are never deleted.');+ else patch({retention_days:lastPrune}, 'prune ON: idle channels prune after '+lastPrune+' days.');+ }); $('prefsBtn').addEventListener('click', async ()=>{ try{ renderPrefs(await fetch('api/prefs').then(r=>r.json())); }catch{} dlg.classList.add('open');
src/server.rs+3−2@@ -551,8 +551,9 @@ async fn set_prefs( 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"}));+ // 0 = UNLIMITED (keep-per-channel toggle off); otherwise 10-5000.+ if n != 0 && !(10..=5000).contains(&n) {+ return Json(serde_json::json!({"ok": false, "error": "max_shots must be 0 (unlimited) or 10-5000"})); } p.max_shots = n; }
src/store.rs+4−2@@ -322,7 +322,8 @@ impl Store { // Keep only the newest max_shots() - and DELETE the older shots' // files, which previously lingered on disk forever. let cap = std::env::var("SHOTLOG_MAX_SHOTS").ok().and_then(|v| v.parse().ok()).unwrap_or(200);- if entries.len() > cap {+ // cap == 0 means UNLIMITED (the "keep per channel" toggle turned off) - never trim.+ if cap > 0 && entries.len() > cap { let trimmed: Vec<Entry> = entries.drain(..entries.len() - cap).collect(); for e in &trimmed { delete_entry_files(e); } }@@ -437,7 +438,8 @@ impl Store { .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 {+ // cap == 0 means UNLIMITED (keep-per-channel toggle off) - never trim.+ while cap > 0 && ch.entries.len() > cap { let old = ch.entries.remove(0); delete_entry_files(&old); }