When DISABLE_REGISTRATION=true (or 1), POST /api/auth/register returns
403 with a clear error. OAuth endpoints (/google, /microsoft) also
refuse to auto-create new accounts — existing OAuth users can still
sign in. First-user setup (empty users table) is always allowed so a
fresh install can still be initialized.
GET /api/auth/config now returns registration_enabled so the login
view can hide the "Create Account" button and the trial banner when
registration is off. Absence of the flag is treated as enabled for
back-compat with older servers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inline editor with:
- Collapsible categories, reorder up/down, delete
- Entries with identifier, name, subtitle, available toggle
- Add/remove with auto-focus on new row
- Empty state prompts first category
- Theme, scroll speed, column count selectors
- Reusable content picker (single/multi-select) against user's image library
- Logo picker + background image picker (multi) via that picker
- Preview button posts unsaved config to /widgets/preview and shows the
returned HTML in a modal iframe (srcdoc + injected <base> so relative
content URLs resolve against our origin)
- Delete confirms with widget name
Also escapes w.name / typeMeta.name / w.id in the widget grid to prevent
stored XSS against admins viewing other users' widgets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-user Account section in Settings with name edit and password
change. Password change requires current password; local auth only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Login view:
- Remove `margin-left: calc(-1 * var(--sidebar-width))` from the
centering wrapper. It was a hack to compensate for the sidebar
offset, but app.js already zeros the app margin on the login
route. On mobile this was pushing the login card ~240px off
the left edge of the viewport.
- Use min-height + padding so the card breathes on short screens.
- Drop inline font-size:11px on the support-token input so the
global .input 16px mobile rule applies (iOS focus-zoom prevention).
app.js:
- Hide the mobile hamburger button on the login route; it has no
function there since the sidebar is already hidden.
Landing page:
- Scope the old blanket `.nav-links { display: none }` to hide only
the section anchors + secondary Sign In button, so the primary
"Start Free Trial" CTA stays visible on mobile.
- Wrap the 5-column Compare table in a horizontal-scroll container
and set min-width:560px so it scrolls instead of overflowing
the page.
- Add min-height:44px to .btn on mobile, tighten section padding
to 16px (from 24px) so content doesn't feel cramped against
the viewport edge.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inline width:NNNpx beats the .modal { width: 95vw } mobile rule due to
specificity. Convert to max-width:NNNpx;width:95vw on the three affected
modals so they cap at their desktop size but still shrink on mobile:
- playlists.js add-item modal (560px)
- device-detail.js assign-playlist modal (650px)
- content-library.js edit-content modal (500px)
Same fix pattern for fixed-width form controls flagged in QA — selects
and inputs change to max-width:NNNpx;width:100% so they keep their
desktop size but shrink to container on mobile:
- admin.js role/plan selects (120/130px)
- teams.js member role + add-device selects (100/200px)
- content-library.js search input + folder filter (250/180px)
- onboarding.js pairing code + display name inputs (240px)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Toast now announces via role="status"/aria-live="polite" by default,
and role="alert"/aria-live="assertive" for errors. Screen readers
previously got nothing when notifications appeared.
- Move playlist-item flex-wrap:wrap from inline style into the
@media (max-width: 768px) block so desktop rows don't wrap controls
when the viewport is intermediate-narrow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds accessible up/down arrow buttons alongside the existing drag-to-
reorder handle on each playlist item. Touch users (and keyboard users)
now have a reliable way to re-order without relying on HTML5 drag-drop,
which is effectively unusable on mobile. First/last items have the
respective arrow disabled.
Uses the same /reorder API the drag handler uses, so behavior stays
consistent. flex-wrap on the item container prevents control overflow
on narrow screens.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Wrap wide tables (admin, settings, reports) in .table-wrap with
min-width on the table so they scroll horizontally on narrow screens
instead of collapsing rows.
- Add global .table-wrap { overflow-x: auto } utility.
- Mobile: add mask-image fade on .tabs right edge to hint scrollability
when tabs overflow; flex-shrink:0 on .tab keeps labels intact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Security fixes:
- Critical: Add ownership checks to assignments PUT/:id and DELETE/:id (IDOR)
- Critical: Add ownership checks to assignments copy-to endpoint for both devices
- High: Validate device ownership when adding to device groups
- High: UUID-validate content ID before LIKE query + scope to owner's playlists
- Low: Handle FK violations gracefully in playlist discard (deleted content/widgets)
- Low: Escape mime_type with esc() in playlist item display (XSS)
Bug fix:
- Device-detail mutation handlers now reload full page to show draft banner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Schema: add status and published_snapshot columns to playlists table.
Migration snapshots all existing playlists as published (idempotent via schema_migrations).
Devices always receive the published_snapshot, not live playlist_items.
Edits from device-detail/groups auto-publish immediately (display updates instantly).
Edits from playlist detail page go to draft (requires explicit publish).
POST /playlists/:id/publish snapshots and pushes to all devices.
POST /playlists/:id/discard reverts playlist_items from published snapshot.
Content deletion scrubs references from all published snapshots.
Frontend: draft badge in playlist list, prominent yellow banner with publish/discard
buttons on playlist detail and device detail pages.
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 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>
- 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>
- 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>
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>