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.24.0
9 files changed
+122−12
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. 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='';