* fix(dashboard): use data-auth-src for thumbnail images in modals and views
Plain <img src> tags can't send the Bearer token, causing 403 on
/api/content/:id/thumbnail. Extracted loadAuthImage/hydrateAuthImages
from content-library.js into utils.js and applied the data-auth-src
pattern to playlists, device-detail, and widgets views.
Closes thumbnail rendering in:
- Playlist items list and add-item modal
- Device assignment list and assign-content modal
- Widget content picker, logo, and background images
* fix(dashboard): add requestAnimationFrame fallback for auth image hydration
The IntersectionObserver callback fires asynchronously and may miss
images on first render when the DOM layout isn't settled yet. Add a
rAF fallback that manually loads any still-unloaded images visible
within the viewport (same 300px margin as the observer).
* fix(dashboard): load visible auth images synchronously, not via observer
getBoundingClientRect() forces layout synchronously so visible images
load immediately. IntersectionObserver is now only used for lazy-
loading off-screen images. This eliminates the async timing gap on
first render where neither the observer callback nor rAF would fire.
* fix(dashboard): load all auth images immediately, skip visibility check
Simplifies hydrateAuthImages to load every img[data-auth-src] directly.
loadAuthImage deletes the attribute so observer double-fire is safe.
This eliminates any possible IntersectionObserver/BoundingClientRect
timing issues on first render.
* debug: add console logs to trace auth image hydration flow
* fix(dashboard): hydrate auth images in device detail initial load
loadDevice() renders the playlist tab with data-auth-src images
but never called hydrateAuthImages. Only the playlist-switch path
(line 1022) had the hydrate call. Added hydrateAuthImages to the
initial contentEl.innerHTML render so thumbnails load on first view.
* chore: remove debug logs, final clean version
Surface the server's manner-of-death (crashed / clean_exit / silent) as a subordinate qualifier ON the
Offline badge (not a 4th liveness state), on both the device list and device-detail. Rides livenessBadge.
- Reliability-aware label (contract §10): clean_exit reads plainly on /player (reliable), "(best-effort)"
on APK/.wgt. silent = "silent (no signal)".
- Honest hover tooltip on every reason (both views), incl. silent = "external/violent: power loss, network,
force-stop, or MDM/kill". Never fabricates a reason (no-reason -> plain Offline); state-gated (reason only
on Offline); clears on re-online (matches the server).
- Filter drill-in: <optgroup> "Offline by reason" -> Offline · silent / crashed / clean exit, matched via a
data-offline-reason attribute (Offline·silent = the MDM-killed set — the Bold use case). Existing
three-state filter (All/Healthy/Reconnecting/Offline) unchanged.
- List label shortened to fit the pill (full text stays on detail; tooltip carries the full honesty both).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The patch4 server derives 3-state liveness (healthy / degraded-reconnecting / offline) and emits it as
data.liveness on dashboard:device-status, but the frontend only consumed binary online/offline — the
signal was thrown away. Add a shared livenessBadge() helper (utils.js) consumed by both the dashboard
device list and the device-detail view (initial render + live statusHandler). Degrades to the binary
status when liveness is absent (old payload / plain reconnect+disconnect emits / DB device object) so
nothing renders blank; unknown/no-data -> offline default. CSS: healthy=green, degraded=amber+pulse
(reads as reconnecting), offline=red — reusing the existing --success/--warning/--danger tokens. Labels
in en.js (all locales fall back to en). Frontend-only; server derivation unchanged. 13/13 unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>