Commit graph

3 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 538f4a7b03 test(api): close #92 follow-up coverage gaps
The non-security gaps named in the public-API self-review:
- gap-fix: zone_id (playlist items) + layout_id (device PUT) accepted and returned on read,
  INCLUDING the cross-tenant rejection (the is_template OR workspace_id guard - the
  security-relevant one).
- docs serving: /openapi.yaml serves the spec, /docs returns the Redoc page.
- i18n drift-guard: apitoken.* keys have full parity across en/es/fr/de/pt (a key missing
  in one locale fails CI).
- token lifecycle branches: token-create workspace-membership validation and last_used_at
  stamping (integration), plus the must_change_password gate (unit test via the in-memory
  DB injection - cross-process WAL visibility is unreliable for that branch in-process).

119 tests total (was 108), all in the existing node --test job.

Closes #92

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:10:36 -05:00
ScreenTinker 2ad9f54b8e test(api): token partition + threat-model + device WS coverage
A dedicated public-API suite (boots the real server as a subprocess) so CI green proves
the token layer, not just the pre-existing tests:

- Partition firewall, derived from the SAME config/api-surface.js server.js mounts from:
  every JWT-only router 401s a token; a public-surface snapshot fails if any router is
  added to the token door; known-privileged routers asserted JWT-only.
- Threat model: role-strip gates, workspace-binding both directions (token ignores
  X-Workspace-Id, JWT honors it), the scope ladder, the render bypass, token lifecycle,
  and JWT no-regression.
- Device WS round-trip via socket.io-client (added as a devDep): valid device_token
  registers + receives its playlist; wrong token rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:45:09 -05:00