Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 450f9fe1ee | |||
| e1c8474271 |
+4
-22
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user