fix: comprehensive security, bug, performance, and UI/UX audit
Lint & Test / test (push) Successful in 20s
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
This commit is contained in:
@@ -329,7 +329,7 @@ body.translations-loaded {
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 4px rgba(29, 185, 84, 0.2);
|
||||
box-shadow: 0 0 0 4px rgba(var(--copper-rgb), 0.2);
|
||||
}
|
||||
|
||||
input:focus-visible,
|
||||
@@ -337,7 +337,7 @@ select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.15);
|
||||
box-shadow: 0 0 0 3px rgba(var(--copper-rgb), 0.15);
|
||||
}
|
||||
|
||||
.tab-btn:focus-visible {
|
||||
@@ -1004,7 +1004,7 @@ button:disabled {
|
||||
.controls button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
box-shadow: 0 0 0 4px rgba(29, 185, 84, 0.25);
|
||||
box-shadow: 0 0 0 4px rgba(var(--copper-rgb), 0.25);
|
||||
}
|
||||
|
||||
.mute-btn:focus-visible,
|
||||
@@ -1012,7 +1012,7 @@ button:disabled {
|
||||
.vinyl-toggle-btn:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 4px rgba(29, 185, 84, 0.25);
|
||||
box-shadow: 0 0 0 4px rgba(var(--copper-rgb), 0.25);
|
||||
}
|
||||
|
||||
.controls button.primary {
|
||||
@@ -1060,7 +1060,7 @@ button:disabled {
|
||||
|
||||
#volume-slider:hover::-webkit-slider-thumb {
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 6px rgba(29, 185, 84, 0.4);
|
||||
box-shadow: 0 0 6px rgba(var(--copper-rgb), 0.4);
|
||||
}
|
||||
|
||||
#volume-slider::-moz-range-thumb {
|
||||
@@ -1075,7 +1075,7 @@ button:disabled {
|
||||
|
||||
#volume-slider:hover::-moz-range-thumb {
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 6px rgba(29, 185, 84, 0.4);
|
||||
box-shadow: 0 0 6px rgba(var(--copper-rgb), 0.4);
|
||||
}
|
||||
|
||||
.volume-display {
|
||||
@@ -1169,7 +1169,7 @@ button:disabled {
|
||||
.vinyl-toggle-btn.active {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
background: rgba(var(--copper-rgb), 0.1);
|
||||
}
|
||||
|
||||
.vinyl-toggle-btn svg {
|
||||
@@ -1393,7 +1393,7 @@ button:disabled {
|
||||
border-radius: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-family: var(--sans, inherit);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
@@ -1402,7 +1402,7 @@ button:disabled {
|
||||
.audio-device-selector select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.15);
|
||||
box-shadow: 0 0 0 3px rgba(var(--copper-rgb), 0.15);
|
||||
}
|
||||
|
||||
.audio-device-status {
|
||||
@@ -1836,13 +1836,18 @@ button:disabled {
|
||||
dialog {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
/* Editorial chrome to match the rest of the Studio Reference layout:
|
||||
no rounded corners, hairline border, and a copper top accent that
|
||||
lets the dialog read as a continuation of the magazine rather than
|
||||
a generic Material modal. */
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-top: 1px solid var(--copper);
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.65);
|
||||
animation: dialogIn 0.25s ease-out;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -2827,7 +2832,7 @@ button.primary svg {
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: var(--accent);
|
||||
background: rgba(29, 185, 84, 0.08);
|
||||
background: rgba(var(--copper-rgb), 0.08);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -3235,12 +3240,14 @@ button.primary svg {
|
||||
}
|
||||
|
||||
.browser-item {
|
||||
background: var(--bg-tertiary);
|
||||
/* Match the editorial card language used elsewhere on the page —
|
||||
transparent background, hairline border, copper-on-hover. */
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
border-radius: 0;
|
||||
padding: 0.6rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -3250,6 +3257,11 @@ button.primary svg {
|
||||
animation-delay: calc(var(--item-index, 0) * 25ms);
|
||||
}
|
||||
|
||||
.browser-item:hover {
|
||||
border-color: rgba(var(--copper-rgb), 0.45);
|
||||
background: rgba(var(--copper-rgb), 0.04);
|
||||
}
|
||||
|
||||
@keyframes itemFadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@@ -3286,14 +3298,14 @@ button.primary svg {
|
||||
height: 56px;
|
||||
font-size: 1.75rem;
|
||||
border-radius: 14px;
|
||||
background: rgba(29, 185, 84, 0.1);
|
||||
border: 1px solid rgba(29, 185, 84, 0.15);
|
||||
background: rgba(var(--copper-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--copper-rgb), 0.15);
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.browser-item.browser-root-folder:hover .browser-icon {
|
||||
background: rgba(29, 185, 84, 0.18);
|
||||
border-color: rgba(29, 185, 84, 0.3);
|
||||
background: rgba(var(--copper-rgb), 0.18);
|
||||
border-color: rgba(var(--copper-rgb), 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@@ -3963,7 +3975,13 @@ html {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.container { padding: 48px 18px 24px; }
|
||||
.container { padding: 32px 18px 20px; }
|
||||
}
|
||||
|
||||
/* Phones: trim the editorial spread further so the first viewport isn't
|
||||
90% chrome. The 56px top pad eats a third of a 360x640 screen. */
|
||||
@media (max-width: 480px) {
|
||||
.container { padding: 16px 12px 16px; }
|
||||
}
|
||||
|
||||
/* ─── Folio marks (page corners, all tabs) ────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user