Commit Graph

9 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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