Adds cross-platform foreground-window tracking and exposes it over REST
(/api/foreground) and the existing WebSocket feed.
- foreground_service.py: Windows probe via ctypes (HANDLE-correct argtypes
to avoid 64-bit handle truncation); macOS via AppKit; Linux via Xlib
(Wayland returns unavailable). TTL cache + per-platform fallback.
- browser_url_service.py: when foreground is a recognised browser, extract
the page title from the window title (browser-name suffix stripped) and
surface `is_browser` + `browser_page_title`. Optional UIA-based URL
extraction behind MEDIA_SERVER_BROWSER_UIA env flag (off by default —
Chromium browsers keep their accessibility tree dormant otherwise).
- websocket_manager: poll foreground every 1s inside the existing status
loop, broadcast `foreground` on connect and `foreground_update` on
change. Diff only on user-visible fields to avoid geometry spam.
- WebUI: new editorial card rendered under the monitor list on the
Display tab — process name, window title, fullscreen/minimized/monitor
chips, browser block when applicable, exe path, PID, started-ago,
geometry, platform. 16px inter-section gap matches Settings cadence.
- i18n: 25 new keys added to both en.json and ru.json.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Security
- Default bind 127.0.0.1; first-run bootstrap generates random api_token
and refuses to bind non-loopback without auth unless explicitly opted in
- Path-traversal hardened: BrowserService.validate_path rejects absolute
paths, drive letters, UNC, NUL bytes. /api/browser/{play,metadata,
thumbnail} now require folder_id and a folder-relative path
- Pydantic validators on links: http(s) URLs only, mdi:<slug> icons only
- Scripts/callbacks/links create/update/delete gated by *_management flags
- Strict CSP, X-Frame-Options DENY, Referrer-Policy no-referrer,
X-Content-Type-Options nosniff
- CORS locked to localhost:<port> + 127.0.0.1:<port> by default; configurable
- config.yaml writes atomic (tmp + os.replace) and 0o600 on POSIX
- Subprocesses spawned in their own process group / new session so timeout
kills the whole tree (Windows CREATE_NEW_PROCESS_GROUP, POSIX
start_new_session=True)
- Frontend XSS: monitor name + details escapeHtml'd; power button moved to
delegated data-action handler; remote MDI SVGs parsed and sanitized
(strip script/foreignObject/on*/javascript: hrefs) before innerHTML
- All dynamic URL segments now wrapped in encodeURIComponent
Bugs
- WebSocket reconnect: close previous socket before opening new, clear
ping interval per-socket, clear reconnectTimeout up-front, retry on
online/visibilitychange, try/catch JSON.parse
- Artwork fetch race: AbortController + generation guard
- _broadcast_after_open: initialize status, swallow per-poll errors,
background tasks tracked in a strong-ref set with done-callback cleanup
- Audio analyzer: sticky _unavailable flag prevents infinite start/stop
spin when no loopback device exists; cleared by set_device()
- Volume short-circuit cache invalidated when server reports remote volume
- Browser thumbnail race: per-folder generation counter + isConnected
checks; aborts in-flight fetches on navigation
- Track-skip uses cached title instead of full WinRT status round-trip
Performance
- Linux MPRIS/pactl and /api/display DDC-CI handlers wrapped in
asyncio.to_thread so blocking IO never stalls the event loop
- browse_directory moved off the event loop (SMB shares could freeze it)
- Windows status poll caches one asyncio loop per worker thread via
threading.local instead of new_event_loop/close on every 0.5s tick
- broadcast() serializes JSON once and uses send_text to all clients
- Hourly thumbnail cache cleanup scheduled in lifespan (was never invoked
— cache grew unbounded)
- Progress drag listeners attached only while dragging
Quality
- All asyncio.get_event_loop() in coroutines → get_running_loop()
- ThreadPoolExecutors shut down cleanly during lifespan teardown
- config_manager dedup: 12 near-identical methods collapsed onto generic
_upsert/_delete helpers (~290 lines removed)
- Service worker no longer pass-throughs every fetch
- M3U playlist written via NamedTemporaryFile (no fixed-path symlink
clobber race)
- __version__ now prefers live pyproject.toml in dev checkouts so
pip install -e . users see the source-of-truth version, not the stale
package-metadata version baked in at install time
UI/UX (Studio Reference)
- Green leftover focus rings (rgba(29,185,84,...)) all replaced with
copper accent (rgba(var(--copper-rgb),...))
- Dialogs: square corners, copper top hairline, unified with editorial
chrome
- .browser-item: transparent with copper hover border (was filled card)
- Audio device select uses var(--sans) instead of generic system font
- Mobile container padding tuned for ≤480px screens
- Breadcrumb home is a real <button> with aria-label; aria-current on root
- i18n: filled display.msg.power_*, execution.*, scripts.params.execute,
callbacks.empty in both en + ru
Backend (routes/display.py, services/display_service.py):
- Probe DDC/CI capabilities per monitor at enumeration time
- New endpoints POST /api/display/{contrast,input_source,color_preset,picture_mode}/{id}
- Picture mode goes through raw VCP 0xDC since monitorcontrol has no
high-level wrapper; labels follow MCCS spec with vendor-friendly fallbacks
- Each capability reports a *_supported flag so the UI can hide rows that
the hardware does not advertise
Frontend (links.js, app.js, styles.css, locales):
- Monitor cards grow a contrast slider (same editorial copper treatment
as brightness) and a "PICTURE TUNING" section beneath
- Picture tuning uses the IconSelect widget (matching the audio device
selector): per-port icons (HDMI, DisplayPort, DVI, VGA, USB-C),
thermometer for color temps, per-mode icons (movie reel, gamepad,
ball, etc.) for picture modes
- Humanizers turn SHOUT_CASE enum names into readable labels
(COLOR_TEMP_6500K -> "6500 K", HDMI1 -> "HDMI 1")
- 14 new i18n keys per locale (en/ru)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move colophon (credit/email/source link) from sticky footer into
a dedicated About dialog, opened from a new header button
- Drop ~64px of bottom container padding now that the footer is gone
- Loosen vinyl-stage aspect-ratio (1:1 -> 1:0.85) so the disc no
longer leaves a tall empty band below the sleeve
- Switch tonearm height: 36% to aspect-ratio: 1 to keep proportions
consistent across the new stage ratio
- Add about.* / dialog.close i18n keys for EN and RU
- Add vinyl-variants-mockup.html as next design reference target
Toggleable theater-scale player view that takes over the viewport
and amplifies the existing Studio Reference aesthetic — same fonts,
same copper/ink palette, just dialed up for immersive listening.
Layout & typography:
- Two-column centerfold: massive vinyl stage left (clamp(., 72vh, 720px)),
editorial column right with Fraunces italic title at clamp(48px, 6.4vw,
112px), Geist Mono console-style metadata strip, oversized timecodes,
full-width amplitude spectrum.
- Mobile / portrait flips to vertical theater (vinyl top, masthead+
transport below) at <=900px or any portrait orientation.
Ambient bloom:
- Duplicate of #album-art rendered behind everything at blur(110px)
saturate(1.6) opacity(0.42) — paints the room in the record's color.
Slow 28s drift animation. Light-theme variant at lower opacity.
- MutationObserver keeps bloom art in sync as tracks change.
- Vignette + edge darkening + subtle paper-grain veil frame the stage.
Interaction:
- Header button (corner-arrows-out icon) toggles; pressing 'F' anywhere
outside text inputs also toggles; ESC exits.
- Native Fullscreen API requested as best-effort sugar on top of the
CSS overlay (works on TV / tablet); CSS overlay alone covers the
CSS-only fallback case (iOS Safari, embedded webviews).
- fullscreenchange listener mirrors OS-level exit back into the overlay.
- Auto-hide chrome + cursor after 2.5s idle, restored on mousemove.
- Focus moves to play/pause on enter; restored to invoking element on
exit.
- Hides mini-player, tab bar, header, folio marks, and other tabs while
active.
Motion:
- 320ms fade-in for the stage, 600ms vinyl rise, 1.4s bloom-in,
staggered 80ms ladder for kicker -> title -> byline -> album -> meta
-> spectrum -> transport. prefers-reduced-motion disables all.
i18n:
- player.fullscreen / player.fullscreen.exit / player.fullscreen.exit_short
added to en.json and ru.json.
Files: index.html (header button + fs-chrome strip + fs-bloom layer),
styles.css (~360-line fullscreen block at end, scoped to body.is-
fullscreen-player), player.js (toggle + init + idle/key/observer
plumbing, ~170 lines), app.js (import + window export + init call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Meta-grid reduced from 4 cells to 2 (State / Source). Elapsed and
Length were duplicates of the timecodes already flanking the
timeline. Added .meta-grid-2 modifier with a 2-column layout.
- Timeline was distorted by legacy rules: .progress-bar:hover did
scaleY(1.4) (inflating the 2px hairline on hover) and the
progress-fill::after handle defaulted to scale(0). Both are now
forced via !important to keep the hairline flat and the copper
handle always visible.
- progress-row uses minmax(0, 1fr) for the bar cell so it shrinks
cleanly inside the grid.
- Removed unused meta.elapsed / meta.length keys from en.json + ru.json.
- Dead JS lookups for #meta-elapsed / #meta-length stay (cheap if-checks,
no-op when DOM elements gone).
Side-by-side comparison surfaced several layout regressions vs. the
mockup. This commit lands all of them at once.
Header
- Restore centered "Media Server / Studio Reference Edition" wordmark
in italic Fraunces
- Move folio marks to fixed page corners (visible on every tab):
TL = green pulse + "Connected · Local 8765"
TR = "Vol. I — Studio Reference · v0.x.x"
- Replace boxed version-label badge with copper mono inline in folio.tr
- Reduce header-to-content gap (container padding-top 28→56 with the
folio now anchored above)
Player view
- Spectrum bars: smaller height (32px), centered with max-width so
they don't span the whole right column
- Spectrogram canvas: hidden by default (opacity 0); reveals only when
visualizer toggle is active. No more leaking into bottom-left.
- VU cluster volume controls: strip legacy box (background, padding,
border-radius); compact stacked layout with thin slider, small mute
button, mono "VOL · XX%" readout
- Disable legacy applyVinylMode() — the .vinyl class added a SECOND
rotation animation on top of the structural .vinyl-stage spin,
causing visible compounding. Vinyl is now purely structural.
Toggles
- Remove vinyl mode toggle button (vinyl is always on)
- Keep audio visualizer (spectrum vis) toggle — still shown by JS
when supported
Mini player
- Force always-visible on non-player tabs regardless of scroll, by
short-circuiting setMiniPlayerVisible when activeTab !== 'player'
i18n
- New keys: header.connected, header.volume, header.edition,
header.edition_sub
- Removed unused: player.folio_left, player.folio_right
- en.json + ru.json updated
Restructures the player tab DOM to actually look like the editorial
mockup, not just inherit new fonts. The previous commit only swapped
tokens & typography on the legacy Spotify-clone layout.
DOM additions (all preserve existing JS-touched IDs):
- Vinyl stage: rotating vinyl wrapping the existing #album-art as a
circular center label; spins only when state=playing via CSS hook
- SVG tonearm: pivots in/out based on data-playstate
- Kicker line: copper italic mono header above the track title
- Editorial 4-cell metadata grid: State / Source / Elapsed / Length
- Decorative spectrum bars (30, CSS-only animation, paused when idle)
- VU meter cluster: needle visual driven by volume %, alongside the
preserved volume slider for a11y
- Folio marks: top-left and top-right of the player container
JS hooks (small, additive):
- updatePlaybackState now sets :root[data-playstate] for CSS
- progress tick mirrors timecode into meta-grid cells
- volume update rotates the VU needle
- folio-version mirrors the version label
i18n:
- new keys: player.kicker, player.modes, player.folio_*, meta.*
- added to both en.json and ru.json
Restored: media_server/static/redesign-mockup.html (Studio Reference
visual reference; deleting it in the prior commit was a mistake).
Replaces the Spotify-clone dark theme with a warm editorial design
language inspired by hi-fi audio mastering and magazine layouts.
- Self-host Fraunces, Geist, and Geist Mono as variable WOFF2 files
(Latin + Latin-ext + Cyrillic subsets, OFL-licensed)
- New design tokens: warm charcoal + copper accent (dark) /
cream paper + hunter emerald (light)
- Editorial typography: Fraunces serif for display + masthead,
Geist for UI, Geist Mono for technical readouts (timecodes, bitrates)
- Player view restyled as magazine spread with framed album art
- Mini player as glassy console strip with copper hairline glow
- Tabs as italic editorial nav with copper underline
- Browser items as gallery cards with editorial typography
- Settings as numbered sections with refined tables
- Quick Access as console rail
- Dialogs and auth modal as paper cards with mono kickers
- Subtle film-grain overlay for analog warmth
- Localized tab labels: Player → Now Spinning, Browser → Library
- Root folder cards with hero-style layout and SVG icons
- Full-width thumbnails with aspect-ratio grid items
- List view column headers (Name, Bitrate, Duration, Size)
- Modernized breadcrumb with pill segments and overflow handling
- Proper skeleton shimmer replacing emoji hourglass on thumbnails
- Pagination shows "Showing X-Y of Z" item count
- Refined hover effects, animations, and visual hierarchy
- Download button revealed on row hover in list view
- Type badges hidden by default, shown on hover
- Localized new keys in en.json and ru.json
- Abstract ReleaseProvider protocol for platform-agnostic version checking
- GiteaReleaseProvider implementation using stdlib urllib
- UpdateChecker service with periodic background checks and WS broadcast
- Persistent dismissible banner in Web UI when a new version is detected
- Health endpoint now returns cached update info
- Configurable via update_check_enabled and update_check_interval settings
- i18n support (EN/RU)
- WebGL shader background with flowing waves, radial pulse, and frequency ring arcs
- Reacts to captured audio data (frequency bands + bass) when visualizer is active
- Uses page accent color; adapts to dark/light theme via bg-primary blending
- Toggle button in header toolbar, state persisted in localStorage
- Cached uniform locations and color values to avoid per-frame getComputedStyle calls
- i18n support for EN/RU locales
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New audio_analyzer service: loopback capture via soundcard + numpy FFT
- Real-time spectrogram bars below album art with accent color gradient
- Album art and vinyl pulse to bass energy beats
- WebSocket subscriber pattern for opt-in audio data streaming
- Audio device selection in Settings tab with auto-detect fallback
- Optimized FFT pipeline: vectorized cumsum bin grouping, pre-serialized JSON broadcast
- Visualizer config: enabled/fps/bins/device in config.yaml
- Optional deps: soundcard + numpy (graceful degradation if missing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect primary monitor via Windows EnumDisplayMonitors API and show badge
- Expand accent color picker with 9 presets and custom color input
- Auto-generate hover color for custom accent colors
- Re-render accent swatches on locale change for proper i18n
- Replace restart-server.bat with PowerShell restart-server.ps1
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections
- Rename Actions tab to Quick Access showing both scripts and configured links
- Add prev/next buttons to mini (secondary) player
- Add optional description field to links (backend + frontend)
- Add CSS chevron indicators on collapsible settings sections
- Persist section collapse/expand state in localStorage
- Fix race condition in Quick Access rendering with generation counter
- Order settings sections: Scripts, Links, Callbacks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add LinkConfig model and links field to settings
- Add CRUD API endpoints for links (list/create/update/delete)
- Add Links management tab in WebUI with add/edit/delete dialogs
- Add live icon preview in Link and Script dialog forms
- Show MDI icons inline in Quick Actions cards, Scripts table, Links table
- Add broadcast_links_changed WebSocket event for live updates
- Add EN/RU translations for all links management strings
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- New display service with DDC/CI brightness and power control via screen_brightness_control and monitorcontrol
- New /api/display/* endpoints (monitors, brightness, power)
- Display tab in WebUI with per-monitor brightness sliders and power toggle
- EDID resolution parsing to distinguish same-name monitors
- Throttled brightness slider (50ms) matching volume control pattern
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Search bar appears when browsing inside a folder
- Client-side filtering with 200ms debounce
- Clear button and search icon
- Hides at root level, resets on navigation
- Localized placeholder (en/ru)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert stacked sections to tabbed interface (Player, Browser, Actions, Scripts, Callbacks) with localStorage persistence
- Add in-memory directory listing cache (5-min TTL) with nocache bypass for refresh
- Defer stat()/duration calls to paginated items only for faster browse
- Move mini player from top to bottom with footer padding fix
- Always show scrollbar to prevent layout shift between tabs
- Add tab localization keys (en/ru)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add Play All button with M3U playlist generation (local temp file with absolute paths)
- Replace folder combobox with root folder cards and home icon breadcrumb
- Fix compact grid card sizing (64x64 thumbnails, align-items: start)
- Add loading spinner when browsing folders
- Cache browse items to avoid re-fetching on view mode switch
- Remove unused browser-controls CSS
- Add localization keys for Play All and Home (en/ru)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add browser UI with three view modes (grid, compact, list) and pagination
- Add file browsing, thumbnail loading, download, and play endpoints
- Add duration extraction via mutagen for media files
- Single-click plays media or navigates folders, with play overlay on hover
- Add type badges, file size display, and duration metadata
- Add localization keys for browser UI (en/ru)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add version label next to Media Server header (fetched from /api/health)
- Move connection status dot before title, remove status text
- Move logout button into header after language selector
- Return 204 instead of 404 for missing thumbnails (eliminates console errors)
- Show "Title unavailable" when media is playing but title is empty
- Add player.title_unavailable translation key for en/ru locales
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Refactored index.html: Split into separate HTML (309 lines), CSS (908 lines), and JS (1,286 lines) files
- Implemented media browser with folder configuration, recursive navigation, and thumbnail display
- Added metadata extraction using mutagen library (title, artist, album, duration, bitrate, codec)
- Implemented thumbnail generation and caching with SHA256 hash-based keys and LRU eviction
- Added platform-specific file playback (os.startfile on Windows, xdg-open on Linux, open on macOS)
- Implemented path validation security to prevent directory traversal attacks
- Added smooth thumbnail loading with fade-in animation and loading spinner
- Added i18n support for browser (English and Russian)
- Updated dependencies: mutagen>=1.47.0, pillow>=10.0.0
- Added comprehensive media browser documentation to README
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add dirty state tracking for script and callback forms
- Add backdrop click event listeners to detect clicks outside dialogs
- Add unsaved changes confirmation when closing dialogs with modifications
- Reset dirty state when opening dialogs or after successful save
- Add localized confirmation messages (EN/RU) for unsaved changes
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>