# 0006: Two permanent layers, real readiness, and an offline continuation that never becomes authority - Status: Accepted - Date: 2026-08-27 - Related: [0005](0005-authoritative-playback.md), Ekklesia [ADR 0043](https://git.riverofglorymn.org/River_of_Glory_Church/Ekklesia) (display offline code) ## Context The legacy `v1` renderer rebuilt image elements per slide and faded the outgoing frame to transparent, which let the black page background show through the middle of every transition. `v2` fixed it with two permanent layers and never fading the outgoing one, and that fix is the single most visible quality difference on a real television. Signage additionally has to transition between things that are not images at all: a provider's live React renderer, mounted through the platform's contribution ABI. ## Decision ### Two permanent scene layers, and one direction of fade The display owns exactly two full-screen layers, created once and never rebuilt. For every transition: 1. Resolve and mount the incoming content in the **hidden** layer — a native image, or a contributed renderer mounted through `context.contributions.mount(...)`. 2. Wait for **actual readiness**: `img.decode()` for an image, and the renderer's own `mount` resolution for a contribution, which the contribution ABI defines as asynchronous precisely so a renderer can say "not yet". 3. Discard completion belonging to a stale generation. A newer transition always wins. 4. Keep the outgoing layer **fully opaque** the entire time. 5. Raise the prepared incoming layer above it at opacity zero. 6. Fade **only the incoming layer** in. 7. Unmount and release the old layer after the fade completes. Both layers are never faded at once, so the black root can never contribute to the composite. ### Failure keeps the last good frame If incoming content cannot be decoded or mounted: keep the frame that is already on screen, record a bounded structured error, mark **that selection** locally unusable, and continue. A broken item is never retried in a tight loop, and one broken item never blanks a screen. An individually empty provider slot is different, and is skipped immediately with no error at all — that is the provider saying "nothing right now", which is ordinary. Only when there is neither renderable timeline content nor a cached good frame does the display show its built-in slate: restrained, non-flashing bars and the words *Content unavailable*, carrying no exception, URL, payload, credential, database detail, or identifier. Detail belongs in Signage's sanitized management telemetry, where an already-authorized operator can read it. A deliberate blackout is not a failure and never reaches the slate: it is pure black. ### The management preview is the same code The Dashboard's live preview mounts the identical renderer host, the identical native-image presentation resolver, and the identical layer transition. It differs in exactly two ways: it does not register a viewer, and it sends no heartbeat. A preview that reimplemented the transition would drift from what the wall actually shows, which makes it worse than no preview. ### Offline continuation, and its honest limit Once mounted, a display survives losing the host. Signage persists its own domain state — viewer identity, the last authoritative snapshot, resolved selections and revisions, audience payloads, the timing anchor, transport state, renderer contribution identity, and the most recent safe error — in **IndexedDB**, and native image bytes in **Cache Storage**. Ordinary HTTP cache is not an asset manager and is not used as one. While offline the display continues deterministically from the last authoritative anchor, applying cached scheduling and expiration, preserving pause, blackout, static hold, and remaining duration, skipping explicitly expired or withdrawn content, keeping the last good frame when expected content is unusable, and never inventing provider data. Catch-up is arithmetic, mirroring the server's own rule, so a long outage does not replay linearly. **A local cursor is never reported back as authority.** On reconnect the server's state wins immediately; incoming artifacts and data are staged before the current frame is replaced, no missed slides are replayed, and superseded payloads and image bytes are pruned only after the new snapshot is usable. ### The limit this cannot fix, stated rather than papered over **A cold browser start while Ekklesia is unreachable cannot mount Signage at all.** The platform's display service worker caches the shell and the declared code, deliberately not the display descriptor — a descriptor is authorization-bearing, and caching one would let a revoked screen keep showing content. Signage cannot and must not work around that: everything above applies to a display that is **already mounted** when its network fails, which is the overwhelmingly common case (a router reboot, an uplink drop, a switch power-cycle). True offline cold start requires something only Core can provide: an authorized offline display descriptor or lease with its own expiry and revocation. That is tracked as an Ekklesia prerequisite, not as Signage work, and this repository does not claim the capability it does not have. ## Consequences - Transitions are visually identical for images and for live React renderers, because readiness is awaited rather than assumed for both. - A provider renderer that throws costs one slide, not the screen. - The preview and the wall cannot disagree. - A screen that reboots during an outage shows Ekklesia's own offline state and recovers by itself when connectivity returns, but shows no Signage content until it can resolve its descriptor. ## Alternatives - **Crossfade both layers.** Rejected: it is the `v1` bug, and it is visible on every transition. - **Treat `onload`, or a fixed delay, as readiness.** Rejected: a large image paints mid-fade, and a renderer that is still fetching a font shows a flash of the wrong thing. - **Cache the display descriptor so a cold start works offline.** Rejected: it is a stored permission decision, and a revoked screen would keep showing content. This is the platform's rule, and Signage respecting it is the point. - **Use the HTTP cache for image bytes.** Rejected: no eviction control, no way to prune superseded renditions deliberately, and no way to know what is actually available offline.