Commit graph

5 commits

Author SHA1 Message Date
ScreenTinker 28885d1a13 Merge branch 'feat/brightsign-ip-from-js'
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
# Conflicts:
#	server/test/device-controls-hidden.test.js
2026-08-10 15:40:31 -05:00
ScreenTinker 08b3d7d404 BrightSign: report IPv6, the attached display and the active video mode
Follow-on from the Node-stdlib work, guided by BrightSign's own dev-cookbook
rather than by guessing at module names.

IPv6 costs nothing extra — it comes from the same os.networkInterfaces() call
the v4 address does. The column, the API field and the dashboard card have all
existed since 1.9.29 and no player has ever filled them; the card is written to
appear ONLY when set, precisely so the overwhelmingly v4 fleet does not pay
screen space for an empty row. fe80:: is skipped for the same reason 169.254 is
— a link-local address is scoped to one interface and cannot be dialled from a
laptop across the office. A ULA is kept, because that one is reachable.

The attached display and video mode are new columns, and they answer the first
question anyone asks about a dark sign: which panel is that, and is the player
outputting at all. screen_width/height could not answer it — they are what the
PAGE believes it has, i.e. the widget's own geometry. Our XT245 drives a CX101
at 1920x1200@60 while the page reports its own canvas.

Per telemetry row rather than on `devices`, because a display can be swapped,
unplugged or renegotiated without the player re-registering.

MULTI-OUTPUT: the output is chosen by screen number, not hard-coded. A
dual-output player registers ONE DEVICE ROW PER OUTPUT (?screen=N →
output_index), so each row must report its own panel — otherwise a box driving
a lobby TV and a menu board shows the lobby TV twice. Both naming forms are
tried: probed on hardware, "hdmi" and "HDMI-1" both resolve to output 1, while
a second output that does not exist fails cleanly ("hdmi2" throws from the
constructor, "HDMI-2" rejects), so a single-output player reports nothing
rather than inventing a screen. That case has its own test.

Dashboard: two cards, shown only when the player reports them, like every other
card in that block.

Verified end to end on the real XT245 (FW 9.1.93.2) — attached_display=CX101,
video_mode=1920x1200@60, alongside local_ip 192.168.1.46, 119616 MB disk,
3656 MB RAM and live CPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-10 15:30:40 -05:00
ScreenTinker def30e6d39 BrightSign: report the LAN address, real disk, memory and load
Every one of these fields existed in the schema, the API and the dashboard,
and every one was NULL or misleading on a BrightSign. The XT245 had 6000
consecutive telemetry rows with local_ip NULL while sitting at a perfectly
reachable 192.168.1.46, and reported "1026 MB" of storage for a 119 GB NVMe.

The host half (autorun.brs) does collect an address, but nothing the host
sends was arriving at all — proven by the storage figure, which was the
browser's cache quota rather than any disk. So the page has to read this
itself, which is also the half that can be delivered: st-bridge.js is served
per page load, while autorun.brs needs a release bump to reach a player.

It is Node's standard library, not a @brightsign module. The widget is created
with nodejs_enabled, so os and fs are simply there — this is what BrightSign's
own dev-cookbook does in html5-app-template (both the .ts and .js variants).

Looking for a platform module is the trap, and it cost most of a day:
@brightsign/networkconfiguration EXISTS but exposes only callback,
getNeighborInformation and enableLeds — no config reader. hostconfiguration
has getConfig()/applyConfig() but returns host settings (forwardingEnabled,
hostName, loginPassword, nameServers) with no address in them. Both enumerated
on the live player, because the JavaScript API doc pages 404 and BrightSign's
own roNetworkConfiguration page links to one of the dead URLs.
getCurrentConfig() is BrightScript-only.

  local_ip          os.networkInterfaces(), skipping internal and 169.254
  ram_total/free    os.totalmem() / os.freemem()
  cpu_usage         1-min load average / core count, as a clamped percentage
  uptime_seconds    os.uptime() — the MACHINE, overriding the page's own
                    performance.now(), so a widget rebuilt by the watchdog no
                    longer hides weeks of real uptime
  storage_*         fs.statfsSync over the mounts under /storage, largest wins
                    (ours boots from NVMe with a dead card slot; others from SD)

Dashboard: the RAM and CPU cards were gated on "is this Android?", which was
right when Android was the only family that could measure them. They now
render for any player that reports the value, so a BrightSign gets them and
Android is untouched — including keeping its "--" cards when no reading has
arrived, since an empty card is a known state and a missing one reads as
"cannot". The BrightSign storage card loses its "player storage" caveat,
because the number is now the disk it always claimed to be.

