A display reports itself offline ready whenever its saved state lists any slides, even if it holds none of the image bytes it would need to show them without a network. The Displays tab then shows a green badge for a screen that would freeze or go to the slate in an outage.
Where
src/display/controller.ts on main (9aa8206) computes it twice, two different ways:
Heartbeat, which is what the server stores and the Dashboard shows (line 765):
A display whose image downloads into its media cache fail shows green "0 cached". Causes include storage CORS not configured for the API origin on a split deployment, a storage outage during prefetch, or a quota refusal.
Before 5f52334 it was worse. On a plain-http:// origin the in-memory fallback counted remembered URLs as cached, so the Stage 0 kiosk showed "2 cached" while unable to paint either image offline. cachedAssetCount is honest now, but offlineReady still never looks at it.
cachedAssetCount counts everything in the cache, not the images this timeline needs. A screen can show "3 cached" while missing the one image its rotation needs next.
How to reproduce
In a display test (for example tests/browser/display.test.ts), use a media cache whose store() always returns undefined, so downloads fail.
Apply a state with native-image selections.
The heartbeat body has offlineReady: true and cachedAssetCount: 0.
Suggested fix
Make readiness mean "local continuation could actually paint what it would replay":
During prefetch(), which already visits every native selection in the state, record how many of the native selections the offline resolver could choose have bytes in the media cache. At minimum, every distinct (providerId, itemId, itemRevision) in the snapshot whose slot is in the snapshot's timeline.
offlineReady = snapshot has selections && every one of those is cached. Contributed-renderer selections don't need bytes from this cache, so they don't count against readiness.
Use one function for both the heartbeat and the diagnostics, so they can't drift apart again.
Optionally, report the coverage (for example "1 of 2 images cached") instead of a raw count, so the badge tells an operator what's missing.
Add a regression test for the failing-download case above, and keep the existing "offline ready after a full cycle" behaviour.
Out of scope / not checked
Whether cachedAssetCount should exclude entries a pending prune is about to remove. Also, the server stores whatever the display reports, so no server or schema change should be needed.
Found by Claude during Stage 0 offline-playback work (found-by-claude).
## What's wrong
A display reports itself **offline ready** whenever its saved state lists any slides, even if it holds none of the image bytes it would need to show them without a network. The Displays tab then shows a green badge for a screen that would freeze or go to the slate in an outage.
## Where
`src/display/controller.ts` on `main` (`9aa8206`) computes it twice, two different ways:
- **Heartbeat**, which is what the server stores and the Dashboard shows (line 765):
```ts
offlineReady: snapshot !== undefined && snapshot.selections.length > 0,
```
- **Local diagnostics** (lines 163–164):
```ts
offlineReady: snapshot !== undefined && cachedAssetCount >= 0 && snapshot.selections.length > 0,
```
`cachedAssetCount >= 0` is always true, so this term does nothing. It reads as if `> 0`, or a real coverage check, was intended.
The Dashboard turns the stored value into the badge in `src/ui/panels/DisplaysPanel.tsx:96-97`:
```tsx
<Badge tone={viewer.offlineReady ? "success" : "neutral"}>
{viewer.offlineReady ? `${viewer.cachedAssetCount} cached` : "Not offline ready"}
</Badge>
```
## How it shows up
- A display whose image downloads into its media cache fail shows green **"0 cached"**. Causes include storage CORS not configured for the API origin on a split deployment, a storage outage during prefetch, or a quota refusal.
- Before `5f52334` it was worse. On a plain-`http://` origin the in-memory fallback counted remembered *URLs* as cached, so the Stage 0 kiosk showed "2 cached" while unable to paint either image offline. `cachedAssetCount` is honest now, but `offlineReady` still never looks at it.
- `cachedAssetCount` counts everything in the cache, not the images this timeline needs. A screen can show "3 cached" while missing the one image its rotation needs next.
## How to reproduce
1. In a display test (for example `tests/browser/display.test.ts`), use a media cache whose `store()` always returns `undefined`, so downloads fail.
2. Apply a state with native-image selections.
3. The heartbeat body has `offlineReady: true` and `cachedAssetCount: 0`.
## Suggested fix
Make readiness mean "local continuation could actually paint what it would replay":
- During `prefetch()`, which already visits every native selection in the state, record how many of the native selections the offline resolver could choose have bytes in the media cache. At minimum, every distinct `(providerId, itemId, itemRevision)` in the snapshot whose slot is in the snapshot's timeline.
- `offlineReady = snapshot has selections && every one of those is cached`. Contributed-renderer selections don't need bytes from this cache, so they don't count against readiness.
- Use **one** function for both the heartbeat and the diagnostics, so they can't drift apart again.
- Optionally, report the coverage (for example "1 of 2 images cached") instead of a raw count, so the badge tells an operator what's missing.
Add a regression test for the failing-download case above, and keep the existing "offline ready after a full cycle" behaviour.
## Out of scope / not checked
Whether `cachedAssetCount` should exclude entries a pending prune is about to remove. Also, the server stores whatever the display reports, so no server or schema change should be needed.
---
_Found by Claude during Stage 0 offline-playback work (`found-by-claude`)._
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What's wrong
A display reports itself offline ready whenever its saved state lists any slides, even if it holds none of the image bytes it would need to show them without a network. The Displays tab then shows a green badge for a screen that would freeze or go to the slate in an outage.
Where
src/display/controller.tsonmain(9aa8206) computes it twice, two different ways:cachedAssetCount >= 0is always true, so this term does nothing. It reads as if> 0, or a real coverage check, was intended.The Dashboard turns the stored value into the badge in
src/ui/panels/DisplaysPanel.tsx:96-97:How it shows up
5f52334it was worse. On a plain-http://origin the in-memory fallback counted remembered URLs as cached, so the Stage 0 kiosk showed "2 cached" while unable to paint either image offline.cachedAssetCountis honest now, butofflineReadystill never looks at it.cachedAssetCountcounts everything in the cache, not the images this timeline needs. A screen can show "3 cached" while missing the one image its rotation needs next.How to reproduce
tests/browser/display.test.ts), use a media cache whosestore()always returnsundefined, so downloads fail.offlineReady: trueandcachedAssetCount: 0.Suggested fix
Make readiness mean "local continuation could actually paint what it would replay":
prefetch(), which already visits every native selection in the state, record how many of the native selections the offline resolver could choose have bytes in the media cache. At minimum, every distinct(providerId, itemId, itemRevision)in the snapshot whose slot is in the snapshot's timeline.offlineReady = snapshot has selections && every one of those is cached. Contributed-renderer selections don't need bytes from this cache, so they don't count against readiness.Add a regression test for the failing-download case above, and keep the existing "offline ready after a full cycle" behaviour.
Out of scope / not checked
Whether
cachedAssetCountshould exclude entries a pending prune is about to remove. Also, the server stores whatever the display reports, so no server or schema change should be needed.Found by Claude during Stage 0 offline-playback work (
found-by-claude).