Commit graph

20 commits

Author SHA1 Message Date
ScreenTinker a4c85eaabc Remove playerContainer position:relative override that nuked YT iframe
createYoutubeEmbed set container.style.position = 'relative' to anchor
the click-to-unmute overlay. That overrode #playerContainer's
position:fixed/inset:0 — the container fell into normal flow with
zero height (the YT iframe inside has no intrinsic size), so the new
absolute-positioned iframe rendered as 100% of 0 = black screen.

The container is already position:fixed, so absolute children anchor
to it correctly without the override. Removed the line. Bumped SW
cache to v7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:36:39 -05:00
ScreenTinker fb0a7f48dd Force YouTube iframe fullscreen with absolute positioning
The previous CSS fix used 100% width/height but YT.Player can bake in
300x150 fallback pixel dimensions if the placeholder isn't laid out at
construction time. Inline pixel dimensions beat percentage CSS at
equal specificity, so the iframe stayed small.

Use absolute positioning with !important to force fullscreen over
whatever YT set inline. Bumped sw cache to v6 to invalidate the
previously-cached player HTML.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:34:40 -05:00
ScreenTinker ed46011ae4 Pin YouTube iframe to fill the player container
The .zone iframe sizing rule only applies to multi-zone layouts. In
fullscreen single-zone mode the YT IFrame API replaces our placeholder
div with an iframe directly inside #playerContainer, where no CSS rule
sized it — leaving it at the iframe default size (~300x150) and
producing a tiny square in the corner. Added explicit rules so any
iframe child of #playerContainer fills the viewport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:59:23 -05:00
ScreenTinker fb58256b1c Fix YouTube autoplay block from stale localStorage gesture flag
userHasInteracted was initialized from localStorage('rd_audio_unlocked')
on every page load. Browser autoplay policy is per-document, so a flag
from a prior session does not actually grant autoplay rights — but the
player code used it to decide whether to start the YouTube embed muted
(autoplay-able) or unmuted (blocked). Result: kiosks with the flag set
loaded a YT embed with mute=0 that the browser refused to start.

- userHasInteracted now always starts as false. The cold-load tap
  overlay flips it to true on real gesture; the 5s auto-dismiss leaves
  it false and playback stays muted (still allowed).
- unlockAudio() now also calls activeYtPlayer.unMute() so the muted
  embed unmutes immediately when the user finally taps the overlay.
- Removed the now-unused localStorage writes of rd_audio_unlocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:56:49 -05:00
ScreenTinker f951d51214 Always show tap overlay on player cold load
Browser autoplay policy is per-document — a previous session's
localStorage flag does not grant the new page autoplay rights. The
'audio previously unlocked, skipping tap overlay' branch was racing
with YouTube's autoplay block, leaving the player stuck on a paused
embed.

Removed the skip-overlay optimization. The existing 5s auto-dismiss
+ muted-connect fallback still handles unattended kiosks, and a real
user only needs to tap once per cold load to get audio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:54:19 -05:00
ScreenTinker 06ba054898 Fix web player TDZ crash on cached-playlist startup
The cached-playlist restore at the top of the script synchronously calls
playCurrentItem -> renderContent -> createYoutubeEmbed, which references
ytGeneration / activeYtPlayer / ytApiReady / ytApiCallbacks. Those were
declared with `let` further down in the script, so the references hit
the temporal dead zone and threw on every cold start with a YouTube
item in the cached playlist:

  Uncaught ReferenceError: can't access lexical declaration
  'ytGeneration' before initialization

Hoisted the four declarations to the top of the script alongside the
other player state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:51:35 -05:00
ScreenTinker c105a5941e Security: fix IDORs, XSS, rate limits, SSRF validation
HIGH 1 (teams IDOR): POST/DELETE /api/teams/:id/devices now require the
caller to own the device before assigning or detaching it. Without this
check, any team member could pull any device into their team via UUID
guess and gain remote-control access.

HIGH 2 (schedules IDOR): PUT /api/schedules/:id now re-verifies
ownership of every changed target field — device_id, group_id,
content_id, widget_id, layout_id, playlist_id. Previously only the
schedule owner was checked, letting users fire arbitrary content on
victim devices via update.

