Commit graph

5 commits

Author SHA1 Message Date
ScreenTinker 5fa6b2d07d A baseline moves when the fix reaches SCREENS, which is not one rule
Two changes that are really one idea: the parity model treated all four
players as if they update the same way, and they do not.

WEB AND BRIGHTSIGN GET audio.volume BACK.

The audit removed it because v1.9.28's index.html contained the string
set_volume zero times, and because the handler read payload.value while the
dashboard sends { level }. The second reason 1.9.31 fixed. The first was
reasoning from the wrong artifact: this player is SERVED BY THE SERVER, so a
browser panel runs whatever build is answering it, not the release its row was
created under. There is no browser panel stuck on the v1.9.28 player once the
server moves — and prod moved tonight. The slider works on those displays right
now while the baseline says it does not, so the dashboard is hiding a working
control from every display that declares nothing.

BrightSign comes with it, on the same served player. The unit-specific doubt is
whether a hwz player's media element is reachable at all — and that is already
answered by audio.mute, which this baseline has always claimed: set_volume
reaches setMediaVolume() and device:mute-changed reaches currentVideoEl.muted,
same element, same path. If hwz swallowed one it would swallow both.

TIZEN DOES NOT COME WITH THEM, AND THE TEST NOW KNOWS WHY.

A .wgt sits on the panel until somebody updates it. Cutting 1.9.31 put nothing
on any screen, so an un-updated Tizen panel still has the broken handler and
moving its baseline would resurrect the dead slider on real hardware.

The test could not express that. It judged every family against "shipped
source", resolved as the newest tag — which is HEAD on a release commit, so
tagging 1.9.31 flipped all four biconditionals at once and demanded a baseline
change for displays that cannot have the fix yet. Green tree, red build, naming
a baseline, with nothing in the diff to explain it. main would have gone red on
the next commit whatever it contained; #242 just got there first.

So the two families are now modelled separately. Server-served: judged against
the working tree, both directions, because both are decidable from the build we
are about to serve. Device artifact: judged against the previous release, and
only in the over-claim direction — "the baseline claims it, so the shipped
player had better implement it" is always true and worth failing on, while
"HEAD gained the handler, so add it" is a guess about how many panels have
updated. The cost is that a stale entry can outlive the artifact reaching the
fleet; that is a judgement about screens, so a person makes it in
player-capabilities.js and records why.

player-capabilities.test.js carried the same stale reasoning hardcoded, and
docs/player-parity.md stated the old facts in four places — a parity matrix
that lies being the exact failure this whole model exists to stop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
2026-08-06 22:54:45 -05:00
ScreenTinker c1270599c3 Make the parity matrix true, and stop three controls that do nothing
The parity doc and the capability model had drifted from the players in both
directions, and nothing failed when they did. Auditing all four players against
their shipped sources turned up three controls a customer can press today that
change nothing, and a set of baselines that were partly too generous and partly
too stingy.

The three dead controls:

  - The volume slider works on Android only. The dashboard sends set_volume as
    { level: 0..1 }; the web player reads payload.value and Tizen reads
    payload.value ?? payload.volume, so on both the number is undefined and the
    handler quietly declines. Three complete, working volume implementations
    that cannot be driven. The fix is one line in each player and belongs to
    those files; audio.volume is out of the web and brightsign baselines until
    it lands, held there by a biconditional test that fails the moment a player
    starts reading `level`.

  - Every #161 Tier-2 command was refused for the entire fleet. lock_now,
    power_menu, status_bar, block_uninstall and unblock_uninstall were gated on
    system.device_owner, which no player declares and no baseline grants, so
    supports() was false everywhere -- including on the device-owner panels the
    feature was built for. The dashboard still drew the buttons because it also
    gates on device.tier === 2. Fixed here: those five now accept
    system.device_owner OR system.kiosk, which PlayerCapabilities.kt declares
    under `if (isOwner)` and nothing else, and which no non-Android player
    declares. Android should declare system.device_owner and retire the
    stand-in.

  - enable_system_capture required the capability it creates. It raises the
    MediaProjection consent dialog -- the way a panel GAINS capture -- and was
    gated on remote.screenshot, so the only panel that needs it was the one
    panel that could not be sent it. Now ungated. The dashboard still hides the
    button behind the same check; that half is a frontend change.

