66 Commits

Author SHA1 Message Date
alexei.dolgolyov d131ba461c fix: production-readiness hardening — security, perf, a11y, observability
Lint & Test / test (push) Successful in 20s
Security
- Default scripts_management, callbacks_management, links_management, and
  media_folders_management to False so a leaked token cannot escalate to RCE
  through admin CRUD endpoints.
- TokenSpec + scope hierarchy (read | control | admin); legacy bare-string
  api_tokens entries promote to admin for back-compat. Management endpoints
  now require admin scope.
- WebSocket subprotocol auth (Sec-WebSocket-Protocol: media-server.token.<T>)
  preferred over ?token= query so the token no longer lands in URL/history/
  Referer; query fallback retained for HA integration back-compat.
- Origin allow-list check on the WS endpoint (CSWSH defence).
- In-process token-bucket rate limiter: 5/min for failed auths,
  10/min for /api/scripts/execute and /api/callbacks/execute.
- shell=False subprocess path (shlex.split) + per-parameter regex `pattern`
  in ScriptParameterConfig to harden shell=true scripts against parameter
  injection (Windows cmd.exe env-var expansion).
- CSP gains form-action, worker-src, manifest-src directives.
- Refuse cors_origins=["*"] at startup; strip token=... from uvicorn access
  logs; validate Gitea release tag against strict SemVer regex.
- noopener noreferrer + no-referrer referrerpolicy on every outbound link.
- icacls hardening of config.yaml on Windows (current user + SYSTEM +
  Administrators only); 0600 still enforced on POSIX.
- WS volume handler clamps input and never drops the socket on bad messages.

Performance
- Album-art read in windows_media gated by track key — was decoding the
  WinRT thumbnail twice per second regardless of track changes.
- /api/media/artwork returns content-derived ETag + Cache-Control so the
  browser sends If-None-Match and gets 304s on track repeats.
- Foreground-service ctypes argtypes hoisted to one-time module init
  (was re-declaring ~14 prototypes per probe).
- display_service _static_cache keyed by (edid_hash, ...) tuple with
  eviction of disappeared monitors — fixes stale capabilities on hot-plug
  swaps where the new topology has the same monitor count.
- Visualizer rAF loop paused on document.hidden, resumed on visible.

Reliability / bug fixes
- Lifespan rewritten as try/yield/finally so a partial-startup failure
  cannot orphan background tasks or executors.
- _run_callback in routes/media.py keeps a strong task ref (GC-safe) and
  uses the dedicated callback executor instead of the default pool.
- macos_media.set_volume() no longer always returns True.
- TrayManager._restart_requested initialised in __init__; set before
  signalling exit so the main thread observes it correctly.
- Missing static_dir now logs a WARNING instead of silent UI disable.

