Reports devices migrated, total playlist items created, videos probed
via ffprobe, and existing schedules. Helps verify the migration ran
correctly on first startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration (database.js): switched from sync execFileSync to async execFile
with promise wrapper, matching the pattern in playlists.js. Probes each
video content item, backfills content.duration_sec, and uses the real
duration in playlist_items. Falls back to the assignment's original
duration_sec if the probe fails or content isn't a video.
V1 import (status.js): moved assignment-to-playlist conversion out of the
synchronous db.transaction() so async ffprobe can run. Content files are
already on disk from the transaction, so probing works. Same fallback logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Videos were getting the default 10s from the assignments table. Now ffprobe
runs for each video content item during migration, backfills the content
table, and uses the real duration in playlist_items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users need to see migrated playlists immediately. The toggle still
allows hiding them if desired.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Devices can now switch between playlists via a dropdown in the playlist
section header. Populates from getPlaylists API, shows auto-generated
label and item count. Selection triggers assignPlaylistToDevice and
refreshes the content list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List view: auto-generated playlists hidden by default with toggle checkbox.
Cards show 'auto' badge and display count. Detail view shows display count
in the header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List and detail endpoints now include display_count (devices using this playlist).
New POST /:id/assign endpoint sets a playlist on a device and pushes the update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export now includes playlists, playlist_items, and device.playlist_id.
Format bumped to screentinker-export-v2. Schedules include playlist_id.
V1 import converts old assignments array into per-device playlists inline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Schedule CRUD now includes playlist_id field. List queries join playlist name.
Scheduler tracks active overrides per device and reverts to original
playlist/layout when no schedule is active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
assign-content now adds content to each device's playlist (auto-creating if needed).
New POST /:id/assign-playlist sets a shared playlist on all group devices.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GET /:id now queries playlist_items via device.playlist_id.
DELETE no longer cleans up assignments table (playlist survives device deletion).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All CRUD routes now read/write playlist_items via device.playlist_id.
Auto-creates a playlist for a device on first content add if none exists.
PUT/DELETE push updates to ALL devices sharing the same playlist.
Copy-to-device creates a playlist on the target if needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the assignments-table query with a playlist_items query keyed on
device.playlist_id. Also eliminates the duplicate payload builder in
scheduler.js — it now calls the shared buildPlaylistPayload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs once at startup. For each device with assignments, creates an
auto-generated playlist (is_auto_generated=1) containing those items
and sets device.playlist_id. Skips if any device already has a playlist.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every device will point to exactly one playlist. Schedules can temporarily
override a device's playlist. Auto-generated playlists (from migration) are
flagged so the UI can filter them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap execFileSync to execFile with promise wrapper in
probeAndUpdateDuration(). Wrap the add-item handler in try/catch
for Express 4.x async safety (Express 4 doesn't catch rejected
promises from async handlers).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If a video's duration_sec is NULL in the content table (e.g. ffprobe
wasn't available at upload time), re-probe it when the content is
added to a playlist. Backfills the content table so subsequent adds
skip the probe. Non-video content and probe failures fall back to
the 10s default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When adding a content item to a playlist without an explicit
duration_sec, use the content's own duration (from ffprobe at upload
time) instead of defaulting to 10s. Falls back to 10s for images
or content without a detected duration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
List view: playlist cards with name, description, item count.
Detail view: inline-editable name/description, ordered item list
with thumbnails, duration editing, drag-to-reorder, remove.
Add-item modal: content/widget picker with search and tabs.
All user strings escaped via esc() helper for safe innerHTML.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full CRUD for playlists and playlist items: get, create, update,
delete, add/remove/reorder items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Import, nav highlight for #/playlists and #/playlists/:id, route
handler delegating to playlists view module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses list icon (lines with dots). Positioned content-adjacent since
playlists are collections of content items.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 of playlist refactor: standalone playlist entities with ordered
items. No changes to existing tables or display behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add requireGroupOwnership middleware to all group endpoints
- Whitelist allowed command types (screen_on/off, launch, update, reboot, shutdown)
- Validate color format as #RRGGBB
- Escape all user-controlled strings (device/group names, emails) in dashboard HTML
- Restrict trust proxy to first hop only (prevents IP spoofing + rate limit bypass)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dashboard now organizes devices by group with colored section headers
- Group command endpoint (POST /groups/:id/command) sends to all members
- Manage modal with multi-group confirmation prompt
- Destructive commands (reboot/shutdown) require confirmation
- Ungrouped devices shown separately at bottom
- trust proxy + X-Forwarded-For for real client IPs behind Nginx
- Hide Android-only telemetry (battery/storage/RAM/CPU/WiFi) for web players
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- 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>
Player download links now appear directly in the Add Display modal below
the pairing form, so new users can find and install a player app without
hunting through Settings. Removed the duplicate downloads section from
the Settings page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- 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>
- Filter out devices with no user_id from the main device listing
- Add GET /api/devices/unassigned endpoint (admin only) for unclaimed devices
- Add "Updating" section to README with upgrade instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>