← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.16.1"+version = "0.17.0" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.16.1"+version = "0.17.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1
@@ -1 +1 @@-0.16.1+0.17.0
bin/adom-shotlog
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.16.1",+  "version": "0.17.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.16.1",+  "version": "0.17.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+13
@@ -394,6 +394,14 @@ header {       </div>       <button class="toggle" id="prefHook" role="switch" aria-checked="true" data-tooltip="Toggle the read-hook on or off. Takes effect on the next image an AI reads."></button>     </div>+    <div class="pref-row">+      <div class="pref-info">+        <div class="pref-name">Update alerts (orange flashes)</div>+        <div class="pref-desc">When new shots arrive: the webview tab highlights orange and the pup window flashes its taskbar. Turn off for calmer eyeballs - shotlog still brings the tab forward, just without the flashing.</div>+        <div class="ai-chip"><span class="lbl">or say to your AI</span>turn off the shotlog orange flashes</div>+      </div>+      <button class="toggle" id="prefAlerts" role="switch" aria-checked="true" data-tooltip="Toggle the orange update cues (webview tab highlight + pup taskbar flash) when new shots arrive."></button>+    </div>     <div class="pref-row">       <div class="pref-info">         <div class="pref-name">Default canvas</div>@@ -879,6 +887,7 @@ header {     document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&dlg.classList.contains('open')) close(); });     function renderPrefs(p){       hook.setAttribute('aria-checked', String(!!p.read_hook));+      $('prefAlerts').setAttribute('aria-checked', String(p.update_alerts!==false));       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;@@ -894,6 +903,10 @@ header {       const now=hook.getAttribute('aria-checked')==='true';       patch({read_hook: !now}, !now?'read-hook ON: images AIs read auto-capture again.':'read-hook OFF: nothing auto-captures until you turn it back on.');     });+    $('prefAlerts').addEventListener('click',()=>{+      const now=$('prefAlerts').getAttribute('aria-checked')==='true';+      patch({update_alerts: !now}, !now?'orange update flashes ON.':'orange update flashes OFF: tabs still come forward, no flashing.');+    });     seg.addEventListener('click',e=>{       const b=e.target.closest('button[data-v]'); if(!b) return;       patch({default_canvas: b.dataset.v}, 'channels now default to '+b.dataset.v+'.');
src/main.rs+3−2
@@ -640,8 +640,9 @@ fn run_inject(                         for a in acts {                             match a.get("action").and_then(|x| x.as_str()) {                                 Some("webview_surface") => ok(&format!(-                                    "Broker: pulling the '{}' webview tab to the front with an ORANGE update alert — surfacing is handled, do NOT open/foreground anything yourself.",-                                    a.get("tab").and_then(|t| t.as_str()).unwrap_or("Shots"))),+                                    "Broker: '{}' webview: {} — surfacing is handled, do NOT open/foreground anything yourself.",+                                    a.get("tab").and_then(|t| t.as_str()).unwrap_or("Shots"),+                                    a.get("cue").and_then(|c| c.as_str()).unwrap_or("tab pulled to front"))),                                 Some("pup_flash") => ok(&format!(                                     "Broker: flashing the pup taskbar orange (session{}) — the human's gentle cue is handled.",                                     a.get("sessions").and_then(|s| s.as_array()).map(|v| format!(" {}", v.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(", "))).unwrap_or_default())),
src/server.rs+25−14
@@ -283,6 +283,7 @@ struct PrefsPatch {     read_hook: Option<bool>,     max_shots: Option<usize>,     retention_days: Option<u64>,+    update_alerts: Option<bool>, }  /// Patch global preferences from the viewer's Preferences dialog.@@ -306,6 +307,9 @@ async fn set_prefs(         }         p.max_shots = n;     }+    if let Some(a) = patch.update_alerts {+        p.update_alerts = a;+    }     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)"}));@@ -544,7 +548,7 @@ fn debounce_ok(key: &str, secs: u64) -> bool { ///    hinted to flash the pup taskbar. ///  - **nobody** watching → force-open a slim ~20% webview, unless ///    `SHOTLOG_NO_FORCE_OPEN` is set.-fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], pref: &str) {+fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], pref: &str, alerts: bool) {     // Pup viewers get an automatic ORANGE TASKBAR FLASH (FlashWindowEx) on their     // hosting window — a gentle "update" cue, never a foreground steal. Works when     // the viewer URL carried ?session=<pup sessionId> (reported in the WS hello).@@ -553,13 +557,15 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo],         .filter(|i| i.surface == "pup")         .filter_map(|i| i.session.clone())         .collect();-    for s in pup_sessions {-        flash_pup_session(&s);+    if alerts {+        for s in pup_sessions {+            flash_pup_session(&s);+        }     }      let has_webview = infos.iter().any(|i| i.surface == "webview");     if has_webview {-        bring_webview_forward(channel, port);+        bring_webview_forward(channel, port, alerts);         return;     }     if infos.is_empty() && std::env::var("SHOTLOG_NO_FORCE_OPEN").is_err() {@@ -636,7 +642,7 @@ fn flash_pup_session(session: &str) { /// shotlog tabs are never cross-touched), then `move-tab` to its own pane: /// Hydrogen auto-activates it (surfaces to top) and `--alert` paints the ORANGE /// update highlight on the tab. Falls back to create-or-refresh if not found.-fn bring_webview_forward(channel: &str, port: u16) {+fn bring_webview_forward(channel: &str, port: u16, alerts: bool) {     if !debounce_ok(&format!("fwd:{channel}"), 2) {         return;     }@@ -655,11 +661,13 @@ fn bring_webview_forward(channel: &str, port: u16) {                 && String::from_utf8_lossy(&o.stdout).contains("204"))             .unwrap_or(false);         if activated {-            // Orange update indicator on the (now-front) tab.-            let _ = std::process::Command::new("adom-cli")-                .args(["hydrogen", "alert", "set", "--name", &tab, "-d", "6000"])-                .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())-                .status();+            // Orange update indicator on the (now-front) tab - user-toggleable.+            if alerts {+                let _ = std::process::Command::new("adom-cli")+                    .args(["hydrogen", "alert", "set", "--name", &tab, "-d", "6000"])+                    .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())+                    .status();+            }             return;         }         // Tab not found → create/refresh it instead.@@ -819,22 +827,25 @@ async fn post_shots(         s.into_iter().collect()     };     let force_open_on = std::env::var("SHOTLOG_NO_FORCE_OPEN").is_err();+    let app_prefs = state.store.app_prefs().await;+    let alerts_on = app_prefs.update_alerts;     let canvas_pref = match state.store.canvas_pref(&req.channel).await {         Some(c) => c,-        None => state.store.app_prefs().await.default_canvas,+        None => app_prefs.default_canvas,     };     let mut actions: Vec<serde_json::Value> = Vec::new();-    if !pup_sessions.is_empty() {+    if !pup_sessions.is_empty() && alerts_on {         actions.push(serde_json::json!({"action":"pup_flash","sessions":pup_sessions,"cue":"orange taskbar flash"}));     }     if has_webview {-        actions.push(serde_json::json!({"action":"webview_surface","tab":format!("Shots: {}", req.channel),"cue":"tab pulled to front + orange alert"}));+        actions.push(serde_json::json!({"action":"webview_surface","tab":format!("Shots: {}", req.channel),+            "cue": if alerts_on {"tab pulled to front + orange alert"} else {"tab pulled to front (update alerts off)"}}));     } else if infos.is_empty() && force_open_on {         actions.push(serde_json::json!({"action":"force_open","canvas": canvas_pref,             "layout": if canvas_pref=="pup" {"tab in the shared pup window"} else {"slim 20% right-side webview"}}));     }     let surfacing = Some(serde_json::json!({"actions": actions}));-    notify_surface(&req.channel, state.port, &infos, &canvas_pref);+    notify_surface(&req.channel, state.port, &infos, &canvas_pref, alerts_on);      // Progressive mini-skill hints — taught at the moment they're relevant, then     // quiet (hint_gate caps how many times each lesson repeats).
src/store.rs+6−1
@@ -97,6 +97,10 @@ pub struct AppPrefs {     /// Env SHOTLOG_RETENTION_DAYS overrides when set.     #[serde(default = "default_retention_days")]     pub retention_days: u64,+    /// Orange update cues when NEW SHOTS arrive: the webview tab highlight and+    /// the pup taskbar flash. Off = still pulls the tab forward, just no flash.+    #[serde(default = "default_true")]+    pub update_alerts: bool, } fn default_canvas() -> String { "webview".into() } fn default_true() -> bool { true }@@ -104,7 +108,8 @@ 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,-        max_shots: default_max_shots(), retention_days: default_retention_days() } }+        max_shots: default_max_shots(), retention_days: default_retention_days(),+        update_alerts: true } } }  /// The flag file the read-hook script checks; present = hook disabled.