UX / accessibility / PWA
- manifest.json theme_color and background_color match the Studio Reference
  base (#0E0D0B); added id and scope for PWA installability.
- ARIA on mini-player icon buttons; inner SVGs marked aria-hidden.
- OS mediaSession API wired so headset / lockscreen / Bluetooth buttons
  drive play/pause/next/prev/seek and show track metadata + artwork.

Observability
- X-Request-ID middleware (accept upstream id if it matches a safe regex,
  otherwise UUID4); request_id_var added to ContextVars and included in
  every log line alongside the token label.
- Audit log (append-only JSONL) for every script + callback execution,
  including the on_play/on_pause/etc. event callbacks. Background-thread
  writer; queue capped; flushed in lifespan teardown.

Deployment
- proxy_headers + forwarded_allow_ips plumbed through Settings →
  uvicorn.Config for reverse-proxy installs.
- HTTPS support via ssl_certfile + ssl_keyfile (+ optional password);
  startup refuses to launch with only one of the pair set.
- Thumbnail cache moved from project-root .cache to
  %LOCALAPPDATA%/media-server/cache (Windows) and
  $XDG_CACHE_HOME/media-server/thumbnails (POSIX).

Tests
- 35 new tests across auth scopes, rate limiter, browser path traversal
  (../ NUL UNC absolute), script-param validation incl. regex, Gitea tag
  whitelist, config atomic write + POSIX perms. 47 passed / 4 skipped.
2026-05-22 22:25:54 +03:00
alexei.dolgolyov e1c8474271 fix(csp): wire display sliders and accent picker without inline on*
Lint & Test / test (push) Successful in 10s
The display brightness/contrast sliders and the accent color picker
rendered dynamic HTML with inline oninput/onchange/onclick attributes,
which are blocked by the script-src 'self' CSP — so display settings
were silently un-clickable from the WebUI.

Replace the inline attributes with data-* markers, then attach proper
event listeners after innerHTML (delegated on the container for the
slider rows, direct for the accent dropdown).
2026-05-19 01:17:47 +03:00
alexei.dolgolyov 61cdce9b60 feat(foreground): track topmost process + browser page title
Lint & Test / test (push) Failing after 8s
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>
2026-05-18 03:11:59 +03:00
alexei.dolgolyov 982dda42ac fix(browser): align list columns via subgrid and fix icon sizing
Lint & Test / test (push) Successful in 10s
- Switch .browser-list to CSS grid + subgrid so header and rows share
  the same column track widths, eliminating misaligned columns when
  content widths differ between rows.
- Apply matching responsive column overrides at the parent grid level.
- Override root-folder SVG sizing (hardcoded 24x24 in browser.js) so it
  fills the 56px icon box instead of rendering at ~43%.
- Make compact grid icon fill its thumb wrapper so the emoji centers
  instead of being stranded in the top-left corner.
- Remove premature isConnected bail in loadThumbnail; the img element
  is intentionally detached when called from renderBrowserGrid/List.
  Post-await checks already handle navigation-away correctly.
2026-05-16 20:12:39 +03:00
alexei.dolgolyov eaeebb64cd fix(csp): replace inline on* handlers with data-on* + JS wiring
Lint & Test / test (push) Successful in 38s
The strict `script-src 'self'` CSP blocks inline onclick/onchange/oninput/
onsubmit attribute evaluation, breaking every button and form in the UI.

- Rename all 53 inline handler attributes in index.html to data-on*
- Add wireInlineHandlers() in app.js that parses each data-on* expression
  on DOMContentLoaded and attaches a proper addEventListener calling the
  matching window-global function. Supports no-arg, string/number/bool/null
  literals, and the `event` token.

CSP stays strict; no unsafe-inline or unsafe-hashes needed.
2026-05-16 18:35:51 +03:00
alexei.dolgolyov bcc6d40ed7 fix: comprehensive security, bug, performance, and UI/UX audit
Lint & Test / test (push) Successful in 20s
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
2026-05-16 13:22:46 +03:00
alexei.dolgolyov 57fdeb70fb feat(displays): expose DDC/CI contrast, input source, color preset, picture mode
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>
2026-05-15 14:28:04 +03:00
alexei.dolgolyov 372e4eb11f fix(displays): keep primary-display star visible on long monitor names
Wrapping overflow:hidden + ellipsis on the parent flex container
clipped the favourite star whenever the monitor name was long enough
to truncate. Move the truncation rules onto a new inner span around
the name text only, and add flex-shrink:0 to the badge so it always
renders in full.
2026-05-01 19:40:12 +03:00
alexei.dolgolyov ec5178142e ui(player): replace footer with About dialog + reclaim dead space
- 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
2026-05-01 11:28:10 +03:00
alexei.dolgolyov f4be2bdb89 fix(player): wire accent picker to editorial copper palette + visual polish
Lint & Test / test (push) Successful in 9s
The accent picker only mutated --accent / --accent-hover, but the redesign
reads everything off --copper, --copper-hi, --copper-lo, --copper-glow.
--accent was a one-way alias of --copper, so picking a color did nothing.

Frontend (player.js):
- applyAccentColor now drives --copper, --copper-hi, --copper-lo, and a
  new --copper-rgb triplet (used by every soft tint / glow on both themes)
- darkenColor / hexToRgbTriple helpers added beside lightenColor

CSS (styles.css):
- introduce --copper-rgb tokens for both themes; --copper-glow now derives
  from rgba(var(--copper-rgb), 0.35) so it follows the picker
- replace 21 hardcoded rgba(224,128,56,...) / rgba(31,78,61,...) literals
  across hover bgs, focus halos, glows, vinyl-label gradients with
  rgba(var(--copper-rgb), ...)
- replace the light-theme vinyl-label gradient hexes with
  var(--copper) / var(--copper-lo)

Other player polish in this changeset:
- track-masthead: padding-right clamp(12px, 1.5vw, 24px) so VU meter,
  spectrum tail, end timecode and controls sit inset from the panel edge
  (zeroed on the single-column mobile breakpoint to keep symmetry)
- VU meter 140→120 px, volume slider 80→64 px to free up row width so
  the cluster stays inline with prev/play/next instead of wrapping
- light-theme VU meter override: cream gauge face, dark-ink scale ticks,
  hunter-emerald needle (replaces the hardcoded black gauge)
- fullscreen meta-cell labels: var(--ink-faint) → var(--copper) so STATE
  and SOURCE read as part of the same editorial system as the kicker
2026-04-25 18:19:19 +03:00
alexei.dolgolyov 51ec1503f4 perf(visualizer): cut spectrum + track-switch CPU significantly
Lint & Test / test (push) Successful in 10s
Frontend hot path (player.js, background.js):
- visualizer rAF: drop per-frame getComputedStyle('--accent') (cached on
  applyAccentColor), build canvas LinearGradient once per accent change
  instead of 32× per frame, batch all bars into a single beginPath/fill
- FPS-gate canvas redraw via frequencyDataVersion so 60-144 Hz monitors
  stop re-rendering identical frames produced at 30 Hz on the backend
- editorial spectrum bars: replace style.height (layout) with
  transform: scaleY (compositor-only); cache bar refs, pre-compute
  per-bar gain/range, dedup writes at 1/1000 quantization
- coalesce VU needle into the visualizer rAF; cache vuNeedle ref;
  dedup angle writes at 0.1°
- updateUI: status-payload fingerprint short-circuits the redundant
  status_update broadcasts that fire during a track change
- swapArtworkSrc: only force layout reflow when keyframe is in flight;
  drop the ?_=Date.now() cache-buster so identical artwork URLs reuse
  the decoded bitmap; mini/glow imgs only re-set src when changed
- drop the fullscreen MutationObserver — fs-bloom-art is mirrored
  directly from the artwork-swap path, eliminating the second blur paint
- updateProgress: skip text writes when the rounded second hasn't moved;
  POSITION_INTERPOLATION_MS 100 → 250
- background.js: lift resizeBackgroundCanvas out of the rAF body, cache
  step, accept new int-scaled wire format

CSS:
- spectrum bars use transform: scaleY(var(--bar-h-scale)) + transition
  on transform; will-change updated to transform
- album-art-glow and fs-bloom-art switched to small-source-blur trick
  (render at 20-25% size, scale 4-6×, lower blur radius) — visually
  equivalent, ~10-25× cheaper repaint on track change
- drop unused transition: filter on .vinyl-stage #album-art

Backend (audio_analyzer.py, websocket_manager.py):
- pre-allocate windowed and cumsum buffers; replace
  np.concatenate(([0.0], np.cumsum(...))) with cumsum[0]=0 +
  np.cumsum(out=cumsum[1:]); float32 hanning window
- RMS via np.dot(mono, mono) — no astype copy, no ** temp
- int16 wire format (scale=1000) — smaller JSON, no Python float boxing
- versioned data + threading.Event so _audio_broadcast_loop is event-
  driven (ev.wait + monotonic seq dedup) instead of polling on a timer
  with the always-false `data is _last_data` identity check

ruff clean, pytest 7 passed / 3 numpy-skipped, esbuild bundle 113.6 kB.
2026-04-25 18:05:57 +03:00
alexei.dolgolyov 4c93bfb8c1 ui(player): soften vinyl-stage halo, transparent-bg album placeholder, crossfade artwork swaps
Lint & Test / test (push) Successful in 1m50s
- vinyl-stage background fades to transparent (matches fullscreen Listening Room) — no more rectangular dark card around the sleeve+disc composition
- album-art placeholder SVG drops the opaque #282828 backdrop in favour of a translucent disc glyph so the sleeve cardstock shows through before the first artwork load
- new swapArtworkSrc helper retriggers the .is-swapping keyframes so artwork changes crossfade instead of popping
2026-04-25 15:22:08 +03:00
alexei.dolgolyov 59840a1190 feat(player): fullscreen "Listening Room" mode
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>
2026-04-25 14:47:53 +03:00
alexei.dolgolyov 2a474ea52c fix(player): redesign cleanup pass — sleeve, tonearm, AGC, dead code
Production-readiness pass before merging the Studio Reference redesign
to master.

Audio (backend):
- Reset AGC `_spectrum_ref` envelope on `start()` so a long silent gap
  between sessions doesn't make the first new transients clip at the
  ceiling. Annotated the trade-off (loud transient lifts reference for
  a few seconds afterwards — the price of real loudness).
- Add `tests/test_audio_analyzer.py` with 10 cases: bin-edge layout,
  AGC attack/release asymmetry, lifecycle reset. Skips numpy-dependent
  cases when numpy isn't installed; CI has it.

Vinyl mode dead code removed:
- The toggle button was dropped during the sleeve refactor but the JS
  state, 2 s `setInterval`, `beforeunload` handler, and `applyVinylMode`
  call (commented out in app.js) all stayed. Now properly excised from
  player.js + app.js + window.* exports.
- Stripped the matching `.album-art-container.vinyl*` CSS block and its
  `vinylSpin` keyframes (~95 LoC).

Sleeve + tonearm fixes:
- Removed the duplicate `.now-playing .vinyl-stage` / `.vinyl-label` /
  `.tonearm` block that was overriding the new `.vinyl-stage` rules by
  source order — the uncommitted tonearm geometry never took effect
  because the stale clone won the cascade.
- Tightened tonearm to 36% × 36% at right:-6%, top:26% so the SVG
  bounding box stays right of the sleeve (sleeve right edge ~68%).
  Needle now lands on the visible disc grooves at both rest and
  playing rotations and never overlaps the cover.
- Removed sleeve `transform: rotate(-2.5deg)` + the matching mobile
  `-1.8deg` override; sleeve now sits flat and squared-off.
- Removed the 1px inset hairline on the sleeve and the 1px outline +
  inset highlight on the album art — cleaner, no semitransparent
  border noise.
- Album art inset 5% to expose a cardstock margin around the print
  (using explicit width/height — `inset` shorthand triggered the CSS
  replaced-element rule that uses the image's intrinsic size and blew
  out the grid track).

Mobile + misc:
- Removed mobile tonearm overrides at 720px and 420px — they were
  calibrated for the pre-sleeve geometry and put the needle back over
  the cover on phones; desktop geometry is proportional and works.
- Added `<meta name="mobile-web-app-capable">` alongside the legacy
  Apple variant to silence the deprecation warning in Chromium.
- Replaced the "PRIMARY" badge on display cards with a copper star
  icon (translation key still drives title + aria-label).
- `.gitattributes` with `* text=auto eol=lf` so Windows checkouts stop
  nagging "LF will be replaced by CRLF".

Annotations:
- "REF · 24" record-label catalogue mark marked as intentional non-i18n
  decoration in index.html.

CI: ruff clean, pytest 7 passed + 3 numpy-skipped (all 10 run on CI).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 14:39:20 +03:00
alexei.dolgolyov b09569f390 fix(vu): drive needle from RMS-dB loudness instead of peak-of-bins
- Backend computes time-domain RMS, maps -60..-6 dB to 0..1, sends as
  `level` alongside the per-frame-normalized frequency bins.
- Frontend prefers `level` directly; drops the peak-of-bins fallback
  and the redundant volume-slider attenuation (loopback capture is
  already post-volume on Windows/macOS).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 12:16:41 +03:00
alexei.dolgolyov f2c82164e8 ui(vu): narrower 44deg swing, peak-based level, faster response; mini progress bar fix
- VU needle swings -22..+22deg instead of -45..+45 for a more realistic VU look
- Switch from RMS to peak frequency reading so the needle catches musical hits
- Faster attack (0.7) and release (0.25) so it swings rather than pinning
- Replace explicit grid lines with subtle repeating-conic-gradient ticks
- Scope mini progress bar styles to .mini-player; taller (3px), clickable
2026-04-25 11:41:32 +03:00
alexei.dolgolyov 968eb156bc fix(player): real audio level on VU; full-width spectrum; hide canvas under vinyl
VU needle reflects actual audio output
- Was just a synthetic wobble bounded by the volume slider value.
  Now reads RMS of the FFT bins (skipping bin 0 / DC) the visualizer
  feeds in, multiplies by current volume, and applies attack/release
  smoothing for analog-feeling ballistics.
- Falls back to the synthetic wobble when audio capture isn't
  running so the needle still looks alive on the static fallback.
- When playback stops, needle settles to the bottom of the swing
  (-45deg) instead of holding the volume position.

Spectrum width — actually fixed this time
- Root cause: CSS repeat() does NOT accept a CSS variable for its
  count argument, so my `repeat(var(--spectrum-bars), 1fr)` rule
  was invalid and silently dropped, leaving the legacy/auto sizing
  behavior. Set grid-template-columns directly from JS to
  `repeat(40, minmax(0, 1fr))`.
- CSS retains a `repeat(40, minmax(0, 1fr))` literal as a default
  so the row renders sane even before JS executes.

Spectrogram canvas under vinyl
- Hidden via display: none — the editorial .spectrum row already
  shows the audio spectrum; the canvas was redundant and ugly.
  Element stays in DOM so the visualizer JS keeps rendering (drives
  album-art bass-pulse + dynamic background bands).
2026-04-25 02:27:56 +03:00
alexei.dolgolyov a0f74dfc39 fix(visualizer): full-width spectrum + device pick auto-starts capture
Spectrum width
- grid-auto-flow: column with implicit columns wasn't reliably
  stretching to fill the parent. Switch to explicit
  grid-template-columns: repeat(var(--spectrum-bars), minmax(0, 1fr))
  with the bar count exposed as a CSS variable from JS so the
  column count and the actual bar count stay in sync.
- !important on display/grid-template-columns/width to defeat any
  legacy descendant rules.

Device selection
- Picking a device in the audio-device dropdown is an explicit
  signal that the user wants capture. Auto-enable the visualizer
  if it isn't already on, then call applyVisualizerMode so the
  WS subscription happens and the badge flips from 'Available' to
  'Active'. Was only doing this when visualizer was already on,
  which is why the user kept seeing 'Available, not capturing'.
2026-04-25 02:24:01 +03:00
alexei.dolgolyov 6066b4a2c5 fix(visualizer): auto-enable actually starts capture; persist audio device
Auto-enable was a no-op
- Writing 'visualizerEnabled'='true' to localStorage from app.js did
  not update the exported `let visualizerEnabled` in player.js. So
  applyVisualizerMode() saw the stale `false` and went into the
  DISABLE branch — leaving the device 'available, not capturing'.
- Add a setVisualizerEnabled() setter exported from player.js and
  call it before applyVisualizerMode() during boot.

Audio device persistence
- Save the selected device name to localStorage on change.
- On loadAudioDevices(), prefer status.current_device (server's
  current state) but fall back to the localStorage value if the
  server doesn't know one (e.g. after a server restart).
- If the saved device wasn't recognized by the server, push it back
  via POST /api/media/visualizer/device so capture lands on it
  immediately. Best-effort; no toast on failure.
2026-04-25 02:17:03 +03:00
alexei.dolgolyov 153424eff8 ui(player): widen spectrum to fill column; swap volume control to left of VU cluster
- Spectrum: switch from flex to grid auto-flow so each bar gets an
  equal-width column slot regardless of bar count. Fewer (40) but
  fatter bars now genuinely span the full grid column.
- Spectrum height bumped to 70px, gap 4px.
- VU cluster: flex-direction: row-reverse so volume controls sit on
  the LEFT, readout in the middle, VU meter on the RIGHT.
- Volume hairline divider switched from border-left to border-right
  so it stays between the volume controls and the readout.
2026-04-25 02:13:22 +03:00
alexei.dolgolyov 336d596b66 fix(ui): full-width spectrum + log-mapped bars; deeper sepia + soft art fade
Spectrum
- Logarithmic frequency-to-bar mapping (squared time so bins
  stretch toward the highs). Per-bar high-end gain ramps from
  1.0x at the lowest bar to 3.0x at the highest, so the right
  half of the spectrum no longer reads as dead air.
- Floor bumped from 6% to 12% so silent bars stay visible.
- Skip bin 0 (DC + sub-rumble) which was overwhelming the lows.
- Use peak (not average) within each band — punchier visual.
- Container height 56→64, gradient now copper-lo → copper →
  copper-hi for more visible top tips. min-width: 0 / box-sizing
  border-box ensures the row truly claims the full grid column.
- Backend FFT path is unchanged: WS audio_data → setFrequencyData
  → renderVisualizerFrame → updateEditorialSpectrum. No
  client-side analyzer added.

Album art (vinyl label)
- Deeper sepia (0.35→0.6) and lower saturate (0.85→0.7) so
  vibrant covers blend into the copper grooves.
- Soft radial mask: outer ~22% of the disc fades toward the
  vinyl black so the album art dissolves into the surface
  rather than terminating at a hard clip edge.
- Hover state pulls the fade inward and eases sepia back so
  the user can still see the real cover at near-natural color.
- Glow tint matches the new sepia depth.
2026-04-25 02:07:20 +03:00
alexei.dolgolyov d937c1590c feat(ui): live VU + audio-driven spectrum, editorial banner, subtler dynamic bg
VU needle (animated)
- Synthetic wobble bounded by current volume runs only while
  state==='playing'. Two combined sines + jitter make it look
  like a real analog needle reacting to peaks.
- Settles back to the static volume-mapped position when paused.

Spectrum (real audio)
- Now driven by the same frequencyData the visualizer canvas
  uses. Each visual bar averages a chunk of frequency bins.
- Spans are now JS-injected (60 bars) instead of hardcoded so
  the bar count is no longer baked in.
- Spectrum spans full width of the masthead column, height
  bumped to 56px for presence.
- CSS animation pauses (sets via `body.audio-spectrum-live`)
  when JS is driving heights so the keyframes don't fight.
- Synthetic CSS animation remains as the fallback when audio
  capture isn't available.

Visualizer auto-enable
- On first install with loopback support, visualizer is
  enabled so the spectrum is alive out of the box.

Dynamic background
- Lower max opacity (1 → 0.45 dark, 0.35 light)
- sepia + saturate filter + hue-rotate keep it palette-aligned
  with the copper editorial tones instead of fighting them
- mix-blend-mode screen (dark) / multiply (light) blends into
  the page background instead of overlaying

Update + connection banners
- Fully restyled: glassy card with copper hairline accent,
  mono uppercase text, copper hairline-border CTA buttons,
  minimal close button. Matches the rest of the editorial
  palette instead of the old solid-green-bar look.
2026-04-25 02:03:15 +03:00
alexei.dolgolyov 77b39e5684 fix(ui): snap player view directly from Studio Reference mockup
Wholesale replacement of the player view markup + a verbatim mockup
CSS block scoped to .now-playing. Previous approach kept restyling
legacy classes which left a layered, inconsistent result. This is a
clean snap: same DOM structure as the mockup, same CSS rules, mapped
onto existing JS-touched IDs.

Markup
- One <section class="now-playing"> with vinyl-stage on left and
  track-masthead on right
- Vinyl spins via data-playstate and contains album art as the
  circular center label (existing #album-art img preserved)
- SVG tonearm pivots in/out by data-playstate
- Track masthead: .kicker (copper italic mono), large italic-serif
  .track-title, italic .track-byline, mono .track-album
- Meta grid: 2 cells (State / Source) with mono labels + italic
  serif values
- 30 .spectrum bars between metadata and transport
- .transport: progress-row (timecode + .progress-track + timecode)
  and .controls (3 .btn-trans buttons + .vu-cluster)
- Volume slider, mute button, visualizer toggle moved to a
  .visually-hidden block — they remain functional for JS / a11y
  but no longer compete for visual real estate. Volume control
  happens via the always-visible mini player.

VU cluster (mockup-faithful)
- 140x60 VU meter with conic-gradient grid background, copper
  needle, "VU" label
- Stacked readout: "OUT <strong>SYS</strong>" / "VOL <strong>72%</strong>"
- Click anywhere on cluster toggles mute (calls toggleMute())
- When muted: needle turns rust, readout strong turns rust,
  OUT label switches to MUTE

JS hooks
- updatePlaybackState already sets :root[data-playstate] (drives
  spin + tonearm)
- Volume tick now updates #vu-vol and #vu-out
- updateMuteIcon updates #vu-out + .vu-cluster.muted class

Scoping
- All new CSS is .now-playing-prefixed so other tabs and dialogs
  are untouched
- Legacy .progress-bar:hover scaleY and ::after scale(0) are
  defeated with !important inside .now-playing
2026-04-25 01:43:11 +03:00
alexei.dolgolyov 265b001b99 fix(ui): close gaps with Studio Reference mockup
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
2026-04-25 01:32:34 +03:00
alexei.dolgolyov 14e9f2294e feat(ui): rebuild player view to match Studio Reference mockup
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).
2026-04-25 01:24:11 +03:00
alexei.dolgolyov ee5184920d fix(visualizer): sync state and re-subscribe from audio device load
- Broaden audio import errors from ImportError to Exception, log at warning
- Move visualizer WS re-subscription into loadAudioDevices() so it runs
  after availability is confirmed from the API
- Show/hide the visualizer toggle button based on fetched availability
2026-04-18 17:48:49 +03:00
alexei.dolgolyov db777fa64b fix: prevent dialog showModal from auto-focusing first input
Lint & Test / test (push) Successful in 1m18s
Patches HTMLDialogElement.prototype.showModal globally to move focus
onto the dialog element itself instead of the first focusable
descendant. On touch devices the previous behavior popped up the
on-screen keyboard whenever a modal opened, which was confusing.
2026-04-07 19:01:42 +03:00
alexei.dolgolyov c50a8f472c fix: make folder status visible with dot + text label
Lint & Test / test (push) Successful in 10s
Status dot was 8x8px with no text, nearly invisible in the table.
Now renders as a colored dot with an adjacent text label
(Available / Unavailable).
2026-03-29 15:07:46 +03:00
alexei.dolgolyov cad6e8a1fe feat: redesign media browser UI
Lint & Test / test (push) Successful in 9s
- 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
2026-03-29 14:59:43 +03:00
alexei.dolgolyov c9ee41ad35 feat: add media folder management from WebUI
Lint & Test / test (push) Successful in 10s
- Add media_folders_management config flag (enabled by default)
- Guard folder CRUD endpoints with 403 when management disabled
- Wire up frontend folder add/edit/delete in Settings tab
- Add per-folder availability check (for network shares)
- Show unavailable badge on offline folders in browser view
- Expose management flag via /api/health endpoint
- Add EN/RU locale keys for folder management UI
2026-03-29 14:44:03 +03:00
alexei.dolgolyov 795a15cb8b feat: add update-available notification system
Lint & Test / test (push) Successful in 10s
- 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)
2026-03-25 11:37:09 +03:00
alexei.dolgolyov 1410a8d2cb feat: typed script parameters with validation and icon-grid selector
Release / create-release (push) Successful in 1s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 36s
Release / build-windows (push) Successful in 1m15s
- Add ScriptParameterConfig model (string, integer, float, boolean, select types)
- Server-side validation at both define-time and execute-time
- Parameters passed as SCRIPT_PARAM_* environment variables
- Web UI parameter editor in script create/edit dialog (add/remove/reorder)
- Icon-grid selector component (ported from wled-screen-controller)
- Replace audio device dropdown with icon-grid selector
- Replace callback event dropdown with icon-grid selector
- Localization for parameter UI (en, ru)
2026-03-25 11:25:03 +03:00
alexei.dolgolyov 4d1bb78c83 feat: make authentication optional — no tokens = no auth
Lint & Test / test (push) Successful in 10s
When no api_tokens are configured (the new default), all endpoints
are accessible without authentication. The frontend detects this
via /api/health's auth_required field and skips the login form.