The baselines describe what an un-updated fielded display can do, and since
v1.9.29 is the first build in which any player declares anything, that means
v1.9.28. Every entry is now justified against `git show v1.9.28:<source>`:

  - android loses display.power (v1.9.28 answers screen_on with a logged no-op,
    so the ON half is dead on every fielded panel and one capability renders
    both buttons) and system.reboot (owner-only; off-owner it paints an
    accessibility power dialog over the signage). Scheduled reboots now skip
    undeclared Android panels rather than logging a reboot that never happened,
    which is the reason that gate exists.
  - tizen gains display.power: v1.9.28 implements both halves with no signing
    and no panel API, so withholding it hid a working control.
  - brightsign loses audio.volume, display.power, system.reboot,
    system.restart_player and offline.cache. All need a host bridge the unit is
    not known to have, and restart_player without one is the page reload that
    darkened a panel on 2026-07-28.

Also found, not fixed here because the files belong to others:
st-bridge.js computeCapabilities() is dead code -- nothing calls BS.capabilities()
-- and its 199 lines of passing tests constrain nothing a BrightSign actually
declares; the two disagree on six capabilities and the bridge is right about
most of them. BrightSign's "Force update" button is dead. PlayerCapabilities.kt
under-declares display.brightness.

The new test reads the player sources rather than the table: a dead-button rule
(every gated command has a branch somewhere), an unreachable-capability rule
(which would have caught system.device_owner), and biconditionals so a fix in a
player fails the test until the baseline follows. Claims that need hardware --
CEC reaching a display, a widget being allowed a service worker, SyncManager
holding frame lock -- are marked unverifiable in the document instead of
asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-06 10:24:10 -05:00
ScreenTinker e5583e529e The Tizen baseline describes a fielded panel, not the one we are shipping
Two more corrections from the cross-player audit, both mine.

audio.volume removed: a fielded Tizen panel has NO set_volume handler — the
command falls through to "unknown command" and the dashboard slider does
nothing. One of the platform branches adds a handler, and those panels will
declare the capability for themselves once they run it; the baseline exists to
describe an un-updated display, so it must not borrow credit from a build that
has not shipped.

remote.screenshot and remote.stream added: both really are implemented in the
shipped player (captureAndSend, startStreaming). Omitting them would have hidden
working controls on every legacy Tizen display the moment gating went live —
the opposite failure, and the more damaging one.

That asymmetry is the thing to hold on to: over-claiming shows a dead button,
under-claiming removes a working one, and only reading the shipped code tells
you which you are doing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 14:21:19 -05:00
ScreenTinker a08e6c3e06 Tizen does not have offline caching — correct the baseline
The platform audit caught my own contract lying. I gave the Tizen baseline
offline.cache; Tizen caches only the playlist JSON (st_payload_cache, in
localStorage) and has no service worker and no media cache, so the bytes still
come from the network and an outage leaves a panel holding a playlist it cannot
play.

That is exactly the claim this model exists to prevent, made by the model itself,
and it would have applied to every legacy Tizen panel — the ones that declare
nothing and depend entirely on the baseline being honest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 14:15:34 -05:00
ScreenTinker 6bc709d2f7 The capability contract: what each player can actually do
Foundation for platform-native parity. The dashboard offered every control to
every display — a browser tab cannot reboot its host, a Tizen TV has no
device-owner concept, a BrightSign has no per-window brightness — so those
buttons did nothing, silently, and read as bugs. "UI that reports success and
changes nothing" is a recurring shape here; this ends it by letting the frontend
hide what a display cannot do.

The player DECLARES its capabilities at registration rather than the server
inferring them from a table, because only the player knows at runtime: an Android
device gains real screenshots when accessibility is switched on and loses Tier-2
commands when it is not device owner.

The trap this had to avoid is the opposite failure. Several hundred displays are
in the field declaring nothing, and none will update before the next dashboard
deploy — treating absence as "supports nothing" would strip the UI for the entire
fleet at once. So an ABSENT declaration falls back to a per-platform baseline,
while an EMPTY one is honoured as a player genuinely saying it can do nothing.
Those two cases are trivial to conflate and the difference is a dark dashboard.

Baselines carry only what has always worked on that platform. Anything
conditional — screenshots needing accessibility, kiosk needing device owner,
native sync needing one L2 network — is omitted, so a legacy display shows those
controls only once it declares them. A control that appears late beats one that
lies now.

Capability names are stable strings because they are persisted per device and
sent over the wire; renaming one silently disables a control on every display
still reporting the old name. An unknown name from a NEWER player is dropped
rather than invalidating the whole declaration.

1094 pass.

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