Commit graph

3 commits

Author SHA1 Message Date
ScreenTinker 3d9ef039e5 Make the volume slider real, and stop an empty playlist wiping the cache
Three faults in the web player, each found by driving the shipped code in a
browser against the real server rather than by reading it.

set_volume did nothing at all. The dashboard sends `{ level: 0..1 }`
(device-detail.js: slider/100) and the Android player reads exactly that; this
player read `payload.value` and divided it by 100. Nothing in the product sends
`value`, so every browser panel acked the command and ignored it — the quietest
possible failure. Correcting only the key would have been worse than leaving it
broken: `level: 0.5` would have become 0.5%, which is inaudible and looks fixed.
The fraction is now canonical, `value` is still read as a percentage for
anything written against the old handler, and the scale is chosen by WHICH KEY
arrived rather than by the size of the number — 1 is legal in both conventions,
so a magnitude guess is guaranteed to be wrong for somebody. Parsing moved into
volumeLevelFromCommand() so it can be asserted without a socket.

setMediaVolume() also wrote `el.muted = (v === 0)`, so any non-zero volume
un-muted whatever was playing. An item an operator had deliberately silenced
started making noise the moment anyone touched the slider — reproduced live:
item flagged muted, one set_volume, muted went false. Mute has four inputs and
a fixed order (lib/media-mute.js), it is resolved when the element is mounted,
and a level is not entitled to overrule it — least of all the autoplay rule,
where unmuting without a gesture costs the video rather than winning the audio.
Volume 0 is silence on its own.

And the service worker pruned its content cache to an EMPTY keep-set.
`assignments: []` is what the server sends for a device between playlists, for a
playlist never published, and inside the `catch` when a published_snapshot fails
to parse — none of which mean "delete the media". Reproduced: three cached
assets, one empty payload, cache emptied. That is only survivable while the
uplink is up, which is precisely when the offline cache is worthless. A cache
kept too long costs disk the quota reclaims anyway.

Verified in Chrome against a live server: volume 0.42/0.8/0/0.25 land on the
element and survive an item change, a muted item stays muted through a volume
command, and three cached assets survive an empty push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-06 16:18:25 -05:00
ScreenTinker c2240288a7 Serve the service worker from the root, so its scope needs no header to survive
Found deploying 1.9.29 to production. A worker's scope defaults to its own
directory, so /player/sw.js could only control /player/ and below; the fix was to
request a wider scope and permit it with Service-Worker-Allowed. That works right
up until something between the origin and the browser does not pass the header
on. Cloudflare served a CACHED response for that path across the deploy —
headers and all — and the registration failed outright.

A rejected registration is worse than a narrow one: the player runs with no
worker at all, on every URL, and nothing about it is visible from the server. The
origin was sending the header correctly the whole time; a cache-busted request
proved it. It self-heals when the edge entry expires, which is precisely the kind
of fix nobody should have to know about.

Served from /, the default scope is already the whole origin and no header has to
survive the trip — through Cloudflare, through whatever a self-hoster puts in
front of it, or through a corporate proxy we will never see. /player/sw.js keeps
serving for players still asking for it, and the header is still sent where it
does survive.

Verified in a real browser: all three of /player, /player/ and /player/index.html
are controlled from root scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-06 09:05:06 -05:00
ScreenTinker 75c1940821 Fix the worker scope that made web offline playback silently inert, and prune superseded assets
Found by QA against a real browser, not by any test in the suite: the bug lived
entirely in the relationship between a URL and a header.

A service worker's default scope is its own directory, so /player/sw.js could
only ever control /player/ and below — which does not include /player itself.
The player is served at all three of /player, /player/ and /player/index.html,
and /player is the one that gets used: it is what the dashboard shows and what
gets typed into a panel. On that URL registration SUCCEEDED, logged "Service
Worker registered", and then controlled nothing. No shell cache, no content
cache, no offline playback, no error. Every web and BrightSign panel served at
/player has been running with its offline story switched off.

Registration now asks for scope '/' and the server sends Service-Worker-Allowed
to permit it. Both halves are required — without the header the registration
does not narrow, it fails outright.

Also: revision-keyed sweeping could not reclaim a replaced asset's predecessor.
A replace writes a NEW randomly-named file, so the superseded copy lives at a
different path entirely and nothing keyed on the asset path can find it; it
would sit there until the quota evicted it. The player now declares the complete
set of media it needs — the raw assignments, so multi-zone items are included
and a prune cannot delete something a zone is still playing — and the worker
drops everything else.

QA results this pass: web player 18/18 against a real browser (cold start with
no network renders a cached video at readyState 4); Android 12/12 on a device
including a replace round-trip that re-fetched 6MB and then dropped it for the
new bytes, and a cold start with the server stopped that played from disk;
Tizen 11/11 for the no-storage path, which must degrade to streaming and must
not claim a capability it cannot honour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 16:30:12 -05:00