- V19 migration: add playlist_current_track_id, playlist_is_playing,
playlist_is_paused, playlist_track_position to channels table
- Channel.java: 4 new fields + getters/setters
- ActivePlaylistState.java: new response DTO with full playback state
- PlaylistService: all command methods now @Transactional and persist state;
new reportPosition() method; getActivePlaylistState() returns the enriched DTO
- PlaylistApiController: GET /active returns ActivePlaylistState;
new POST /{playlistId}/position endpoint for position heartbeats
- renderer.js: on reconnect reads isPlaying/isPaused/currentTrackId/position
and calls _resumeTrack() to seek and optionally pause; 5-second interval
reports currentTime to /position while playing; _xsrfToken() helper
extracted and reused across all playlist POSTs
- playlist.js: loadActivePlaylist seeds playbackState from ActivePlaylistState
so the admin panel shows the correct playing/paused indicator on load
Remove the collapsible toggle and the separate detail card. The panel is
now always-visible at the bottom of the rail (max-height: 40vh, scrollable).
Expanding a playlist shows its controls/tracks inline directly under its
row — no nested card, no second title. Track list caps at 4 visible items
(max-height: 4 * 30px). Playback controls and now-playing label sit in a
single horizontal row alongside the track name.
Add a @RestControllerAdvice that catches NoResourceFoundException and
returns a clean 404 with a single DEBUG log instead of a full stack trace.
Handles browser devtools requests for .js.map files and similar missing
static resources.
Move rename/delete buttons into the list item row (only shown when expanded)
and remove the redundant title/header section from the detail panel, so the
playlist name only appears once regardless of expansion state
- Phantom playlists: stop pushing locally on create; let PLAYLIST_CREATED
STOMP event add the entry to avoid the REST/STOMP race that doubled items
- Row click: attach toggle-expand listener on the li instead of the name
span only; add cursor:pointer to .playlist-list-item
- Input styling: apply surface-3 bg, border, radius, and color to
.playlist-create-row input and .playlist-rename-form input with focus ring
- Play/pause error toast: apiFetch now skips json() when response has no
application/json Content-Type (empty 200 body from play/pause endpoints)
- Admin panel in rail with collapsible playlist section (create, rename,
delete, expand, drag-reorder tracks, playback controls, active selection)
- playlist.js IIFE wired to REST API with CSRF meta-tag injection
- Live events forwarded from console.js via window CustomEvent
- Admin-only now-playing pill overlaid on canvas surface
- playlist-now-playing marketplace script draws rounded pill on canvas
- Fix: add data-asset-type/id/name to asset list items so playlist.js can
populate the track-add dropdown
- Fix: renderer.js _onPlaylistTrackEnded reads XSRF-TOKEN cookie for CSRF
- Fix: playlist.js commandPause no longer passes headers:{} that stripped CSRF
- Fix: PLAYLIST_PREV restarts current track when currentTime >= 3 s
- renderer.js: track playlistState object updated by PLAYLIST_* events
from the STOMP topic; _playTrack() drives a dedicated Audio element
independent of the per-asset loop controller; _onPlaylistTrackEnded()
calls /track-ended to let the server decide next/ended; fetch active
playlist on connect startup; updateScriptWorkerPlaylist() pushes state
to the worker on every change
- script-worker.js: playlist context object added to every script's
context (active, paused, playlistName, trackName, trackIndex,
trackCount); new 'playlist' message handler keeps it in sync;
destructured in the context prelude so scripts can use it directly
Remove eyebrow labels and card-header wrapper divs from all dashboard
cards. Every card now uses a plain h3 + muted description line,
consistent with the two settings cards.
- dashboard-grid uses align-items: stretch so both cards match height
- .card is now a flex column; .control-actions uses margin-top: auto to
pin the save button row to the bottom of each card
- Volume slider label gets grid-column: 1 / -1 via .span-full so it
fills the full card width instead of sharing a column with width/height
- Drop the redundant 'Settings' eyebrow from both Overlay and
Integrations cards; the h3 heading is sufficient
- Change dashboard-grid from auto-fit/minmax to repeat(2, 1fr) so the
two settings cards always split the full width evenly
- Replace large 200x200 icon tiles with compact horizontal nav cards
(icon + title + description, grid-wrapped, responsive)
- Split single Settings card into separate Overlay and Integrations
cards, each with its own Save button and status indicator
- Fix canvas save wiring bug: add missing #save-canvas-btn and
#canvas-status elements that were referenced in JS but absent from HTML
- Remove silent saveCanvasSettings() side-effect from saveScriptSettings()
- Replace window.prompt delete confirmation with inline two-step confirm
flow (Delete button → confirm panel → cancel/confirm)
- Add danger-zone collapsible section with danger-border styling
- Add responsive media queries: topbar stacks on narrow viewports,
nav cards and dashboard grid collapse to single column below 700px
- Remove dead CSS classes (dashboard-action, dashboard-tile,
dashboard-toggle-tile, large-dashboard-tiles, etc.)
- Merge near-duplicate renderAdmins/renderSuggestedAdmins into single
parameterised renderAdminList() helper
- Remove unused addVersionAttributes() call from dashboard route
Fetches NOTIFIED reports on page load and renders an amber warning panel above
the main content. Each notice shows the asset ID, optional resolution note,
date, a link to the admin console, and a dismiss button that transitions the
report to RESOLVED. Panel hides itself when all notices are cleared.
WebSocket subscription refreshes the list live on COPYRIGHT_WARNING messages.
- /report: public three-step page to find a broadcaster, pick an asset, and
submit a DMCA-style claim; uses asset.url fallback for images without previews
- Admin console: flag button opens report modal for the selected asset
- Settings page: sysadmin copyright reports section with status/broadcaster
filters, paginated table, and review modal with action radio buttons
- Footer on index.html links to /report
- Add StringNormalizer.normalize() (trim + toLowerCase ROOT) for username normalization
- Migrate SystemAdministratorService private normalize() to use StringNormalizer.normalize()
- Remove now-unused Locale import from SystemAdministratorService
- Add StringNormalizerTest coverage for new normalize() method
- Add SystemAdministratorServiceTest with 10 unit tests covering all public methods
- Replace JsonSupport inner class (AtomicReference lazy singleton) with simple static final ObjectMapper field
- Replace TODO code-smell comments in TwitchEmoteService, SevenTvEmoteService, and MarketplaceScriptSeedLoader with descriptive Javadoc
- Use normalize() consistently in ChannelDirectoryService.topicFor()
- Extract admin endpoint group (add/remove/list admins, suggestions) into dedicated ChannelAdminApiController
- ChannelApiController reduced from 665 to 520 lines; removes 3 dependency injections
- ChannelDirectoryService: extract resolveOrderForSort() helper from bulk-reorder comparator lambda
- Remove TODO comment from ChannelApiController (partially addressed by this split)
- Add AssetPatch.forOrder() and simplify AssetPatch.fromVisibility() factory
- Replace null-chain AssetPatch constructors in ChannelDirectoryService with forOrder()
- Extract OAuthTokenCipher.buildFromKeys() to deduplicate two fromEnvironment() overloads
- Replace inline upload-size guards in createAsset/createScriptAttachment with enforceUploadLimit()
- Make script attachment content endpoint public for all channels (not just hard-coded 'gasolinebased')
- Extract StringNormalizer.toLowerCaseRoot() utility and use it in ChannelDirectoryService/ChannelSettingsService