## v0.2.5 (2026-05-16)

### Security

- **Loopback-by-default + auto-generated token:** Server now binds `127.0.0.1` by default; first-run bootstrap generates a random `api_token` and refuses to bind a non-loopback interface without auth unless explicitly opted in. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Browser path-traversal hardening:** `BrowserService.validate_path` now rejects absolute paths, drive letters, UNC paths, and NUL bytes. `/api/browser/{play,metadata,thumbnail}` require a `folder_id` plus a folder-relative path — arbitrary filesystem reads via the browser API are no longer possible. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Strict input validation on links/scripts:** Pydantic validators reject non-http(s) URLs and any icon outside the `mdi:<slug>` namespace. Create/update/delete on scripts, callbacks, and links is gated by the corresponding `*_management` flags. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Hardened response headers + CORS:** Strict `Content-Security-Policy`, `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 for trusted origins. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Atomic config writes with restrictive permissions:** `config.yaml` writes go through a temp file + `os.replace` and land with `0o600` on POSIX, so a crash mid-write can never leave a half-written token on disk readable to other users. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Subprocess process-group isolation:** Spawned scripts/callbacks now get their own process group (`CREATE_NEW_PROCESS_GROUP` on Windows, `start_new_session=True` on POSIX), so a timeout actually kills the whole tree instead of orphaning child processes. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Frontend XSS hardening:** Monitor name + details are `escapeHtml`'d, the power button moved to a delegated `data-action` handler, and remote MDI SVGs are parsed and sanitized (strip `<script>`, `<foreignObject>`, `on*` handlers, `javascript:` hrefs) before they touch `innerHTML`. All dynamic URL segments now go through `encodeURIComponent`. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **CSP-compliant event wiring:** Strict `script-src 'self'` was blocking every inline `onclick`/`onchange`/`oninput`/`onsubmit` in the UI, leaving buttons and forms silently dead. All 53 inline handler attributes in `index.html` were renamed to `data-on*` and a new `wireInlineHandlers()` in `app.js` parses each expression on `DOMContentLoaded` and attaches a real `addEventListener` — supports no-arg calls, string/number/bool/null literals, and the `event` token. No `unsafe-inline` or `unsafe-hashes` needed. ([eaeebb6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/eaeebb6))

### Bug Fixes

- **WebSocket reconnect robustness:** Close the previous socket before opening a new one, clear the ping interval per-socket, clear `reconnectTimeout` up-front, retry on `online`/`visibilitychange`, and wrap `JSON.parse` in try/catch — eliminates the stale-socket leaks and "stuck offline after sleep" cases. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Artwork fetch race:** `AbortController` + generation guard so a rapid track change can no longer paint the previous track's artwork over the current one. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Audio analyzer no longer spins infinitely without a loopback device:** A sticky `_unavailable` flag short-circuits start/stop; cleared by `set_device()` so the user can recover once a device appears. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Volume short-circuit cache invalidation:** Cache is now busted when the server reports a remote volume change, so the UI no longer ignores volume updates that happened outside the app. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Browser thumbnail race:** Per-folder generation counter + `isConnected` checks; in-flight fetches are aborted on navigation, so thumbnails from a folder you already left can't paint into the current view. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Track-skip uses cached title** instead of a full WinRT status round-trip — skip feedback is now instant. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Browser list column alignment:** `.browser-list` switched to CSS grid + subgrid so header and rows share column tracks, eliminating the misaligned columns when content widths differed between rows. Matching responsive column overrides applied at the parent. Root-folder SVG sizing (hardcoded 24×24 in `browser.js`) now fills the 56px icon box instead of rendering at ~43%. Compact-grid icon fills its thumb wrapper so the emoji centers instead of being stranded top-left. Premature `isConnected` bail removed from `loadThumbnail` — the img element is intentionally detached when called from `renderBrowserGrid/List`, and the post-await checks already handle navigation-away correctly. ([982dda4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/982dda4))

### Performance

- **Blocking IO off the event loop:** Linux MPRIS/`pactl` calls, `/api/display` DDC/CI handlers, and `browse_directory` are all wrapped in `asyncio.to_thread` — slow SMB shares or laggy monitors can no longer stall the entire async runtime. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Windows status poll loop reuse:** The 0.5s status poll now caches one asyncio loop per worker thread via `threading.local` instead of `new_event_loop`/`close` on every tick. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **WebSocket broadcast: serialize once:** `broadcast()` serializes JSON a single time and uses `send_text` to fan out to all clients. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Thumbnail cache cleanup actually runs:** The hourly cleanup task was defined but never scheduled — it is now wired into the lifespan handler so the cache no longer grows unbounded. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Progress drag listeners attached only while dragging** — no more global `mousemove` handler firing on every cursor twitch. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))

### UI/UX

- **Copper accent consistency:** Green leftover focus rings (`rgba(29,185,84,…)`) replaced with copper (`rgba(var(--copper-rgb),…)`) across the UI. Dialogs now have square corners and a copper top hairline so they read as part of the editorial chrome. `.browser-item` is transparent with a copper hover border (was a filled card). ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Audio device select** uses `var(--sans)` instead of the generic system font so it matches surrounding controls. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Mobile padding tuned for ≤480px screens.** ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **Accessible breadcrumb home:** Now a real `<button>` with `aria-label`, and `aria-current` is set on the root. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- **i18n gaps filled:** `display.msg.power_*`, `execution.*`, `scripts.params.execute`, `callbacks.empty` now have proper en + ru strings. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))

---

### Development / Internal

#### Quality

- All `asyncio.get_event_loop()` in coroutines migrated to `get_running_loop()` (the former is deprecated in Python 3.12+). ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- `ThreadPoolExecutor`s now shut down cleanly during lifespan teardown. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- `config_manager` dedup: 12 near-identical CRUD methods collapsed onto generic `_upsert`/`_delete` helpers — about **290 lines removed** with no behavior change. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- Service worker no longer pass-throughs every fetch. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- M3U playlist written via `NamedTemporaryFile` so a fixed-path symlink can no longer clobber it. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- `__version__` prefers live `pyproject.toml` in dev checkouts so `pip install -e .` users see the source-of-truth version, not the stale metadata baked in at install time. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))
- `_broadcast_after_open` hardening: initialize status, swallow per-poll errors, and track background tasks in a strong-ref set with done-callback cleanup so they aren't garbage-collected mid-flight. ([bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40))

---

<details>
<summary>All Commits</summary>

| Hash | Message | Author |
|------|---------|--------|
| [982dda4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/982dda4) | fix(browser): align list columns via subgrid and fix icon sizing | alexei.dolgolyov |
| [eaeebb6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/eaeebb6) | fix(csp): replace inline on* handlers with data-on* + JS wiring | alexei.dolgolyov |
| [bcc6d40](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/bcc6d40) | fix: comprehensive security, bug, performance, and UI/UX audit | alexei.dolgolyov |

</details>
