Commit graph

15 commits

Author SHA1 Message Date
ScreenTinker 7eab9c6092 PiP overlay MVP: push image/web overlays to a device or group (#109)
Implements the #109 MVP from docs proposal: a floating overlay PUSHED to a device or
group in real time, rendered above the playlist without disturbing it. Scope is the
MVP only — video/RTSP, MQTT, offline-queue, and the priority/stacking system are
deferred to follow-up PRs as the proposal specifies.

Protocol (/device socket, player-agnostic):
- device:pip-show { pip_id, type:image|web, uri, position, width, height, duration,
  title?, title_color?, background_color?, opacity?, border_radius?, close_button? }
- device:pip-clear { pip_id? }
The player fetches uri itself (same trust model as remote_url content; server never
proxies). type:web is full-trust by design, hence the 'full' token scope.

Server (server/routes/pip.js, new; mounted in config/api-surface.js PUBLIC_ROUTERS):
- POST /api/pip and POST /api/pip/clear + DELETE /api/pip, all requireScope('full').
- Resolves device_id to a device OR a group, expands a group to members, and emits
  per-device — reusing the group command route's room-size online check and
  {device_id, name, status: sent|offline} result shape. Generates pip_id.
- Validates type/position allowlists, uri http(s), numeric bounds on
  width/height/duration/opacity/border_radius, colors via the existing VALID_COLOR
  (#RRGGBB; transparency is the separate opacity field).
- Workspace-isolated: every target query is scoped to req.workspaceId, so a token
  bound to workspace A can't address workspace B (404). Offline devices are reported,
  never queued (PiP is ephemeral).

Player overlay layer (Tizen; tizen/js/pip-overlay.js, new):
- A #pip sibling ABOVE #stage that PlaylistPlayer/ZoneRenderer never touch.
- applyOrientation now applies the SAME transform to #pip as #stage, so corner
  positions track the visible CONTENT in all four orientations.
- image -> <img>, web -> <iframe> (muted by default: empty allow= denies autoplay),
  sized/positioned/styled per payload, optional title bar.
- Single overlay slot, last-show-wins; duration timer (0 = until cleared); pip-clear
  (id-aware) or timer tears down; teardown wrapped so a malformed payload can't wedge
  the layer. Reports show/clear over device:log (tag 'pip').

Dashboard: a minimal "Send overlay" / "Clear overlay" tester on the device-detail
controls (device/group via the open device, type, uri, position, duration), calling
POST /api/pip through the api helper.

Tests (server suite green, 161/161):
- api.test.js: PiP tier — authz (read/write 403, full passes), workspace isolation
  (wsA token -> wsB device 404), payload validation, device + group targeting, clear;
  plus the PUBLIC_ROUTERS snapshot-firewall updated for /api/pip.
- pip-overlay.test.js: loads the real player.js + pip-overlay.js in a vm with a DOM
  shim; proves the overlay shows, auto-dismisses on the duration timer, and never
  changes the playlist signature / touches #stage; web->iframe, last-show-wins,
  id-aware clear, malformed-payload safety.

Not in this PR (intentional):
- Android player overlay — fast-follow. Protocol + server are player-agnostic; the
  Android layer (an overlay View above the player, orientation-matched to MainActivity's
  rootView rotation) is the same shape and lands next.
- OpenAPI docs for POST /api/pip — the contract test's scope heuristic only treats
  'command' paths as full-scope, so documenting a full-scope non-command route there
  needs that heuristic extended first; deferred with the docs item (proposal §8.6).
- video/rtsp types, MQTT, offline queue-on-reconnect, priority/stacking, arbitrary
  (x,y)/selector positioning (proposal §6).

Refs #109

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:42:32 -05:00
ScreenTinker e2ff8f47b7 Tizen player: real Samsung B2B fleet control (#125), folding in #126
#123 already shipped a placeholder device:command handler (#121/#122): screen_off
was a black overlay, reboot/shutdown a toast, update a "re-install" toast. This
replaces that with the real control surface from #125, reconciled into the single
handler #123 introduced (rather than landing a second, competing handler).

- NEW tizen/js/device-control.js: window.STDeviceControl = { run, capabilities,
  backend }. Feature-detects webapis.systemcontrol.* (Tizen 6.5/7, sync/throws) then
  b2bapis.b2bcontrol.* (SSSP/Tizen 4, async), normalises both to Promises, re-probes
  each call. run() never rejects; resolves { ok, supported, action, note, reload }.
  Panel power: setPanelMute (mute ON = backlight OFF) -> setDisplayPanel/setPanelStatus
  fallback. reboot -> rebootDevice(); shutdown mutes the panel and notes SSSP has no
  true power-off; update/reload -> reload:true.
- tizen/js/app.js: device:command now calls STDeviceControl.run and reports the
  outcome via reportCmd (device:log tag=command -> dashboard:device-log, plus a
  structured device:command-result), reloading ~1.2s later on result.reload. screen_off
  falls back to the existing black overlay (showScreenOff) when no B2B surface exists;
  screen_on/launch still clear the overlay + keepAwake. Dropped the now-dead
  tryPowerControl. reportCapabilities() runs on device:registered so the dashboard sees
  the backend ("none" on web/URL-Launcher/consumer TV).
- tizen/config.xml: partner-level b2bcontrol + systemcontrol privileges (ignored, not
  fatal, on unsigned/URL-Launcher/web/consumer builds).
- tizen/index.html: load $WEBAPIS/webapis.js + $B2BAPIS/b2bapis.js before the app
  scripts (404 harmlessly off-hardware) and device-control.js before app.js.
- tizen/README.md: rewrote the remote-control table for real B2B control + a
  partner-signing caveat; added device-control.js to the file list.

Supersedes PR #126 (feat/tizen-device-command-125), which targeted main unaware that
this branch already had a device:command handler.

Verified: node --check on both JS files; config.xml well-formed (xmllint). Not yet
validated on a real SSSP panel — the control surface only takes effect on a
partner-signed .wgt (backend reports "none" on the dev/URL-Launcher build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:28:08 -05:00
ScreenTinker 9c4b48800f Tizen player 1.9.1-beta3: bug fixes, multi-zone layouts, video walls
Brings the Tizen TV player to parity with the other players: closes the five
Tizen issues Bold Media Group filed (#118-#122) and adds the two larger renderer
features it was still missing.

Fixes (#118-#122)
- #118 Sticky "Not authenticated" banner. On TV sleep/wake the socket reconnects
  and a heartbeat could land on the fresh, not-yet-registered socket; the server
  rejected it and the old handler painted a permanent banner AND dropped the saved
  credentials, forcing a re-pair. Heartbeats are now gated on a per-connection
  authenticated flag (true only between device:registered and disconnect/auth-error),
  the heartbeat stops on connect/disconnect/auth-error, the banner clears on
  device:registered, and the auth-error toast is non-sticky.
- #119 app_version stuck at 1.0.0. Resolved at runtime from config.xml via the Tizen
  application API, with a fallback constant that build-wgt.sh stamps from config.xml.
- #121 Remote commands. Added a device:command handler (refresh/launch/screen_on/
  screen_off; honest no-op toasts for update/reboot/shutdown, which need B2B/MDM
  privileges a sideloaded app lacks). Removed the dead device:reload listener.
- #120 Dashboard preview. Added device:screenshot-request + remote-start/remote-stop.
  Images capture; video/YouTube fall back to a status card (TV hardware video plane
  and cross-origin iframes can't be read into a canvas).
- #122 Updates/boot. Documented the real paths (re-sideload or URL Launcher/MDM
  refresh; display-level kiosk/boot settings) since a sideloaded .wgt has no in-app
  OTA or config.xml autostart.

Multi-zone layouts (Android parity)
- New ZoneRenderer ports the Android ZoneManager: zones positioned by percent
  geometry with z_index/fit_mode/background, assignments grouped by zone_id
  (unassigned content goes to the first zone), each zone rotating independently with
  the same per-item schedule gating (#74/#75). app.js selects the renderer from
  payload.layout; single-zone playback is unchanged.

Video walls (web-player parity; Android has none)
- New WallController mirrors the web player: when payload.wall_config is present the
  stage is positioned (vw/vh) as this screen's slice of the wall. The leader plays
  normally and broadcasts wall:sync at 4Hz; followers hold the leader's item, align
  index, and lock their video to the leader's clock with a latency-compensated drift
  controller (hard-seek past 0.3s, gentle +/-3% playbackRate nudge past 0.05s), and
  request an immediate position on (re)connect via wall:sync-request. Per-tile
  rotation is not applied yet (matches the web player). Wall emits are gated on
  auth + connection so a pre-register tick can't trip device:auth-error.

Not ported: video-wall per-tile rotation, plus the minor Android-only reporting
events (device:playback-state, device:log) and the N/A offline-cache events
(device:content-ack/content-delete). None affect on-screen playback.

Verified: JS syntax + headless unit tests of zone grouping/geometry and wall
leader/follower + drift logic. NOT yet validated on Tizen hardware - multi-screen
video sync in particular needs a real wall to tune.
2026-06-18 13:28:08 -05:00
ScreenTinker 1f794ff7b4 chore(release): v1.9.1-beta1
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
2026-06-12 22:37:50 -05:00
ScreenTinker 4b688fcfb1 chore(release): v1.9.0
Some checks are pending
CI / Unit tests (node --test) (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
2026-06-11 21:11:31 -05:00
ScreenTinker 2ccf3264a9 feat(scheduling): per-item schedule blocks (#74 dayparting, #75 auto-expire)
Some checks are pending
CI / Unit tests (node --test) (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
Each playlist item can carry schedule blocks (active days, start/end
time-of-day, optional start/end dates). An item plays when the screen's
local "now" matches at least one block; an item with no blocks always
plays. #74 covers time-of-day/day-of-week windows including overnight
wrap; #75 covers inclusive date ranges (auto-expiry). Evaluation is
on-device, so dayparting and expiry work offline.

- Shared evaluator contract: shared/schedule-vectors.json (39 vectors —
  DST US+AU, overnight-wrap anchoring, timezone correctness, date
  boundaries). Canonical JS evaluator in server/lib/schedule-eval.js;
  Kotlin and Tizen ports kept in lockstep by drift guards (Tizen byte-diff
  test, Kotlin JUnit reads the shared JSON, new android-test CI job).
- All three players (web, Android, Tizen) filter by schedule against their
  own clock, idle with a "Nothing scheduled" message + 30s re-check when
  everything is filtered, and fail open on any evaluator error.
- Editor: per-item schedule modal + row badge in the playlist editor;
  client validation mirrors the server; editing marks the playlist draft.
- Part B (behaviour change): device/group schedule overrides now evaluate
  in each device's effective timezone instead of server-local time.
- Device detail shows the reported timezone + a clock-skew warning.
- i18n for en/es/fr/de/pt across all new strings (namespaced itemsched.*
  to avoid colliding with the device-schedule calendar's schedule.*).
- CHANGELOG documents the feature, the Part B change, the fail-open
  guarantee, and the scheduled-single-video re-render tradeoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:46:41 -05:00
ScreenTinker e8a318e5fb chore(release): v1.8.3
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
2026-06-11 09:38:42 -05:00
ScreenTinker 53e32d31e2 fix(bump-version): do not rewrite the tizen config.xml XML declaration (#77)
bump-version.sh matched `<?xml version="1.0" ...?>` - the XML format version,
which has a leading space before version= just like the widget attribute - and
rewrote it to the app version, producing invalid XML that breaks the Tizen .wgt
build: 'XML version "X.Y.Z" is not supported, only XML 1.0 is supported'. (CI did
not catch it because the no-Tizen-CLI build path just zips the files without
validating the XML.)

- bump-version.sh: skip the `<?xml` declaration line in the tizen version sed.
- tizen/config.xml: restore the declaration to version="1.0" (prior bumps had
  corrupted it to 1.8.2).

The widget version and tizen:application required_version are still updated /
left alone correctly (verified with a dummy bump + an XML parse).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:56:59 -05:00
ScreenTinker 3545830ea6 chore(release): v1.8.2 2026-06-11 08:42:57 -05:00
ScreenTinker e9c89343d7 chore(release): v1.8.1 2026-06-10 14:12:47 -05:00
ScreenTinker 5530d6cfcd docs: tag-based upgrade flow + upgrade.sh, Tizen install paths
- scripts/upgrade.sh: upgrade a self-hosted instance to a tagged release
  (default latest). Backs up the db (.backup), checks out the tag, npm ci
  --omit=dev, restarts the service (SERVICE_NAME override), reports the version.
- README: replace the git-pull update flow with scripts/upgrade.sh (latest or a
  pinned tag); keep main as the bleeding-edge option. Add a Samsung Tizen entry
  to device setup (URL Launcher -> /player).
- tizen/README: point path A at the server's built-in /player, and explain why
  the released .wgt is unsigned (Samsung distributor certs are DUID-locked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:12:29 -05:00
ScreenTinker 4f56199bc7 chore(release): v1.8.0 2026-06-10 13:46:17 -05:00
ScreenTinker dfc8a4e358 feat(player): software orientation (portrait + flipped) on both players (1.7.12)
The dashboard exposes landscape / portrait / landscape-flipped / portrait-flipped
and the README promises rotation, but neither player ever read the device's
orientation field - it was hardcoded landscape. Reported by a customer testing
Firestick + Samsung signage.

Rotate the CONTENT in software, not the panel: Fire TV / Android TV / Tizen are
fixed-landscape and ignore setRequestedOrientation (can't physically rotate).
- Android (MainActivity): applyOrientation() resizes rootView to the rotated
  dimensions, recenters, and rotates 0/90/180/270. rootView is the shared
  container for single-zone AND multi-zone, so both are covered. Driven from the
  playlist-update payload.
- Tizen (app.js): CSS transform on the stage (rotate + swapped 100vh/100vw),
  same four values, from the playlist payload.

Verified on an Android 16 emulator: device set to portrait -> 'Applied
orientation: portrait (rotation=90, swap=true)' and the video renders rotated.
2026-06-09 21:43:08 -05:00
ScreenTinker 5396cf9896 chore(tizen): dev-signing setup + support@screentinker.net author email
- config.xml author email -> support@screentinker.net
- build-wgt.sh: stage app files only before signing (keeps README/build script
  out of the .wgt), auto-add the Tizen CLI to PATH if installed.
- README: document the configured 'ScreenTinker' signing profile (self-signed
  author + default Tizen distributor) — installs on dev-mode TVs / emulator;
  production retail needs a Samsung distributor cert.

Signed .wgt + the author cert are not committed (build artifact / secret).
2026-06-09 19:10:28 -05:00
ScreenTinker 0cfa09046c feat(tizen): Samsung Tizen TV web player (.wgt)
Ports the ScreenTinker player to a Tizen TV / signage web app, speaking the
SAME /device socket.io protocol as the Android player — no server changes; a
Tizen display pairs from the same dashboard.

- app.js: device protocol client — register (pairing_code | device_id+token),
  device:registered/paired/unpaired/playlist-update, 15s heartbeat, keep-awake.
  Always reaches the server prompt until the display is actually paired; a
  saved-but-unreachable server falls back to the prompt (no blank screen); BACK
  returns to it.
- player.js: fullscreen single-zone renderer — image (duration timer), video
  (play-to-end + loop), YouTube (iframe embed), widget (iframe render endpoint).
- config.xml: Tizen TV manifest; build-wgt.sh packages (signs if Tizen CLI
  present, else unsigned); README covers URL-Launcher and signed-.wgt deploy.

Validated: headless protocol test vs the live server passed end-to-end
(register -> pair -> reconnect-auth -> playlist(2) -> content 200); loads +
renders in Chromium with no JS errors.

Not yet ported (fullscreen single-zone covers most signage): multi-zone, video
walls, screenshots, remote control, self-OTA. .wgt is a build artifact (gitignored).
2026-06-09 19:01:58 -05:00