feat(foreground): track topmost process + browser page title
Lint & Test / test (push) Failing after 8s

Adds cross-platform foreground-window tracking and exposes it over REST
(/api/foreground) and the existing WebSocket feed.

- foreground_service.py: Windows probe via ctypes (HANDLE-correct argtypes
  to avoid 64-bit handle truncation); macOS via AppKit; Linux via Xlib
  (Wayland returns unavailable). TTL cache + per-platform fallback.
- browser_url_service.py: when foreground is a recognised browser, extract
  the page title from the window title (browser-name suffix stripped) and
  surface `is_browser` + `browser_page_title`. Optional UIA-based URL
  extraction behind MEDIA_SERVER_BROWSER_UIA env flag (off by default —
  Chromium browsers keep their accessibility tree dormant otherwise).
- websocket_manager: poll foreground every 1s inside the existing status
  loop, broadcast `foreground` on connect and `foreground_update` on
  change. Diff only on user-visible fields to avoid geometry spam.
- WebUI: new editorial card rendered under the monitor list on the
  Display tab — process name, window title, fullscreen/minimized/monitor
  chips, browser block when applicable, exe path, PID, started-ago,
  geometry, platform. 16px inter-section gap matches Settings cadence.
- i18n: 25 new keys added to both en.json and ru.json.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 03:11:59 +03:00
parent 0cf49deac0
commit 61cdce9b60
15 changed files with 1571 additions and 3 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Smoke tests for the foreground tracker.
The OS-specific probe code is hard to mock end-to-end inside a CI container,
so these tests focus on the platform-agnostic surface: the dataclass shape,
TTL caching, and graceful fallback when the platform probe raises. The
Windows/Linux/macOS probes themselves are exercised through manual runs.
"""
from __future__ import annotations
import time
import pytest
from media_server.services import foreground_service as fg
def setup_function(_):
fg.reset_cache()
def test_unavailable_default_shape():
info = fg.ForegroundInfo(available=False)
d = info.to_dict()
assert d["available"] is False
assert d["pid"] is None
assert d["process_name"] is None
assert d["is_fullscreen"] is False
assert "platform" in d
def test_cache_returns_same_instance(monkeypatch):
calls = {"n": 0}
def fake_probe():
calls["n"] += 1
return fg.ForegroundInfo(available=True, pid=42, process_name="x.exe")
monkeypatch.setattr(fg, "_probe", fake_probe)
a = fg.get_foreground_info()
b = fg.get_foreground_info()
assert a is b
assert calls["n"] == 1
def test_cache_force_refresh(monkeypatch):
calls = {"n": 0}
def fake_probe():
calls["n"] += 1
return fg.ForegroundInfo(available=True, pid=calls["n"])
monkeypatch.setattr(fg, "_probe", fake_probe)
fg.get_foreground_info()
fg.get_foreground_info(force_refresh=True)
assert calls["n"] == 2
def test_cache_ttl_expiry(monkeypatch):
calls = {"n": 0}
def fake_probe():
calls["n"] += 1
return fg.ForegroundInfo(available=True, pid=calls["n"])
monkeypatch.setattr(fg, "_probe", fake_probe)
monkeypatch.setattr(fg, "_CACHE_TTL", 0.0)
# Re-bind the cache's TTL by exercising it twice with TTL 0.
fg.get_foreground_info()
fg.get_foreground_info()
assert calls["n"] == 2
def test_probe_crash_returns_unavailable(monkeypatch):
def boom():
raise RuntimeError("kaboom")
# Force every platform branch to call our crashing probe.
monkeypatch.setattr(fg, "_probe_windows", boom)
monkeypatch.setattr(fg, "_probe_linux", boom)
monkeypatch.setattr(fg, "_probe_macos", boom)
info = fg._probe()
assert info.available is False
assert info.error and "kaboom" in info.error