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
+318
View File
@@ -9321,3 +9321,321 @@ body.is-fullscreen-player .now-playing .vu-meter {
body.is-fullscreen-player .fs-bloom #fs-bloom-art { animation: none !important; }
:root[data-theme="light"] body.is-fullscreen-player .fs-bloom { opacity: 0.22; }
}
/* ════════════════════════════════════════════════════════════════
FOREGROUND container — editorial process plate
════════════════════════════════════════════════════════════════ */
.foreground-container {
background: transparent;
border: 0;
padding: 0;
box-shadow: none;
margin-top: 28px;
}
.foreground-stage {
min-height: 360px;
}
/* Match the inter-section gap used between .settings-section blocks
in the Settings tab — keeps cadence consistent across tabs. */
.display-container > * + * {
margin-top: 16px;
}
.foreground-card {
position: relative;
display: block;
padding: clamp(24px, 3vw, 40px) clamp(24px, 3vw, 40px) 28px;
border: 1px solid var(--rule);
border-top: 2px solid var(--copper);
background:
radial-gradient(120% 80% at 0% 0%, rgba(var(--copper-rgb), 0.05), transparent 60%),
var(--bg-paper);
box-shadow:
0 1px 0 var(--bg-paper),
0 28px 60px -28px rgba(0, 0, 0, 0.45),
0 8px 20px -10px rgba(0, 0, 0, 0.25);
}
.foreground-card[data-fullscreen="1"] {
border-top-color: var(--copper-hi);
box-shadow:
0 1px 0 var(--bg-paper),
0 28px 60px -28px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(var(--copper-rgb), 0.18),
0 0 60px -12px var(--copper-glow);
}
.foreground-card .fg-kicker {
display: flex;
align-items: center;
gap: 14px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--copper);
margin-bottom: 22px;
}
.foreground-card .fg-kicker::before,
.foreground-card .fg-kicker::after {
content: "";
height: 1px;
background: var(--copper);
opacity: 0.6;
flex: 0 0 24px;
}
.foreground-card .fg-kicker::after { flex: 1 0 auto; }
.foreground-card .fg-process {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(34px, 4.4vw, 56px);
line-height: 1.02;
letter-spacing: -0.02em;
font-variation-settings: 'opsz' 144;
color: var(--ink);
margin: 0 0 10px;
word-break: break-word;
overflow-wrap: anywhere;
transition: color 180ms var(--ease, ease);
}
.foreground-card .fg-process:hover {
color: var(--copper-hi);
}
.foreground-card .fg-window-title {
font-family: var(--serif);
font-style: italic;
font-size: 20px;
font-weight: 300;
color: var(--ink-soft);
font-variation-settings: 'opsz' 60;
margin-bottom: 22px;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.foreground-card .fg-window-title:empty { display: none; }
.foreground-card .fg-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 28px;
}
.foreground-card .fg-chips:empty { display: none; }
.fg-chip {
display: inline-flex;
align-items: center;
padding: 5px 11px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-soft);
background: transparent;
border: 1px solid var(--rule-strong);
border-radius: 999px;
line-height: 1.2;
white-space: nowrap;
}
.fg-chip.fg-chip-accent {
color: var(--copper);
border-color: var(--copper);
background: rgba(var(--copper-rgb), 0.07);
}
.fg-chip.fg-chip-mute {
color: var(--ink-mute);
border-color: var(--rule);
}
.foreground-card .fg-details {
display: block;
margin: 0;
border-top: 1px solid var(--rule);
}
.foreground-card .fg-row {
display: grid;
grid-template-columns: minmax(160px, 220px) 1fr;
gap: 24px;
padding: 14px 0;
border-bottom: 1px solid var(--rule);
align-items: baseline;
min-width: 0;
}
.foreground-card .fg-row dt {
font-family: var(--mono);
font-size: 9px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--copper);
margin: 0;
}
.foreground-card .fg-row dd {
font-family: var(--serif);
font-style: italic;
font-size: 18px;
color: var(--ink);
font-variation-settings: 'opsz' 30;
margin: 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.foreground-card .fg-mono {
font-family: var(--mono);
font-style: normal;
font-size: 13px;
letter-spacing: 0.02em;
color: var(--ink-soft);
font-variant-numeric: tabular-nums;
word-break: break-all;
}
.foreground-empty {
padding: 60px 24px;
text-align: center;
color: var(--ink-mute);
}
.foreground-empty svg {
width: 64px;
height: 64px;
margin-bottom: 14px;
opacity: 0.55;
color: var(--ink-faint);
}
.foreground-empty p {
font-family: var(--serif);
font-style: italic;
font-size: 18px;
color: var(--ink-soft);
margin: 0;
}
.foreground-empty .foreground-empty-error {
margin-top: 10px;
font-family: var(--mono);
font-style: normal;
font-size: 11px;
letter-spacing: 0.06em;
color: var(--ink-mute);
word-break: break-word;
}
/* ─── Header status badge ──────────────────────────────────── */
.foreground-status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
padding: 0 12px 0 10px;
margin-right: 4px;
background: transparent;
border: 1px solid var(--rule-strong);
border-radius: 999px;
color: var(--ink-soft);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.04em;
cursor: pointer;
max-width: 240px;
transition: color 180ms ease, border-color 180ms ease, background 180ms ease;
}
.foreground-status-badge:hover {
color: var(--ink);
border-color: var(--copper);
background: rgba(var(--copper-rgb), 0.06);
}
.foreground-status-badge.hidden { display: none !important; }
.foreground-status-badge .fg-badge-mark {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ink-mute);
flex-shrink: 0;
}
.foreground-status-badge.is-media .fg-badge-mark,
.foreground-status-badge.is-fullscreen .fg-badge-mark {
background: var(--copper);
box-shadow: 0 0 8px var(--copper-glow);
}
.foreground-status-badge.is-fullscreen {
border-color: var(--copper);
color: var(--ink);
}
.foreground-status-badge .fg-badge-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140px;
}
.foreground-status-badge .fg-badge-tag {
color: var(--copper);
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 10px;
flex-shrink: 0;
}
.foreground-status-badge .fg-badge-tag.hidden { display: none; }
/* ─── Light theme overrides ──────────────────────────────── */
:root[data-theme="light"] .foreground-card {
background:
radial-gradient(120% 80% at 0% 0%, rgba(var(--copper-rgb), 0.05), transparent 60%),
var(--bg-paper);
box-shadow:
0 1px 0 var(--bg-paper),
0 22px 50px -24px rgba(26, 23, 21, 0.20),
0 6px 16px -8px rgba(26, 23, 21, 0.12);
}
:root[data-theme="light"] .foreground-card[data-fullscreen="1"] {
box-shadow:
0 1px 0 var(--bg-paper),
0 22px 50px -24px rgba(26, 23, 21, 0.28),
0 0 0 1px rgba(var(--copper-rgb), 0.20),
0 0 50px -12px var(--copper-glow);
}
:root[data-theme="light"] .foreground-status-badge {
border-color: rgba(26, 23, 21, 0.18);
}
:root[data-theme="light"] .foreground-status-badge:hover {
background: rgba(var(--copper-rgb), 0.08);
}
/* ─── Mobile breakpoint ──────────────────────────────────── */
@media (max-width: 720px) {
.foreground-card {
padding: 22px 18px 20px;
}
.foreground-card .fg-process {
font-size: 30px;
}
.foreground-card .fg-window-title {
font-size: 16px;
}
.foreground-card .fg-row {
grid-template-columns: 1fr;
gap: 4px;
padding: 12px 0;
}
.foreground-card .fg-row dd {
font-size: 16px;
}
.foreground-status-badge {
max-width: 160px;
}
.foreground-status-badge .fg-badge-name {
max-width: 80px;
}
.foreground-status-badge .fg-badge-tag {
display: none;
}
}