From bcc6d40ed73b0d0d088a652abe6dd0e145f6fc76 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 16 May 2026 13:22:46 +0300 Subject: [PATCH] fix: comprehensive security, bug, performance, and UI/UX audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 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: + 127.0.0.1: 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 `; } const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' \u00B7 '); - const detailsHtml = details ? `${details}` : ''; + const detailsHtml = details ? `${escapeHtml(details)}` : ''; const primaryBadge = monitor.is_primary ? `
- ${monitor.name}${primaryBadge} + ${escapeHtml(monitor.name)}${primaryBadge} ${detailsHtml}
${powerBtn} @@ -303,6 +306,11 @@ export async function loadDisplayMonitors() { container.appendChild(card); }); + // Bind a single delegated click handler for the power buttons. + // Avoids inline onclick="..." with interpolated monitor data. + container.removeEventListener('click', _onPowerButtonClick); + container.addEventListener('click', _onPowerButtonClick); + // Enhance every tuning