diff --git a/server/player/index.html b/server/player/index.html
index 6ed46a4..a2d3872 100644
--- a/server/player/index.html
+++ b/server/player/index.html
@@ -423,6 +423,15 @@
// playback muted).
let userHasInteracted = false;
let advanceTimer = null;
+ // Buffered widget swap (#directory-board black-cycle): build the next widget iframe
+ // behind the current content and reveal it only on 'load', so a widget reload never
+ // blanks the screen. WIDGET_SWAP_TIMEOUT_MS reveals anyway if 'load' never fires (a
+ // network hang must not leave a frozen/blank board). A solo/held widget re-fetches its
+ // data every WIDGET_SOLO_REFRESH_MS — decoupled from duration_sec, because a static
+ // board re-querying the DB + re-rendering every few seconds, fleet-wide, is pure waste.
+ const WIDGET_SWAP_TIMEOUT_MS = 8000;
+ const WIDGET_SOLO_REFRESH_MS = 60000;
+ let pendingWidgetSwap = null; // { iframe, timer } while a new widget iframe loads
// Per-zone rotation timers (multi-zone). Each zone advances independently on
// its own interval, decoupled from the fullscreen advanceTimer/nextItem.
let zoneTimers = {};
@@ -2035,8 +2044,16 @@
zoneTimers = {};
}
- function teardownCurrentMedia() {
- if (advanceTimer) { clearTimeout(advanceTimer); advanceTimer = null; }
+ function teardownCurrentMedia(keep) {
+ // On a buffered widget reveal (keep set) the widget's advance/refresh timer was just
+ // armed by renderContent and must survive; only a real teardown cancels it.
+ if (!keep && advanceTimer) { clearTimeout(advanceTimer); advanceTimer = null; }
+ // Cancel any in-flight buffered widget swap so its deferred reveal can't fire after
+ // we've torn down (which would drop the incoming content). reveal() nulls this before
+ // calling teardownCurrentMedia(iframe), so preserving `keep` is safe.
+ if (pendingWidgetSwap && pendingWidgetSwap.iframe !== keep) {
+ clearTimeout(pendingWidgetSwap.timer); pendingWidgetSwap = null;
+ }
clearZoneTimers();
const container = document.getElementById('playerContainer');
if (container) {
@@ -2048,7 +2065,13 @@
v.load();
} catch (e) { /* element may already be detached */ }
});
- container.innerHTML = '';
+ if (keep) {
+ // Buffered widget swap: drop the outgoing content but keep the freshly-loaded
+ // iframe we're swapping in.
+ Array.from(container.children).forEach(ch => { if (ch !== keep) { try { ch.remove(); } catch (e) {} } });
+ } else {
+ container.innerHTML = '';
+ }
}
// #146 fix: also release currentVideoEl even if it was DETACHED from the container —
// a detached-but-playing