← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.23.13"+version = "0.24.0" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.23.13"+version = "0.24.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
SKILL.md+8
@@ -219,6 +219,14 @@ every gallery card. Make it specific. **Bad:** `"screenshot"`, `"test"`, `"after fix"` **Good:** `"PCB layout after routing power traces"`, `"Checkout form with autofill dropdown open"` +## Fill window (zoom a shot to a lightbox)++Click a shot to open the zoom modal, then click the image (or the ⛶ button) to+FILL THE WINDOW. This uses the standard Fullscreen API: in Hydrogen Desktop's+native webview it grows the panel to a 90%-of-window lightbox that escapes the+skinny panel; in a pup/browser it goes true fullscreen; on a phone it fills the+screen. Esc / ✕ / click-the-backdrop exits. One code path, right on every surface.+ ## The viewer  ```bash
VERSION+1−1
@@ -1 +1 @@-0.23.13+0.24.0
bin/adom-shotlog
⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+21
@@ -308,3 +308,24 @@ Two bugs that both stranded the human on webview when their default was pup:    a mismatch returns consolidated:false so the caller opens the right surface. ALSO: post_shots now logs a `CANVAS '<ch>' -> <canvas> (<why>; default=<d>)` line every time, so "why did it open there?" is answerable from the log, never a guess.++## Fill-window uses the STANDARD Fullscreen API (2026-07-23)+The image lightbox (#lightbox) goes fullscreen via element.requestFullscreen()+- NO Adom-specific call. HD's native WebView2 implements the standard API+specially: requestFullscreen from inside it grows the webview to 90% of the HD+window (centered lightbox), restores on exit. So the standard API is the only+hook needed. Rules that make it work on EVERY surface with one code path:+- The click IS the user gesture requestFullscreen requires; call it straight+  from the click handler (never after an await / setTimeout - that drops the+  gesture and the call rejects).+- Feature-detect (if el.requestFullscreen) and ALWAYS add the .open class first+  so the CSS fixed-overlay is the graceful fallback when the API is absent+  (older iOS Safari won't fullscreen a non-video element) or the gesture is+  rejected. exitFullscreen is ASYNC - .open clears on the fullscreenchange+  event, not synchronously (don't assert it closed on the next line).+- Listen for fullscreenchange; when document.fullscreenElement is null, drop+  .open so DOM state and the host stay in lockstep (Esc / host chrome exits).+- Style the fullscreen element to FILL (:fullscreen{width/height:100%}) + inner+  img object-fit:contain, max 100%. Never set fixed px - let HD size the 90%.+Verified in pup (real fullscreen: document.fullscreenElement===#lightbox) and+via the fallback overlay; Esc layers (exit fill, then close modal).
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.23.13",+  "version": "0.24.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.23.13",+  "version": "0.24.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+88−7
@@ -256,7 +256,42 @@ header {   font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text3); } .modal-card .m-meta .dim-main { color: var(--text2); } .modal-card .m-meta .dim-missing { color: var(--danger); font-weight: 600; } .modal-card .m-meta .sep { color: var(--border); }-.modal-card img { display: block; width: 100%; max-height: 84vh; object-fit: contain; background: #000; }+.modal-card .m-img-wrap { position: relative; }+.modal-card .m-img-wrap img { display: block; width: 100%; max-height: 84vh; object-fit: contain; background: #000; cursor: zoom-in; }+/* "Fill window" button overlaid on the modal image (top-right). In HD this+   grows the webview to a 90%-of-window lightbox; elsewhere it's true+   fullscreen. */+.modal-card .m-fill-btn { position: absolute; top: 10px; right: 10px; width: 34px; height: 34px;+  display: inline-flex; align-items: center; justify-content: center;+  background: rgba(13,17,23,.72); color: var(--text); border: 1px solid rgba(255,255,255,.18);+  border-radius: 8px; cursor: pointer; backdrop-filter: blur(3px); transition: background .15s, border-color .15s; }+.modal-card .m-fill-btn:hover { background: rgba(0,184,176,.22); border-color: rgba(0,184,176,.6); color: var(--accent-bright); }+.modal-card .m-fill-btn svg { width: 17px; height: 17px; }+/* Modal close (×) — top-right of the whole card; the image no longer closes on+   click (it fills the window now), so closing is explicit. */+.modal-card .m-close { position: absolute; top: 8px; right: 8px; width: 30px; height: 30px; z-index: 2;+  display: inline-flex; align-items: center; justify-content: center;+  background: rgba(13,17,23,.7); color: var(--text2); border: 1px solid var(--border);+  border-radius: 8px; cursor: pointer; font-size: 17px; line-height: 1; }+.modal-card .m-close:hover { color: var(--text); border-color: rgba(0,184,176,.5); }++/* Fill-window LIGHTBOX — the element that goes fullscreen via the standard+   Fullscreen API. In HD's native webview this becomes a 90%-of-window lightbox+   that escapes the skinny panel; in pup/browser it's true fullscreen; on a+   phone or where the API is missing, the .open class alone makes it a full+   fixed-overlay (graceful CSS fallback, no API needed). */+#lightbox { position: fixed; inset: 0; z-index: 300; display: none;+  align-items: center; justify-content: center; background: rgba(0,0,0,.95); }+#lightbox.open { display: flex; }+#lightbox img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }+/* When the host actually grants fullscreen, fill whatever rectangle it gave us+   (HD's 90%, or the true screen) — never a fixed cap; let the host size it. */+#lightbox:fullscreen, #lightbox:-webkit-full-screen { width: 100%; height: 100%; background: #000; }+#lightbox .lb-close { position: absolute; top: 14px; right: 16px; width: 40px; height: 40px;+  display: inline-flex; align-items: center; justify-content: center;+  background: rgba(13,17,23,.72); color: #fff; border: 1px solid rgba(255,255,255,.22);+  border-radius: 9px; cursor: pointer; font-size: 20px; line-height: 1; backdrop-filter: blur(3px); }+#lightbox .lb-close:hover { background: rgba(0,184,176,.28); border-color: rgba(0,184,176,.7); } /* Popup array inside the modal */ .modal-card .m-popups { padding: 10px 16px 14px; } .modal-card .m-popups .pop-h { font: 600 12px 'Familjen Grotesk', sans-serif; color: var(--text2);@@ -569,10 +604,17 @@ header { </div> <div class="toast" id="toast"></div> <div class="modal" id="modal"><div class="modal-card" id="modalCard">-  <div class="m-desc" id="mDesc"></div><div class="m-meta" id="mMeta"></div><img id="mImg" alt="">+  <button class="m-close" id="mClose" data-tooltip="Close (Esc)" aria-label="Close">✕</button>+  <div class="m-desc" id="mDesc"></div><div class="m-meta" id="mMeta"></div>+  <div class="m-img-wrap" id="mImgWrap">+    <img id="mImg" alt="">+    <button class="m-fill-btn" id="mFillBtn" aria-label="Fill window"+      data-tooltip="Fill the window — zoom this image to a big lightbox.&#10;In Hydrogen Desktop it escapes the panel to 90% of the window; in a pup/browser it goes fullscreen. Esc to exit."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg></button>+  </div>   <div class="m-popups" id="mPopups" style="display:none"></div>   <div id="mNerd"></div> </div></div>+<div id="lightbox"><img id="lbImg" alt=""><button class="lb-close" id="lbClose" aria-label="Exit fill window">✕</button></div> <div class="paste-overlay" id="pasteOverlay"><div class="paste-box" id="pasteMsg"></div></div>  <script>@@ -582,6 +624,7 @@ header {   const feed = $('feed'), toast = $('toast'), wsLed = $('wsLed');   const modal = $('modal'), mImg = $('mImg'), mDesc = $('mDesc'), mMeta = $('mMeta');   const mPopups = $('mPopups'), mNerd = $('mNerd');+  const lightbox = $('lightbox'), lbImg = $('lbImg');    // Channel from URL path /log/<channel>/ (matches Rust route + base href)   // Single-window app: shotlog owns its channel tabs. The path (or ?channel=)@@ -909,12 +952,50 @@ header {     modal.classList.add('open'); modal.scrollTop=0;   }   function closeModal(){ modal.classList.remove('open'); }-  // Close on: backdrop click, clicking the zoomed image again, or Escape.-  // (Metadata/desc clicks are preserved — only the backdrop and image close.)+  // Modal closes on: backdrop click, the × button, or Escape (when NOT filling).   modal.addEventListener('click', e => { if(e.target===modal) closeModal(); });-  mImg.style.cursor='zoom-out';-  mImg.addEventListener('click', e => { e.stopPropagation(); closeModal(); });-  document.addEventListener('keydown', e => { if(e.key==='Escape' && modal.classList.contains('open')) closeModal(); });+  $('mClose').addEventListener('click', e => { e.stopPropagation(); closeModal(); });++  // ── Fill window: zoom the image to fill the surface via the standard+  // Fullscreen API. In Hydrogen Desktop's native webview this grows the panel+  // to a 90%-of-window lightbox (HD implements the standard API specially); in+  // a pup/browser it's true fullscreen; where the API is unavailable (some+  // phones), the .open class alone makes the lightbox a full fixed overlay.+  // ONE code path — the host decides the final rectangle; we just request+  // fullscreen on the right element and style it to fill its viewport.+  const rfs = lightbox.requestFullscreen || lightbox.webkitRequestFullscreen;+  const xfs = document.exitFullscreen || document.webkitExitFullscreen;+  function fsElement(){ return document.fullscreenElement || document.webkitFullscreenElement || null; }+  function fillWindow(src, alt){+    // The click that called this IS the user gesture the API requires.+    lbImg.src = src; lbImg.alt = alt || '';+    lightbox.classList.add('open');           // fallback overlay + backdrop, always+    if (rfs) { try { const p = rfs.call(lightbox); if (p && p.catch) p.catch(()=>{}); } catch {} }+  }+  function exitFill(){+    // If the host granted fullscreen, leaving it fires fullscreenchange, which+    // clears .open. Otherwise (fallback overlay) clear it here directly.+    if (fsElement() === lightbox && xfs) { try { xfs.call(document); } catch { lightbox.classList.remove('open'); } }+    else { lightbox.classList.remove('open'); }+  }+  // Keep DOM state in lockstep with the host: if the user exits fullscreen by+  // any means (Esc, host chrome), drop the lightbox too.+  const onFsChange = () => { if (!fsElement()) lightbox.classList.remove('open'); };+  document.addEventListener('fullscreenchange', onFsChange);+  document.addEventListener('webkitfullscreenchange', onFsChange);+  // Clicking the image (or the ⛶ button) fills the window; backdrop/× exits.+  mImg.addEventListener('click', e => { e.stopPropagation(); fillWindow(mImg.src, mImg.alt); });+  $('mFillBtn').addEventListener('click', e => { e.stopPropagation(); fillWindow(mImg.src, mImg.alt); });+  lightbox.addEventListener('click', e => { if (e.target === lightbox || e.target === lbImg) exitFill(); });+  $('lbClose').addEventListener('click', e => { e.stopPropagation(); exitFill(); });++  document.addEventListener('keydown', e => {+    if (e.key !== 'Escape') return;+    // Layered escape: exit fill first (if not already handled natively by the+    // host), then close the modal.+    if (lightbox.classList.contains('open')) { exitFill(); return; }+    if (modal.classList.contains('open')) closeModal();+  });    function renderAll() {     feed.innerHTML='';