- Backend: auth.py skips verification when api_tokens is empty
- Frontend: shared getAuthHeaders()/hasCredentials() helpers replace
  scattered token logic across all JS modules
- Health endpoint exposes auth_required for frontend discovery
- config.example.yaml ships with tokens commented out
- CLI --show-token and startup log reflect disabled state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:59:55 +03:00
alexei.dolgolyov 5439af1955 Add CI/CD pipelines, NSIS installer, ES module bundling, and ruff linting
Lint & Test / test (push) Failing after 9s
Release / create-release (push) Successful in 1s
Release / build-windows (push) Successful in 59s
- Add Gitea Actions workflows: test.yml (lint + test on push/PR) and
  release.yml (build + NSIS installer + upload on v* tags)
- Add NSIS installer with optional desktop shortcut and auto-start
- Add esbuild bundler: ES module migration with IIFE bundle output
- Add build-dist-windows.sh for cross-building Windows distribution
- Fix all ruff lint errors (import sorting, unused imports, line length)
- Remove redundant scripts (start-server.bat, stop-server.bat,
  start-server-background.vbs)
- Update CLAUDE.md with CI/CD and release documentation
2026-03-23 02:01:28 +03:00
alexei.dolgolyov be48318212 Add dynamic WebGL background with audio reactivity
- 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>
2026-03-19 01:07:46 +03:00
alexei.dolgolyov 0eca8292cb Fix loopback device status showing 'Unavailable' after change
The POST /visualizer/device response has 'success' but no 'available'
field, causing updateAudioDeviceStatus to always fall to 'Unavailable'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:17:13 +03:00
alexei.dolgolyov 3cfc437599 Add UI animations: dialogs, tabs, settings, browser stagger, banner pulse
- Dialog modals: scale+fade entrance/exit with animated backdrop
- Tab panels: fade-in with subtle slide on switch
- Settings sections: content slide-down on expand
- Browser grid/list items: staggered cascade entrance animation
- Connection banner: slide-in + attention pulse on disconnect
- Accessibility: prefers-reduced-motion disables all animations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:45:02 +03:00
alexei.dolgolyov a20812ec29 Add PWA support: installable standalone app with safe area handling
- Service worker, manifest, and SVG icon for PWA installability
- Root /sw.js route for full-scope service worker registration
- Meta tags: theme-color, apple-mobile-web-app, viewport-fit=cover
- Safe area insets for notched phones (container, mini-player, footer, banner)
- Dynamic theme-color sync on light/dark toggle
- Overscroll prevention and touch-action optimization
- Hide mini-player prev/next buttons on small screens
- Updated README with PWA and new feature documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:17:56 +03:00
alexei.dolgolyov 652f10fc4c Reduce visualizer latency, tighten UI paddings, fix mobile browser toolbar
- Visualizer: FPS 25→30, chunk_size 2048→1024, smoothing 0.65→0.15
- Beat effect: scale 0.03→0.04, glow range 0.5-0.8→0.4-0.8
- UI: reduce container/section paddings from 2rem to 1rem
- Source name: add ellipsis overflow for long names
- Mobile browser toolbar: use flex-wrap instead of column stack,
  hide "Items per page" label text on small screens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:35:23 +03:00