HIGH 3 (filename XSS): file.originalname captured by multer bypassed
sanitizeBody. New safeFilename() wraps every INSERT path (multipart
upload, remote URL, YouTube). Frontend sinks now go through esc() in
content-library.js, device-detail.js, video-wall.js. Web player gets
an inline escHtml helper for its info overlay where filenames, device
name, and serverUrl land in innerHTML.

HIGH 4 (kiosk public XSS): config.idleTimeout is now coerced via the
existing safeNumber() helper at both interpolation sites. A crafted
value with a newline can no longer escape the JS line comment to
inject arbitrary code into the public render endpoint.

HIGH 5 (folder DoS): POST /api/folders enforces a per-user cap of 100
folders (429 on overflow). Superadmin exempt.

MED 1 (SSRF): ImageLoader.decodeUrl rejects any URL scheme other than
http(s) so a malicious remote_url can't read local files via file://.
On the server, validateRemoteUrl() is extracted and now also runs on
PUT /api/content/:id remote_url updates — previously the SSRF check
only fired on POST.

MED 2 (fingerprint takeover): the WS device:register fingerprint
reclaim path now rejects takeover while the target device is online or
within 24h of its last heartbeat. A leaked fingerprint can no longer
hijack an active display.

MED 3 (npm audit): bumped uuid 9.x -> 14.0.0 (v3/v5/v6 buffer bounds
CVE; we only use v4 so not exploitable, but clears the audit). path-
to-regexp resolved to 0.1.13 via npm audit fix. 0 vulns remaining.

MED 4 (folder admin consistency): ownedFolder() and the content.js
folder_id move check now both treat only superadmin as privileged,
matching GET /api/folders. Previously a plain "admin" could rename
or delete folders they couldn't see, and could move content into
folders they couldn't list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:37:18 -05:00
ScreenTinker 52dd44a3e8 Add group-level scheduling, group playlist assignment, and persist audio unlock
Phase 4 group scheduling: schema migration adds group_id to schedules with
CHECK constraint, scheduler evaluates group+device schedules with priority,
group deletion converts schedules to per-device copies. Dashboard gets
playlist assignment dropdown and current playlist label on group headers.
Player persists audio unlock state in localStorage so version reloads
don't lose audio on unattended displays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 20:22:42 -05:00
ScreenTinker 2104c9cc9f Auto-reload web player when server code changes
Player polls /api/version every 30s and reloads if the hash changes.
Server hash now includes player/index.html and sw.js so player code
updates are detected without requiring a hard refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 22:44:47 -05:00
ScreenTinker ad3095cdf5 Fix player video cycling bug and connecting overlay during cached playback
Clear pending advance timers when switching content items to prevent stale
image/widget duration timers from interrupting video playback. Also skip
showing "Connecting..." overlay when cached playlist is already playing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 22:34:48 -05:00
ScreenTinker d73abc809d Simplify service worker: stop intercepting content requests
The SW was causing "unexpected error" on video/image fetches due to
range request handling, opaque response caching, and stale SW races.

Fix: SW now ONLY caches player page + socket.io JS for offline boot.
Content files are left to browser native HTTP cache (server already
sets Cache-Control: public, max-age=2592000, immutable).

Also: auto-reload player when new SW activates so deploys take effect
immediately without manual hard refresh.

Bumped cache to v5 — activate purges all old caches (including the
broken rd-content-v1 content cache).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 22:24:15 -05:00
ScreenTinker b4ac2fb821 Fix broken service worker + device auth rejection on playlist refresh
Bug 1 (SW): Rewrote service worker fetch handler:
- Skip range requests (video seeking) to avoid caching partial responses
- Skip non-GET requests entirely
- Use ignoreSearch on cache match to avoid query-param misses
- Don't cache opaque cross-origin responses
- Outer catch on Cache API failures
- Don't intercept catch-all requests (let browser handle natively)
- Bump cache version to v4 to purge broken cached responses

Bug 2 (auth): Playlist refresh register was missing device_token,
causing auth rejection every 5 minutes. Fixed by including token
in the refresh-register emit. Added diagnostic logging on both
client and server for token validation failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 22:18:08 -05:00
ScreenTinker dc7450b6a7 Offline resilience: persist playlist cache for cold-start recovery
Web player:
- Cache playlist JSON to localStorage on every update
- Restore and start playing immediately on boot before connecting
- Clear cache on unpair/reset

