← Commit history
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! {