alexei.dolgolyov 92d6709d58 Refactor monolithic app.js into 8 modular files
Split 3803-line app.js into focused modules:
- core.js: shared state, utilities, i18n, API commands, MDI icons
- player.js: tabs, theme, accent, vinyl, visualizer, UI updates
- websocket.js: connection, auth, reconnection
- scripts.js: scripts CRUD, quick access, execution dialog
- callbacks.js: callbacks CRUD
- browser.js: media file browser, thumbnails, pagination, search
- links.js: links CRUD, header links, display controls
- main.js: DOMContentLoaded init orchestrator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:34:01 +03:00
alexei.dolgolyov 9404b37f05 Codebase audit fixes: stability, performance, accessibility
- Fix CORS: set allow_credentials=False (token auth, not cookies)
- Add threading.Lock for position cache thread safety
- Add shutdown_executor() for clean ThreadPoolExecutor cleanup
- Dedicated ThreadPoolExecutors for script/callback execution
- Fix Mutagen file handle leaks with try/finally close
- Reduce idle WebSocket polling (0.5s → 2.0s when no clients)
- Add :focus-visible styles for playback control buttons
- Add aria-label to icon-only header buttons
- Dynamic album art alt text for screen readers
- Persist MDI icon cache to localStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 12:10:24 +03:00
alexei.dolgolyov 73a6f387e1 Add friendly media source names with brand icons
- Registry of 17 popular media apps (browsers, players, streaming)
- Substring matching resolves raw process names to friendly names
- Brand-colored SVG icons displayed inline next to source name
- Russian locale support for Yandex Music (Яндекс Музыка)
- Unknown sources fall back to .exe-stripped name

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:03:46 +03:00
alexei.dolgolyov 3d01d98da0 Style audio device select, hide mini player volume on tablet
- Native select with explicit font stack and focus glow
- Hide mini player volume section below 900px

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:10:28 +03:00
alexei.dolgolyov 00d313daa1 Fix vinyl angle persistence on toggle, group player toggle buttons
- Save vinyl rotation angle before flipping vinylMode flag off
- Wrap vinyl + visualizer buttons in .player-toggles container
- Move margin-left:auto from individual buttons to group

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 21:44:48 +03:00
alexei.dolgolyov 0691e3d338 Add audio visualizer with spectrogram, beat-reactive art, and device selection
- 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>
2026-02-27 21:42:19 +03:00
alexei.dolgolyov 8a8f00ff31 Persist vinyl rotation angle across page reloads
- Save current rotation angle to localStorage every 2s and on page unload
- Restore angle on page load via CSS custom property --vinyl-offset
- Extract angle from computed transform matrix

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 21:06:20 +03:00
alexei.dolgolyov 397d38ac12 Add primary display indicator, custom accent color picker, restart script
- 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>
2026-02-27 16:18:18 +03:00
alexei.dolgolyov adf2d936da Consolidate tabs, Quick Access links, mini player nav, link descriptions
- 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>
2026-02-27 15:08:09 +03:00
alexei.dolgolyov 99dbbb1019 Add header quick links with CRUD management and icon enhancements
- 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>
2026-02-27 14:42:18 +03:00
alexei.dolgolyov a568608ec3 Add display brightness and power control
- 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>
2026-02-27 13:54:43 +03:00