diff --git a/internal/web/public/css/wall.css b/internal/web/public/css/wall.css index aa3aea5..ebb91d5 100644 --- a/internal/web/public/css/wall.css +++ b/internal/web/public/css/wall.css @@ -276,6 +276,19 @@ body.cursor-idle * { text-transform: uppercase; letter-spacing: 0.1em; } +/* Shown while a camera is on but pump-cv hasn't decoded a frame yet + (cold start / RTSP disconnected) — beats a broken-image icon. */ +.wall-cam-wait { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + color: #6a6a72; + letter-spacing: 0.04em; +} +.wall-cam-wait[hidden] { display: none; } /* Toggle switch — small, kiosk-friendly */ .wall-cam-toggle { position: relative; @@ -448,7 +461,14 @@ body.sleeping .wall-sleep { cursor: pointer; } Hidden by default; wall.js toggles `hidden` based on the state poll. */ .wall-calib { position: fixed; - inset: 0; + /* Start below the shared navbar (header.html) so the kiosk operator can + still navigate away from the AI page while calibration is pending — + inset:0 here used to paint the overlay over the navbar entirely. + wall.js measures the real navbar height into --wall-navbar-h. */ + top: var(--wall-navbar-h, 56px); + right: 0; + bottom: 0; + left: 0; background: var(--wall-bg, #0a0a0c); z-index: 1000; display: flex; @@ -497,6 +517,12 @@ body.sleeping .wall-sleep { cursor: pointer; } height: 100%; object-fit: cover; } +.wall-calib-prev-wait { + font-size: 0.9rem; + color: #6a6a72; + letter-spacing: 0.04em; +} +.wall-calib-prev-wait[hidden] { display: none; } .wall-calib-prev-label { position: absolute; top: 6px; diff --git a/internal/web/public/js/wall.js b/internal/web/public/js/wall.js index e287513..85e17d3 100644 --- a/internal/web/public/js/wall.js +++ b/internal/web/public/js/wall.js @@ -28,6 +28,21 @@ clipClose: document.getElementById('wallClipClose'), }; + // ─── Navbar height → CSS var ──────────────────────────────────────── + // The shared navbar (header.html) sits above the wall grid. Its real + // height depends on the logo size and font metrics, so measure it and + // publish it as --wall-navbar-h. Both the grid's height calc and the + // calibration overlay's top offset read this var; without it they fall + // back to a 56px guess that's ~25px short of the actual navbar. + function measureNavbar() { + const nav = document.querySelector('.navbar'); + if (!nav) return; + const h = Math.round(nav.getBoundingClientRect().height); + if (h > 0) document.documentElement.style.setProperty('--wall-navbar-h', h + 'px'); + } + measureNavbar(); + window.addEventListener('resize', measureNavbar, { passive: true }); + // ─── Header date + clock ──────────────────────────────────────────── function renderHeader() { const now = new Date(); @@ -156,6 +171,7 @@
+
off
@@ -180,8 +196,16 @@ if (!tile) return; const img = tile.querySelector('.wall-cam-img'); const off = tile.querySelector('.wall-cam-off'); - img.hidden = false; + const wait = tile.querySelector('.wall-cam-wait'); off.hidden = true; + // Until the first frame decodes, show a "waiting" tile rather than a + // broken-image icon. pump-cv 404s the snapshot endpoint while a camera + // has no decoded frame yet (cold start / RTSP disconnected), so the + // error fires every poll until the stream comes up. + img.hidden = true; + if (wait) wait.hidden = false; + img.onload = () => { img.hidden = false; if (wait) wait.hidden = true; }; + img.onerror = () => { img.hidden = true; if (wait) wait.hidden = false; }; const tick = () => { // Cache-buster — the snapshot endpoint sends Cache-Control: no-store // anyway, but `?t=` defends against any intermediate caching layer. @@ -198,8 +222,11 @@ if (!tile) return; const img = tile.querySelector('.wall-cam-img'); const off = tile.querySelector('.wall-cam-off'); + const wait = tile.querySelector('.wall-cam-wait'); + img.onload = img.onerror = null; img.hidden = true; img.removeAttribute('src'); + if (wait) wait.hidden = true; off.hidden = false; } @@ -448,10 +475,21 @@ calib.prevs.innerHTML = camNames.map(n => `
${escapeHTML(n)} - ${escapeHTML(n)} preview + + waiting for camera…
`).join(''); + // Show the frame once it decodes; fall back to the "waiting" label + // whenever the snapshot 404s (no frame yet) instead of a broken image. + camNames.forEach(n => { + const tile = calib.prevs.querySelector(`.wall-calib-prev[data-cam="${cssEscape(n)}"]`); + if (!tile) return; + const img = tile.querySelector('img'); + const wait = tile.querySelector('.wall-calib-prev-wait'); + img.onload = () => { img.hidden = false; if (wait) wait.hidden = true; }; + img.onerror = () => { img.hidden = true; if (wait) wait.hidden = false; }; + }); } function calibStartPreviews(camNames) { diff --git a/internal/web/public/wall-sw.js b/internal/web/public/wall-sw.js index ee9f2ca..cccf209 100644 --- a/internal/web/public/wall-sw.js +++ b/internal/web/public/wall-sw.js @@ -16,7 +16,10 @@ // cache. v4: the /wall/ HTML shell is now served network-first (below), so a // stale shell can no longer be pinned on a long-lived kiosk — this bump also // flushes any v3 cache still holding the pre-navbar shell. -const CACHE_NAME = "pump-wall-v4"; +// v5: wall.css/wall.js changed (calibration overlay no longer paints over the +// navbar; navbar height measured into --wall-navbar-h) — flush the v4 precache +// so the cache-first /fs/ handler stops serving the old shell assets. +const CACHE_NAME = "pump-wall-v5"; const SHELL = [ "/wall/", "/fs/public/css/wall.css",