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.21.0
9 files changed
+45−15
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.20.1"+version = "0.21.0" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.20.1"+version = "0.21.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1@@ -1 +1 @@-0.20.1+0.21.0
bin/adom-shotlog⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+3@@ -99,6 +99,9 @@ The viewer is a real UI, held to `~/.claude/skills/adom/ui-implementation-refere left/top min (`Math.max(4,x)`), so a 300px tooltip spilled off the right in the skinny ~360px panel. Fix = `Math.min(Math.max(M,x), vw-tw-M)` on BOTH axes + tooltip `max-width: min(300px, calc(100vw - 16px))`. Never a CSS `::after`.+- **Number inputs ALWAYS show their unit** (%, shots, days) as an inline suffix+ inside the box (`.num-wrap` + `.unit`). A bare number box leaves the human+ guessing what they're changing. User called it. - **Buttons are styled pills, never raw `<button>`.** The `.hdr-btn` system (below) is the only header-button style. A raw `<button>` = the browser's grey default = instant "hideous". Icon buttons use inline SVG (Lucide-style stroke), never unicode
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.20.1",+ "version": "0.21.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.20.1",+ "version": "0.21.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+32−10@@ -309,11 +309,17 @@ 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;+/* Number input with an ALWAYS-VISIBLE unit suffix - a bare number box leaves+ 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;- color: var(--text); font: 600 13px 'JetBrains Mono', monospace; text-align: center;- outline: none; }-.num-input:focus { border-color: rgba(0,184,176,.6); }+ padding: 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; } .ai-chip .lbl { color: var(--text3); font-family: 'Satoshi', sans-serif; font-size: 9px; letter-spacing: .04em; text-transform: uppercase; margin-right: 6px; } @@ -420,8 +426,9 @@ header { <div class="pref-desc">How wide the slim webview panel is when shotlog opens it, as a percent of your workspace (10-50).</div> <div class="ai-chip"><span class="lbl">or say to your AI</span>make the shotlog webview panel 25% wide</div> </div>- <input class="num-input" id="prefWidth" type="number" min="10" max="50" step="5"- data-tooltip="Percent of the workspace width for the skinny shotlog panel (10-50). Applies to newly opened/auto-opened panels.">+ <span class="num-wrap" data-tooltip="Percent of the workspace width for the skinny shotlog panel (10-50). Applies to newly opened/auto-opened panels.">+ <input class="num-input" id="prefWidth" type="number" min="10" max="50" step="5"><span class="unit">%</span>+ </span> </div> <div class="pref-row"> <div class="pref-info">@@ -429,8 +436,9 @@ header { <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.">+ <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> <div class="pref-row"> <div class="pref-info">@@ -438,8 +446,9 @@ header { <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.">+ <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> </div> </div>@@ -454,6 +463,7 @@ header { <script> (function() { const $ = id => document.getElementById(id);+ const APP_VERSION='__SHOTLOG_VERSION__'; // stamped by the server at serve time const feed = $('feed'), toast = $('toast'), wsLed = $('wsLed'); const modal = $('modal'), mImg = $('mImg'), mDesc = $('mDesc'), mMeta = $('mMeta'); const mPopups = $('mPopups'), mNerd = $('mNerd');@@ -969,6 +979,18 @@ header { 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)}`);+ // Version handshake: the broker announces its version on connect; a page+ // from an older deploy reloads ITSELF (once) so no surface ever goes stale.+ ws.addEventListener('message', ev => {+ try { const m = JSON.parse(ev.data);+ if (m.type === 'welcome' && m.version && APP_VERSION.indexOf('__') !== 0 && m.version !== APP_VERSION) {+ if (!sessionStorage.getItem('slReloaded-' + m.version)) {+ sessionStorage.setItem('slReloaded-' + m.version, '1');+ location.reload();+ }+ }+ } catch {}+ }); ws.onopen = () => { wsLed.classList.add('live'); wsLed.setAttribute('data-tooltip','WebSocket live — the server knows you are watching.'); activeWs = ws; // Tell the server WHICH surface is watching + whether it's on-screen right now,
src/server.rs+5@@ -128,6 +128,7 @@ async fn serve_viewer() -> Response { // no-cache: the viewer UI must NEVER go stale in a long-lived webview/pup // tab - without this, browsers heuristically cached old HTML across // deploys and reloads showed a months-old UI.+ let html = html.replacen("__SHOTLOG_VERSION__", env!("CARGO_PKG_VERSION"), 1); let mut res = Html(html).into_response(); res.headers_mut().insert( header::CACHE_CONTROL,@@ -1169,6 +1170,10 @@ async fn ws_connection(mut socket: WebSocket, channel: String, state: Arc<AppSta .fetch_add(1, std::sync::atomic::Ordering::Relaxed); let mut conn_surface: String = "browser".into(); state.store.register_viewer(&channel, vid, "browser".into(), true, None).await;+ // Version handshake: stale pages reload themselves on mismatch.+ let _ = socket.send(Message::Text(+ serde_json::json!({"type": "welcome", "version": env!("CARGO_PKG_VERSION")}).to_string().into()+ )).await; loop { tokio::select! {