Compare commits

...

2 Commits

Author SHA1 Message Date
alexei.dolgolyov 450f9fe1ee chore: release v0.2.7
Lint & Test / test (push) Has been skipped
Release / create-release (push) Successful in 3s
Release / build-linux (push) Successful in 30s
Release / build-windows (push) Successful in 52s
2026-05-19 01:34:36 +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
6 changed files with 65 additions and 37 deletions
+4 -22
View File
@@ -1,25 +1,8 @@
## v0.2.6 (2026-05-18)
## v0.2.7 (2026-05-19)
### Features
### Bug Fixes
- **Foreground-window tracker (cross-platform):** New `/api/foreground` endpoint plus live `foreground` / `foreground_update` messages on the existing WebSocket feed. Windows uses a ctypes `GetForegroundWindow` probe with HANDLE-correct `argtypes` so 64-bit window handles aren't truncated; macOS uses AppKit's `NSWorkspace.frontmostApplication`; Linux uses Xlib's `_NET_ACTIVE_WINDOW` (Wayland sessions return a structured "unavailable" so the UI can render gracefully). Each platform has a TTL cache and per-platform fallbacks. ([61cdce9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/61cdce9))
- **Browser page-title surfacing:** When the foreground process is a recognised browser, the window title is stripped of the trailing browser-name suffix and exposed as `browser_page_title` alongside `is_browser`. Optional UIA-based URL extraction sits behind the `MEDIA_SERVER_BROWSER_UIA` env flag (off by default — Chromium browsers keep their accessibility tree dormant unless something asks, and enabling it has a measurable cost). ([61cdce9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/61cdce9))
- **Foreground card in the Web UI:** New editorial card under the monitor list on the Display tab renders process name, window title, fullscreen / minimized / monitor chips, the browser block when applicable, exe path, PID, started-ago, geometry, and platform. 16px inter-section gap matches the Settings cadence. 25 new i18n keys added to both `en.json` and `ru.json`. ([61cdce9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/61cdce9))
- **WebSocket integration:** The existing 1s status loop now polls foreground every tick, broadcasts `foreground` on connect, and emits `foreground_update` only when user-visible fields actually change — geometry deltas alone don't spam the channel. ([61cdce9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/61cdce9))
### Security
- **Loopback-by-default in the shipped config:** `config.example.yaml` now defaults `host: 127.0.0.1`. The server still refuses to bind a non-loopback interface without either `api_tokens` configured or an explicit `allow_lan_without_auth: true` opt-in — this hardens fresh installs where a user copies the example config verbatim. ([0cf49de](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0cf49de))
### Reliability
- **Diagnosable silent boot failures:** Pre-uvicorn fatal errors (config parse failures, port conflicts, missing dependencies on Windows) are now mirrored to `startup-errors.log` in the config directory. Previously, launches via `wscript`/`pythonw` (the hidden Startup-folder shortcut path) had no console attached, so any startup crash was effectively invisible — the user just saw "nothing happened". ([0cf49de](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0cf49de))
---
### Development / Internal
- **Lint cleanup:** Sorted the Xlib import in `foreground_service.py` so `ruff check` is clean. Same module, no behaviour change.
- **Display tab sliders + accent picker now respond to clicks/drags:** Both the brightness and contrast sliders on the Display tab, and the accent-color picker in the header, were rendering dynamic HTML with inline `oninput` / `onchange` / `onclick` attributes — every one of which the server's strict `script-src 'self'` CSP silently dropped. The result: brightness and contrast couldn't be changed from the WebUI at all, and picking a custom accent did nothing. Replaced the inline attributes with `data-*` markers and wired proper `addEventListener` calls (delegated on the slider container, direct on the accent dropdown), so the controls work under the strict CSP without any `unsafe-inline` / `unsafe-hashes` relaxation. ([e1c8474](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/e1c8474))
---
@@ -28,7 +11,6 @@
| Hash | Message | Author |
|------|---------|--------|
| [61cdce9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/61cdce9) | feat(foreground): track topmost process + browser page title | alexei.dolgolyov |
| [0cf49de](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0cf49de) | fix(config): secure-by-default loopback bind and startup-error logging | alexei.dolgolyov |
| [e1c8474](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/e1c8474) | fix(csp): wire display sliders and accent picker without inline on* | alexei.dolgolyov |
</details>
+33 -6
View File
@@ -182,8 +182,7 @@ export async function loadDisplayMonitors() {
<span class="display-slider-label" data-i18n="display.contrast">${t('display.contrast')}</span>
<input type="range" class="display-slider display-contrast-slider"
min="0" max="100" value="${contrastValue}"
oninput="onDisplayContrastInput(${monitor.id}, this.value)"
onchange="onDisplayContrastChange(${monitor.id}, this.value)">
data-display-slider="contrast" data-monitor-id="${monitor.id}">
<span class="display-slider-value" id="contrast-val-${monitor.id}">${contrastValue}%</span>
</div>`;
}
@@ -296,8 +295,7 @@ export async function loadDisplayMonitors() {
</svg>
<span class="display-slider-label" data-i18n="display.brightness">${t('display.brightness')}</span>
<input type="range" class="display-slider display-brightness-slider" min="0" max="100" value="${brightnessValue}" ${brightnessDisabled}
oninput="onDisplayBrightnessInput(${monitor.id}, this.value)"
onchange="onDisplayBrightnessChange(${monitor.id}, this.value)">
data-display-slider="brightness" data-monitor-id="${monitor.id}">
<span class="display-slider-value display-brightness-value" id="brightness-val-${monitor.id}">${brightnessValue}%</span>
</div>
${contrastRow}
@@ -306,10 +304,15 @@ export async function loadDisplayMonitors() {
container.appendChild(card);
});
// Bind a single delegated click handler for the power buttons.
// Avoids inline onclick="..." with interpolated monitor data.
// Bind a single delegated click handler for the power buttons,
// plus input/change handlers for the brightness & contrast sliders.
// Avoids inline on* attributes (blocked by script-src 'self' CSP).
container.removeEventListener('click', _onPowerButtonClick);
container.addEventListener('click', _onPowerButtonClick);
container.removeEventListener('input', _onDisplaySliderInput);
container.addEventListener('input', _onDisplaySliderInput);
container.removeEventListener('change', _onDisplaySliderChange);
container.addEventListener('change', _onDisplaySliderChange);
// Enhance every tuning <select> with an IconSelect now that the
// cards are in the DOM (IconSelect needs offsetParent + sibling).
@@ -456,6 +459,30 @@ function _onPowerButtonClick(event) {
if (Number.isFinite(id)) toggleDisplayPower(id);
}
function _onDisplaySliderInput(event) {
const el = event.target.closest('input[data-display-slider]');
if (!el) return;
const id = Number(el.dataset.monitorId);
if (!Number.isFinite(id)) return;
if (el.dataset.displaySlider === 'brightness') {
onDisplayBrightnessInput(id, el.value);
} else if (el.dataset.displaySlider === 'contrast') {
onDisplayContrastInput(id, el.value);
}
}
function _onDisplaySliderChange(event) {
const el = event.target.closest('input[data-display-slider]');
if (!el) return;
const id = Number(el.dataset.monitorId);
if (!Number.isFinite(id)) return;
if (el.dataset.displaySlider === 'brightness') {
onDisplayBrightnessChange(id, el.value);
} else if (el.dataset.displaySlider === 'contrast') {
onDisplayContrastChange(id, el.value);
}
}
export async function toggleDisplayPower(monitorId) {
const btn = document.getElementById(`power-btn-${monitorId}`);
const isOn = btn && btn.classList.contains('on');
+24 -5
View File
@@ -207,20 +207,39 @@ export function renderAccentSwatches() {
const swatches = accentPresets.map(p =>
`<div class="accent-swatch ${p.color === current ? 'active' : ''}"
style="background: ${p.color}"
onclick="selectAccentColor('${p.color}', '${p.hover}')"
data-accent-color="${p.color}" data-accent-hover="${p.hover}"
title="${p.name}"></div>`
).join('');
const customRow = `
<div class="accent-custom-row ${isCustom ? 'active' : ''}" onclick="document.getElementById('accentCustomInput').click()">
<div class="accent-custom-row ${isCustom ? 'active' : ''}" data-accent-custom-row>
<span class="accent-custom-swatch" style="background: ${isCustom ? current : '#888'}"></span>
<span class="accent-custom-label">${t('accent.custom')}</span>
<input type="color" id="accentCustomInput" value="${current}"
onclick="event.stopPropagation()"
onchange="selectAccentColor(this.value, lightenColor(this.value, 15))">
<input type="color" id="accentCustomInput" value="${current}">
</div>`;
dropdown.innerHTML = swatches + customRow;
// Wire CSP-safe handlers (script-src 'self' blocks inline on* attributes).
dropdown.querySelectorAll('.accent-swatch[data-accent-color]').forEach(el => {
el.addEventListener('click', () => {
selectAccentColor(el.dataset.accentColor, el.dataset.accentHover);
});
});
const customRowEl = dropdown.querySelector('[data-accent-custom-row]');
const customInput = dropdown.querySelector('#accentCustomInput');
if (customRowEl && customInput) {
customRowEl.addEventListener('click', (e) => {
// The native color popup only opens from a user-initiated click on
// the <input>. Forward clicks on the row to the input — except when
// the input itself was the source (avoids re-entry).
if (e.target !== customInput) customInput.click();
});
customInput.addEventListener('click', (e) => e.stopPropagation());
customInput.addEventListener('change', () => {
selectAccentColor(customInput.value, lightenColor(customInput.value, 15));
});
}
}
export function selectAccentColor(color, hover) {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "media-server-frontend",
"version": "0.2.6",
"version": "0.2.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "media-server-frontend",
"version": "0.2.6",
"version": "0.2.7",
"devDependencies": {
"esbuild": "^0.27.4"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "media-server-frontend",
"version": "0.2.6",
"version": "0.2.7",
"private": true,
"description": "Frontend build tooling for media server WebUI",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "media-server"
version = "0.2.6"
version = "0.2.7"
description = "REST API server for controlling system-wide media playback"
readme = "README.md"
license = { text = "MIT" }