Android app:
- Cache playlist JSON to EncryptedSharedPreferences on every update
- Restore cached playlist on cold-start, play from disk-cached content
- Update cache on content deletion, clear on unpair

Server (device socket):
- Fingerprint reconnect: issue fresh token instead of rejecting
- Send device:paired on fingerprint recovery for claimed devices
- Add status logging and dashboard notification on fingerprint reconnect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:49:45 -05:00
ScreenTinker 1d253c4cae Android + web player: handle device_token authentication
Follows up on the security audit remediation (afbe113) which added
device_token auth to the WebSocket /device namespace.

Android player (ServerConfig.kt, WebSocketService.kt):
- Persist device_token in EncryptedSharedPreferences alongside device_id
- Send device_token in device:register on reconnect and playlist refresh
- Save/overwrite token from device:registered response (handles legacy
  devices getting their first token)
- Handle device:auth-error by clearing credentials and showing pairing screen
- clearDeviceCredentials() method wipes device_id, device_token, is_paired

Web player (player/index.html):
- Save deviceToken in localStorage config from device:registered response
- Send device_token in register() payload on reconnect
- Handle device:auth-error and device:unpaired events — clear config and
  show re-pair UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:52:52 -05:00
ScreenTinker e7081a579c Fix widget assignments, designer scaling, and cache strategy
- Make assignments.content_id nullable so widgets can be assigned to playlists
- Fix designer publish to use vw units matching preview (was hardcoded px)
- Add px-to-vw conversion in text widget renderer for backward compat
- Fix webpage widget zoom scaling
- Add widget rendering support in fullscreen player mode
- Set no-cache headers on JS/CSS/HTML for instant updates (ETag/304)
- Set 30-day cache on media files and uploaded content for Cloudflare

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 16:25:05 -05:00
ScreenTinker e2879fff58 Instant playlist push, fix YouTube looping, auto-fetch video titles
- Push playlist updates to devices instantly via WebSocket on all
  assignment mutations (add, update, delete, reorder, copy)
- Fix YouTube videos skipping early: remove duration_sec timeout (was
  defaulting to 10s), use generation counter to ignore stale player
  callbacks, disable YouTube loop param for multi-item playlists
- Auto-fetch YouTube video title via oEmbed API when no name provided
- Show actual video duration in M:SS format in playlist instead of
  misleading assignment duration_sec
- Pre-fill server URL from origin on web player setup
- Bump playlist poll interval to 5min (fallback only, push is primary)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:42:41 -05:00
ScreenTinker 8a84923d72 Fix YouTube playback: use IFrame API, fix playlist change detection, network-first caching
- Replace raw iframe YouTube embeds with official YT IFrame Player API for proper
  error handling (150/153/100/101) and unmute support
- Fix playlist not updating when single item changes by comparing full content
  fingerprint (id + url + filepath + filename) instead of just content_id
- Add click-to-unmute overlay for YouTube since iframe swallows click events
- Remove hardcoded origin param from server-side YouTube URLs (caused Error 153
  when player domain differs from server)
- Switch service worker to network-first for player assets so deploys take effect
  without hard refresh; keep cache-first for uploaded content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 14:56:49 -05:00
ScreenTinker af371b9d89 Fix YouTube embed error 153 - add mute, origin, and enablejsapi params
- Add mute=1, enablejsapi=1, and origin params to YouTube embed URLs
- Fix applies at creation time (content route) and playback time (player)
- Existing YouTube content gets fixed params via fixYoutubeUrl() helper
- Also fixes content library preview iframe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 14:25:44 -05:00
ScreenTinker d18045a386 Fix web player single-video loop and service worker cache errors
- Loop single-video playlists natively instead of destroying/recreating the element
- Skip caching HTTP 206 partial responses in service worker (video range requests)
- Bump service worker cache version to invalidate old cache

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 12:58:43 -05:00
ScreenTinker 1594a9d4a4 Initial open source release
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 12:14:53 -05:00