Verified on the real XT245 (FW 9.1.93.2): 116.8 GB free of 116.8 GB, 2.68 GB
of 3.57 GB RAM, 3% CPU, uptime tracking the machine, local_ip 192.168.1.46 —
matching the address found independently by MAC-vendor scan, and a disk figure
matching the kernel's own block count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-10 15:05:06 -05:00
ScreenTinker 3333d5e968 Stop Android panels losing controls when they update
A declared capability set REPLACES the per-platform baseline rather than
merging with it, so anything the baseline grants and the player omits is a
control the operator loses by updating. Three were being lost.

- display.brightness: the per-window dim (setWindowBrightness) is Tier 0 —
  no permission, no owner, no WRITE_SETTINGS — and MainActivity applies it
  unconditionally. It was simply never declared.

- remote.screenshot / remote.stream: gated on the accessibility service,
  while captureScreen() falls through to ScreenshotCapture.captureView,
  a plain view draw with no permission check. A Tier-0 panel lost live view
  and screenshots by updating, and a GRANTED MediaProjection never became a
  capability either — consent given, capture working, server still refusing,
  because nothing re-declared.

- system.device_owner: no player declared it, so the server accepted
  system.kiosk as a stand-in for every Tier-2 command. Declaring the
  canonical name makes refusals say what they mean; the stand-in can retire
  one release after this reaches displays.

display.power stays conditional on purpose: screen_on works anywhere via a
wake lock but screen_off needs owner/admin/accessibility, and a control that
sleeps a panel it cannot wake is worse than no control. It is the sole entry
in the DELIBERATE set in player-parity-baselines.test.js.

Also fixes the capture-bootstrap gate in device-detail.js. It hung off
can('remote.screenshot'), which hid the button from exactly the panels that
need it. The gate is now Android-and-nothing-else, NOT "Android that lacks
capture": /api/devices/:id ships capabilitiesFor(), which flattens declared
and baseline into one array, and the android baseline contains
remote.screenshot — so a "lacks capture" test hides the button from all ~440
undeclared panels in the field. isAndroidDevice() mirrors platformFamily()
with all four signals in order; an Android-test-only helper classified every
Tizen TV as Android, since Tizen registers android_version 'Tizen 6.5'.

Tests: the suite could not see any of this. Mutation testing showed deleting
either capability line, or reverting isAndroidDevice to its buggy form, left
all tests green. Added an update-invariant test (declared set vs baseline,
with an argued exception list), a test that executes the shipped helper
rather than the harness stub, and a legacy-panel test using the shape the API
actually returns instead of one it never produces. All four mutations now
fail.

Verified on a real Android 16 device across all three tiers: Tier 0 captures
live video (no accessibility, no MediaProjection, no owner), Tier 1 gains
display.power via accessibility, Tier 2 declares system.device_owner and every
Tier-2 command delivers. An in-place upgrade from the pre-change build lost
nothing and gained exactly these three.

Baselines deliberately NOT moved — a baseline entry moves in the release
AFTER the one carrying the player fix, once it has reached displays.

Parity gaps 3 and 4 were implemented, audited and reverted; docs/player-parity.md
records why so the next attempt starts from the traps. Gap 3 (wiring "Force
update") meets an unbounded synchronous download against a 120s watchdog and a
3-attempt counter with no version binding, so three presses refuse a panel every
future version. Gap 4 (deferring to BS.capabilities()) removes working
screenshot/stream from diskless BrightSigns that capture to RAM, over-declares
transitions, and rides a probe timeout that discards a late answer permanently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-10 10:31:33 -05:00
ScreenTinker 0082191f9b Show only the controls a display can actually honour
Every device control was offered to every display. A browser tab was shown
"Reboot device", a Tizen TV was shown screen power, a player with no
framebuffer read was shown a live view that stayed black. They all looked
like working buttons and did nothing — the "reports success and changes
nothing" shape that keeps costing people days.

Players now declare what they can do at registration, because only the
player knows at runtime: an Android panel gains real screenshots when
accessibility is switched on and loses Tier-2 when device owner is revoked.
The dashboard hides what is not supported rather than disabling it, and the
Info tab lists the capability set so a missing control is explainable.

The declaration is three-state and the middle state is load bearing: NULL
means "has never told us anything" and falls back to a per-platform
baseline, because several hundred displays in the field will not update
before this deploys and blanking their controls would be a far worse bug.
An empty array means "I genuinely can do nothing" and is honoured.

Hiding a button is not enforcement, so unsupported commands are also
refused server-side — the socket is reachable directly and a stale tab
still renders the old controls. Group sends report skipped devices
separately from sent ones; counting an unreachable member as "sent" is how
an operator walks away believing the whole group rebooted.

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