Commit Graph
29 Commits
Author SHA1 Message Date
4gray 59e08fd2d6 feat(embedded-mpv): add Windows frame-copy support (#1175)
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
2026-07-15 21:27:56 +02:00
4grayandClaude Fable 5 7d75d989e8 feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* feat(embedded-mpv): Linux frame-copy helper via headless EGL

Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4):

- frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL
  path (moved verbatim); Linux acquires an EGL display in order
  surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core
  desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv
  eglGetProcAddress. The helper's own GL calls link against glvnd
  libOpenGL, so no display server is required.
- frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the
  helper and the reader addon, replacing the macOS-only
  clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on
  the same clock.
- embedded_mpv_frame_reader.c: real implementation now also on __linux__
  (the code was already POSIX apart from the clock call).
- binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking
  system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and
  the build-time library dir. The in-process addon still does not link
  libmpv - the ban only binds in-process, the helper is out of process.
- build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR
  or /usr/include) so a distro libmpv-dev install builds without staging a
  vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the
  vendored lib dir.

Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md
(idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump
g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at
1080p60 with 0 torn reads, clean quit with no leaked processes or shm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): enable the frame-copy engine gates on Linux

Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared
dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch,
darwin arm64-only), now backs all four gates so they cannot drift:

- main.ts: the persisted Settings toggle promotes to the env flag on Linux
  too (this runs before window creation and controls the sandbox relax).
- EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable.
- EmbeddedMpvFrameCopyAdapter.isSupported.

getSupport() ordering: the frame-copy branch moves above the Linux-only
native-engine prerequisites - the X11/Xwayland display-server check and
the system-mpv-on-PATH probe only bind the --wid native engine, while the
frame-copy helper renders offscreen (headless EGL) and links libmpv
itself. createSession() also skips resolving the native window handle for
frame-copy sessions, which the adapter ignores anyway, so native-Wayland
sessions no longer trip the window-handle assertion.

Settings copy: the i18n frame-copy description now says macOS (Apple
Silicon) and Linux in all 18 languages; stale macOS-only doc comments in
the settings/support interfaces updated alongside.

Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux
x64/arm64, win32) and service specs covering Linux activation under
native Wayland, macOS arm64 staying active, macOS x64 staying native, and
the skipped window handle for frame-copy sessions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(packaging): CI + package guards for the Linux frame-copy helper

- build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the
  Linux runner (the helper's EGL backend needs them now that the helper
  target builds on Linux), and verify the built helper exists and DOES
  link libmpv - the inverse of the addon's no-libmpv rule, which still
  holds and stays validated.
- electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux
  apps. It links the build host's system libmpv, which end-user systems
  cannot be assumed to have; the support probe treats the missing helper
  as frame-copy-unavailable (dev-build-only engine until the
  bundled-runtime staging milestone).
- frame_helper_gl.h: log the chosen EGL display tier to stderr (the
  adapter mirrors helper stderr), so bring-up problems on exotic setups
  are diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): document the Linux frame-copy port

- architecture doc: frame-copy section covers Linux (EGL display tiers,
  build deps, package strip), Linux support matrix notes the frame-copy
  exception to the X11 + system-mpv requirements, Linux measured baseline.
- RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the
  production helper + reader probe; viewport-size claim reproduced.
- PORTING.md: Linux marked done with pointers to what changed; Windows
  remains the open port and its perf gate the open decision.
- CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev
  build requirements, system-headers fallback, helper strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(embedded-mpv): commit the Linux frame-copy measurement probe

linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the
production helper, attaches the frame-reader addon to the announced shm
generation, and reports new-frame fps, copy wall time, produce->copy age,
torn reads and pixel spread. Usage documented in RESULTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address multi-agent review findings on the Linux port

Confirmed findings (each verified by 3 adversarial reviewers):

- CI would fail to link the helper: -lOpenGL needs the unversioned glvnd
  libOpenGL.so, shipped only by libopengl-dev, which neither the runner
  images nor the previous apt line provide. Added to the workflow and to
  every documented Linux build-dep list.
- The new 'test -x' dist guard could never pass: webpack's dist asset
  copy drops file modes (helper arrives as 0644). The guard is now
  'test -f'; electron-after-pack.cjs restores the execute bit on packaged
  helpers (also fixes packaged-macOS spawns); the support probe now
  requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable
  and falls back to native instead of failing spawn with EACCES.
- The Settings frame-copy toggle was unreachable in exactly the Linux
  states the port targets: the native-Wayland and missing-system-mpv
  unsupported payloads omitted frameCopyAvailable, and toggle visibility
  derives solely from it. Both returns now advertise availability.

Also from review:

- build-embedded-mpv.js keeps the old graceful-skip contract when the new
  system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is
  missing (previously such machines skipped; a hard electron-build
  failure was a regression).
- createSession derives the window-handle skip from the dispatched addon
  instead of re-evaluating the engine gate, so the two cannot disagree.
- The render thread logs the GL renderer string (surfaceless Mesa can
  silently pick llvmpipe on non-Mesa-primary systems; now diagnosable —
  verified 'Mesa Intel Iris Xe' on this machine).
- Specs pin the new semantics: frameCopyAvailable advertised while native
  is unsupported (Wayland / no mpv), frame-copy supported without a
  system mpv, and the handle-skip test disposes its session through the
  owning adapter.
- Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the
  Windows porter; helper-strip removal correctly gated on milestone 4
  (bundled libmpv), not milestone 3; RESULTS.md preamble notes the
  RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address Greptile/Codex review comments

- Sandbox gate requires a usable helper (Greptile P1, security): the
  main.ts env promotion now also probes for an executable
  iptvnator_mpv_helper before relaxing the window sandbox — a stale
  opt-in on packaged Linux (helper deliberately stripped) or after a
  cleaned native build no longer costs a sandboxless launch for an
  engine that cannot activate. Helper discovery (addon candidate paths +
  X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts,
  shared by main.ts and the service; the service keeps thin instance
  wrappers so tests can stub per scenario. New util spec pins the
  platform matrix, candidate resolution, and the execute-bit semantics.
- Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput()
  now also removes iptvnator_mpv_helper and
  embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot
  leave a previous helper advertising frame-copy support against a
  runtime the build just declared unavailable.
- Multiarch default lib dir (Greptile P1, partially refuted): -l
  resolution never depended on our -L (the compiler's built-in search
  paths include the Debian/Ubuntu multiarch dir — proven by the green CI
  run linking with a nonexistent -L dir), but the system-dev fallback
  now defaults to /usr/lib/<multiarch-triple> when present so the -L
  flag and the helper's baked rpath point somewhere real.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): harden Linux frame-copy port

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:42:50 +02:00
4grayandClaude Fable 5 26271fc076 feat(embedded-mpv): frame-copy rendering engine (experimental, macOS Apple Silicon) (#1169)
* spike(embedded-mpv): frame-copy pipeline prototype (helper + shm ring + Electron viewer)

Standalone macOS spike for the frame-copy unification direction from the
2026-07-10 analysis: a helper process renders mpv offscreen into a GL FBO,
reads frames back through an async PBO ring, and publishes BGRA frames into
a 3-slot POSIX shm seqlock ring; a minimal Electron viewer copies the newest
frame via a plain-C N-API addon and uploads it to a WebGL canvas per rAF.

First numbers on M1 Pro (see spike README): 4K60 HEVC hwdec sustained at
60 fps end to end, ~1.2 ms shm copy + ~3.5 ms texture upload, ~10 ms
produce-to-upload age, zero torn frames. Remaining gates: weak hardware,
long-run pacing, HDR, latency flash test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): add RESULTS.md measurement log with M1 Pro baseline

Structured per-machine table with repro commands so the pending Intel Mac
and Windows iGPU runs can be appended and compared one-to-one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): pacing/judder instrumentation + 50/25 fps and HDR gate results

Viewer now measures inter-frame intervals on both clocks (present side and
producer side): stddev/p99/max, late-frame counters vs the producer's median
interval, and a cumulative LONGRUN summary every 30 s. The addon exposes the
producer timestamp (produceMs) for this.

Measured on M1 Pro: 50 fps and 25 fps cadences are clean (late frames only
at startup; residual jitter is 120 Hz rAF grid quantization, bounded by one
display tick), and 4K25 HDR10 PQ/BT.2020 is tonemapped to SDR by mpv before
readback at full rate with unchanged copy costs. RESULTS.md carries the
tables and HDR-clip repro commands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): record 10-minute 4K60 HEVC long-run results

Zero dropped frames and zero torn reads after the first-minute warmup over
~8.5 minutes; steady-state late frames (~0.4%) track the 12 s test clip's
--loop restarts, not the copy pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): viewport-scaling measurement + integration design draft

Confirms the render-at-viewport-size claim (4K source in a 720p FBO costs
720p: 0.17 ms readback / 0.16 ms copy / 0.17 ms upload at 60 fps) and adds
DESIGN.md — the draft integration architecture: per-session helper process
linking bundled libmpv on all platforms (finally full-featured + Wayland-
agnostic Linux), JSON-over-stdio control evolving the Linux wid protocol,
unchanged EmbeddedMpvSession renderer contract, shm generations for resize,
packaging via the existing vendored-runtime tooling, rollout behind its own
flag with the docked path as default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): auto-detect Homebrew prefix and Node headers for Intel Macs

BREW_PREFIX was hardcoded to /opt/homebrew (Apple Silicon) and NODE_INC to
one nvm version; both now resolve via brew --prefix and the PATH node's
execPath, so the pending Intel Mac run needs no Makefile edits. README gets
a fresh-machine checklist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): self-contained measurement bundle for machines without Node/pnpm

make-bundle.sh assembles a tarball with the spike sources, vendored N-API
headers (Makefile prefers them when present, so no Node install is needed),
pre-generated 4K HEVC/HDR10 test clips, and an official Electron dist
download for the target arch. collect-results.sh builds and runs the full
RESULTS.md scenario suite automatically (plus an optional --long 10-minute
run) and writes one results-<host>-<date>.txt to send back. Target-machine
prerequisites shrink to Xcode CLT + brew mpv — built for the pending Intel
Mac baseline run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): support MacPorts libmpv and legacy-macOS bundles

Makefile and collect-results.sh now detect libmpv in the Homebrew prefix or
MacPorts /opt/local (Homebrew is unsupported on legacy macOS like High
Sierra; 'sudo port install mpv +libmpv' provides libmpv there). make-bundle
takes ELECTRON_VERSION/BUNDLE_SUFFIX overrides — Electron 27+ needs macOS
10.15, so High Sierra bundles ship Electron 26.6.10 (LSMinimumSystemVersion
10.13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* spike(embedded-mpv): scope macOS frame-copy engine to Apple Silicon only

Owner decision 2026-07-10: skip Intel Mac measurements and gate the future
frame-copy engine on arm64. Intel Macs able to run the app at all are a
shrinking 2015-2020 cohort and keep the docked/external/web player paths;
the macOS hardware gate closes with the M1 Pro numbers, and remaining
hardware risk moves to the Windows/Linux ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): frame-copy helper process and shm frame reader (native layer)

iptvnator_mpv_helper: one-process-per-session libmpv host that renders
offscreen at viewport size (headless CGL + async PBO ring, validated in
spikes/mpv-frame-copy), publishes BGRA frames into a seqlock shm ring with
resize generations, plays audio directly, and speaks a stdio protocol —
tab-separated commands in, JSON events out. The snapshot event mirrors
NativeEmbeddedMpvSessionSnapshot; status semantics (END_FILE reasons,
eof-reached with keep-open, pause gated on loaded path, fatal-only status
flips) are ported from embedded_mpv.mm.

embedded_mpv_frame_reader.node: plain-C N-API reader the preload script
uses to memcpy the newest complete frame into a V8 ArrayBuffer (Electron's
memory cage forbids zero-copy). Stub exports off macOS.

Both build as extra binding.gyp targets through build-embedded-mpv.js; the
helper gets the same libmpv dependency-path rewrite + ad-hoc re-sign as the
addon and is validated by the forbidden-link check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): frame-copy engine wiring in main process and preload

EmbeddedMpvFrameCopyAdapter implements the NativeEmbeddedMpvAddon surface
over a per-session helper process (spawn, stdio protocol, snapshot cache,
graceful quit->SIGTERM->SIGKILL teardown), so EmbeddedMpvNativeService
reuses its polling/diff/power-blocker/recording logic unchanged. The
IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY flag (darwin/arm64 only) routes
getAddon() to the adapter and reports engine: 'frame-copy' in support.

The preload frame pump loads the shm reader addon, copies the newest frame
once per rAF into a reused buffer, and uploads it to WebGL2 on the
renderer's canvas — no frame data crosses the contextBridge; the bridge
only gains attachEmbeddedMpvFrameView/detachEmbeddedMpvFrameView. The
experiment flag relaxes the window sandbox for that native require;
contextIsolation and nodeIntegration:false stay on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): frame-copy canvas mode in the player component + docs

EmbeddedMpvPlayerComponent renders <canvas data-embedded-mpv-frame> when
support reports engine 'frame-copy' and the session controller starts/stops
the preload frame pump around the session lifecycle. The bounds provider
skips HIDDEN_BOUNDS and the popover cutout for this engine — the canvas is
ordinary DOM, dialogs and popovers stack above it natively; bounds sync
still drives the helper's render size. Adapter unit tests cover spawn args,
snapshot caching, shm generations, protocol encoding, unexpected-exit
mapping, and dispose escalation. Architecture doc and CLAUDE.md describe
the engine, its flag, and the sandbox trade-off.

Verified end to end in the built app (M1 Pro): engine detection, helper
spawn, lavfi playback onto the canvas via CDP-injected smoke — including an
orientation fix (helper FLIP_Y already yields texture-order rows; the pump
shader must not flip uv again).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): close helper stdin on dispose + lifecycle logging

Live testing surfaced a stray idle helper that survived a session switch;
until the root cause is pinned down, dispose now also closes the child's
stdin (the helper exits on EOF) as a second kill path besides quit ->
SIGTERM -> SIGKILL, and spawn/dispose/exit are logged with the session id
so leaks are attributable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): reap sessions when the renderer reloads or crashes

Root cause of the stray idle helper found during live testing: session
teardown lives in the renderer's Angular lifecycle, which never runs on a
renderer crash or hard reload — the main process kept the session (and its
frame-copy helper process / native mpv handle) alive until app shutdown.
EmbeddedMpvNativeService now watches the main window's webContents for
render-process-gone and did-navigate (full reloads only; in-app Angular
routing emits did-navigate-in-page) and disposes every session. Applies to
both engines. Verified live: location.reload() during frame-copy playback
logs 'Disposing 1 session(s): renderer reloaded' and the helper exits
cleanly. Regression test drives both events against the service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): Settings toggle for the frame-copy engine

New 'Embedded MPV: frame-copy engine' checkbox in Settings > Playback,
shown only when the machine can run it (macOS arm64 with the helper binary
present — support now reports frameCopyAvailable). The choice persists to
the main-process config store because the engine relaxes the window sandbox
for the preload frame pump, which is fixed at window creation: main.ts
reads the store before creating the window and sets the engine env var; an
explicitly set env var (including '0') always wins, and the UI shows a
restart hint while the saved choice differs from the active engine.
Localized in all 18 locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): aspect-fit rendering in the frame-copy helper

The helper now observes dwidth/dheight and renders its FBO at the
aspect-fit size of the video inside the requested viewport, bumping a shm
generation on change — letterbox bars are never baked into frames (the VOD
watch shell's ~2:1 box no longer shows black side bars; the canvas
background is transparent so the sides show the app surface, while
fullscreen keeps its black backdrop). Frames also get smaller than the
viewport when aspects differ, trimming copy cost. Aspect override changes
refit automatically. Snapshots now carry videoWidth/videoHeight, and the
adapter forwards IPTVNATOR_EMBEDDED_MPV_AUDIO_DELAY to mpv's audio-delay
for lip-sync tuning until proper calibration lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): require frame-copy artifacts in macOS package validation + docs

macOS packages that ship embedded_mpv.node must also ship the
iptvnator_mpv_helper binary and the embedded_mpv_frame_reader.node addon —
they come out of the same binding.gyp run, and a package missing them would
silently lose the frame-copy engine. Covered in the package-identity test.
Architecture doc and CLAUDE.md document the Settings toggle, aspect-fit
rendering, audio-delay passthrough, and the renderer-reload session reaping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(electron): inline TS helpers so the sandboxed preload keeps working

The frame pump's async/await (target es2015 + importHelpers) made webpack
externalize tslib in main.preload.js. Sandboxed preloads can only require
Electron's built-in module whitelist, so the entire preload script failed
to load and window.electron disappeared for every run without the
frame-copy flag. importHelpers:false for electron-backend keeps the preload
bundle self-contained — and future async code in preload can no longer
silently reintroduce the breakage. Verified live: sandboxed run now has the
bridge, reports engine 'native' and frameCopyAvailable true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): commit the frame-copy analysis handoff + source inventory

The 2026-07-10 analysis that led to this branch now lives next to the spike
(spikes/mpv-frame-copy/ANALYSIS.md), and the architecture doc's What To
Commit section lists the frame-copy engine sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address review findings on the frame-copy engine

- Stale pump attach can no longer win over a newer session: attach/detach
  bump a shared epoch and async attach waits re-check it after every await,
  so an attach for a replaced session aborts instead of installing itself
  (greptile P1).
- A failed frame-view attach (no canvas, no WebGL2, reader missing) now
  disposes the session and surfaces the error UI instead of leaving audio
  playing behind a black canvas (codex P2).
- A stale frame-copy opt-in without the helper binary falls back to the
  native engine instead of reporting embedded MPV unsupported, and the
  Settings checkbox stays visible while a saved opt-in exists so it can
  always be cleared (codex P2). Regression test covers the fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(packaging): make the darwin frame-copy packaging test host-agnostic

On non-macOS CI hosts validatePackagedEmbeddedMpv also reports that macOS
link validation needs a macOS host, so the success-path assertion now
checks only the frame-copy artifact requirement instead of expecting an
empty error list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): Windows/Linux porting handoff for the frame-copy engine

Self-contained entry point for porting sessions on other machines: current
state and coordination constraints, per-OS task lists (Linux EGL first,
then Windows WGL + named shm — the decisive iGPU perf gate), the
hard-won gotchas from the macOS integration (preload/tslib sandbox
breakage, V8 memory cage, frame orientation, stale-attach epoch, dispose
escalation, node-gyp naming, snapshot protocol semantics), testing
recipes, and the suggested milestone order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): branching and merge strategy in the porting handoff

Port work goes to stacked branches off the frame-copy branch (PR base =
frame-copy branch, sequential merges, stack depth one), never into the
frozen PR #1169 branch itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): drop stale uncommitted note from porting handoff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): harden frame-copy helper startup

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 19:48:07 +02:00
4grayandClaude Fable 5 09764e24d4 feat(portals): back button on the in-portal search page (#1142)
* feat(portals): back button on the in-portal search page

Opening a "grey" (unmatched) film from an actor page navigates to the
portal search prefilled with ?q=, but the search page had no way back —
users had to re-navigate through the sidebar to reach the actor's
filmography again.

SearchLayoutComponent gains a showBackButton input + backClick output;
both the Xtream in-portal search (SearchResultsComponent) and the Stalker
search wire it to Location.back(). Shown only on the nested in-portal
search — the top-level global search (sidebar destination) and the dialog
mode (own close button) are excluded. location.back() unwinds the full
history, so search → actor → movie detail → … all chain correctly.

Tests: search-layout spec covers the conditional button + backClick.
Docs: CLAUDE.md actor-pages note updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): address search back-button review (dialog guard, RTL-safe spacing)

- showBackButton now also checks !dialogRef, so a hypothetical dialog
  opened with isGlobalSearch:false from a workspace route can't show a
  back arrow that navigates the page behind the dialog (greptile).
- Replaced the fragile negative margin with a header-title-group flex
  wrapper (gap: 4px) that keeps the arrow and title together regardless
  of the outer gap or Material padding, and is RTL-safe (greptile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:20:34 +02:00
4grayandClaude Fable 5 862b271eac feat(tmdb): dashboard trending + hero enrichment, cross-portal Similar rail (#1132)
* feat(dashboard): TMDB trending rail and hero enrichment (backdrop, badges, S/E)

Two dashboard additions, both async after first paint so the page renders
exactly as fast as before:

Trending rail ("Trending this week", dashboardRails.tmdbTrending toggle,
default on, rendered only when TMDB is opted in AND the Electron DB
worker is available):
- TmdbTrendingService fetches /trending/{movie,tv}/week (one request
  each, cached one day per language in tmdb_metadata under
  trending:week), merges by popularity; exposed via the enrichment
  facade (getTrendingWeek)
- DashboardTrendingService matches the titles against imported Xtream
  playlists with ONE batched DB_MATCH_TITLES request, applying the same
  two-tier + year-compatibility rule as actor pages; matched cards show
  the playlist name and navigate straight to the detail view, unmatched
  cards open the global search prefilled (?q=)
- The load fires only after the dashboard's own recent/favorites data
  is in (never competes for the worker at startup) and once per session
- DashboardRailCard gained optional queryParams for the search links

Hero enrichment:
- DashboardHeroTmdbService patches the hero with a TMDB backdrop (only
  when the item has none), a rating badge and up to two genre chips —
  via the enrichment facade, so previously opened items resolve from
  the SQLite cache without network; memoized per title per session,
  staleness-guarded against hero changes in flight
- Series heroes show the tracked "S{n}·E{n}" badge from the playback
  position; the watch-progress bar no longer applies to live heroes

Settings: new dashboardRails.tmdbTrending toggle in Settings > Dashboard.
i18n: 3 new keys translated into all 17 locales via tools/i18n patches.

Tests: dashboard-trending.service.spec.ts (gating, matching, year guard,
single-flight); settings fixtures updated. Docs updated
(tmdb-metadata-enrichment.md Dashboard Integration section, CLAUDE.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): resolve hero TMDB extras for Stalker embedded-series items

Stalker vclub items carry type 'movie' in activity rows but are TV shows
on TMDB, so the hero's movie lookup found no confident match and the
backdrop/badges never appeared — while the detail view (which resolves
via is_series) showed them. When a movie-typed hero item has no movie
match, retry the lookup as TV: the detail view has usually already
cached that resolution, and misses are negative-cached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(tmdb): cross-portal "Similar" rail — Stalker gets it, Xtream gains other-portal matches

The Similar rail only existed on Xtream because it matched against the
locally loaded catalog; Stalker catalogs are server-paginated, so its
detail views had no rail despite tmdb_recommendations being cached.

New CrossPortalSimilarService (libs/services) matches recommendations
against ALL imported Xtream playlists with one batched DB_MATCH_TITLES
worker request — the same two-tier normalized-title + year-compatibility
rule as actor pages and the trending rail. Electron-only; resolves to []
in the PWA.

- Stalker: the shared VodDetailsComponent (movies; covers catalog and
  inline detail hosts) and stalker-series-view (series) now render a
  "Similar" rail from cross-portal matches, each card badged with the
  source playlist and navigating into that portal's detail view.
- Xtream: vod/serial detail rails keep instant local-catalog matches and
  append cross-portal matches (current playlist excluded, deduplicated
  against local hits by normalized exact title), also playlist-badged.
- Loads async after the detail view renders, staleness-guarded; the
  section only appears when there is something to show.

Tests: cross-portal-similar.service.spec.ts (PWA gate, navigation
targets, playlist exclusion, type/year guards). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): drop TestBed from cross-portal similar spec

The services Jest target has no @angular/core/testing (same CI failure
as the cache spec earlier) — construct the service via Injector.create +
runInInjectionContext instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dashboard): address PR review — reactive opt-out gates, retry after empty trending load

- Trending rail and hero TMDB extras now vanish immediately when the
  TMDB opt-in is switched off mid-session: the render computeds read the
  settings signal through isAvailable/isEnabled instead of trusting data
  loaded earlier (Codex P2 ×2).
- loadedOnce latches only after a successful non-empty load, so a
  transient TMDB outage on first visit no longer suppresses the rail for
  the whole session — the next dashboard visit retries (greptile P2).
- Unified the duplicated heroTmdbExtras() read in the hero computed
  (greptile P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 12:21:11 +02:00
4grayandClaude Fable 5 ce24e16e32 feat(portals): two-state VOD detail pages (browse ↔ watch) with season tabs (#1127)
* feat(ui): slim ContentHero to non-scrolling hero block

The page scroll container moves out of the hero into the upcoming
PortalDetailShellComponent; the hero no longer hosts a default content
slot for player/seasons/extras.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): add PortalDetailShellComponent with browse/watch states

Two-state detail layout shell: owns page scroll, hero collapse animation
(~300ms, reduced-motion aware), Escape-to-close-player handling, and an
About block that re-stamps host-provided *detailTags/*detailMeta
templates in watch state. The [detail-player] slot is never wrapped in a
shell conditional so the host-owned player subtree survives state
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): restyle inline player header as now-playing bar

Adds a back button (emits the existing closed output — in watch state
back returns to browse, not route navigation) and replaces the
hardcoded header strings with i18n keys (PORTALS.NOW_PLAYING,
CLOSE_PLAYER, BACK_TO_BROWSE).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(xtream): migrate VOD and serial detail templates to detail shell

Hero chips/meta/actions become *detailTags/*detailMeta/*detailActions
templates; the inline player moves to the full-width [detail-player]
slot; trailer and similar rail move to [detail-extras]. Escape and the
now-playing back button close the inline player back to browse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(stalker): migrate shared VodDetails and series view to detail shell

The shared VodDetailsComponent (Stalker VOD, collections, search) and
StalkerSeriesViewComponent move their hero content into
*detailTags/*detailMeta/*detailActions templates and host the inline
player in the full-width [detail-player] slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): season tabs replace two-level seasons navigation

SeasonTabsComponent renders a pill row (dropdown beyond 6 seasons), an
optional season description and a back-to-playing chip. The season
container auto-selects a season (inline-playing episode's season →
most recent in-progress → first), emits seasonSelected for
auto-selections so lazy-load/enrichment hooks keep firing, highlights
the inline-playing episode, and loses the seasons grid + 'Back to
seasons' button. Download/progress helpers move to pure utils; season
strings are now translatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(portals): season descriptions under the season tabs

Xtream reads season overviews from get_series_info; Stalker exposes the
TMDB season overview through TmdbEnrichmentService.getSeason (same
cache rows as the episode enrichment) and keys it per tmdbId so views
reused across detail navigations cannot leak descriptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document two-state detail layout and season tabs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): app-prefix detail template directive selectors, build fixes

detailTags/detailMeta/detailActions → appDetailTags/appDetailMeta/
appDetailActions per @angular-eslint/directive-selector; type the
Escape host listener as Event; drop a redundant ?? in season tabs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(i18n): translate new detail-page keys across all 17 locales

13 PORTALS.* keys (now-playing bar, About block, season tabs, episode
empty states) translated via the i18n-fill workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): keep now-playing bar actions inside the viewport

The nowrap title made the header grid track grow to min-content and
pushed Copy/Close out of the page; min-width: 0 on the header/title row
lets the title truncate instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): expandable season description and episode info dialog

Season descriptions get the same show more/less toggle as the hero
(new shared ExpandableTextComponent); episode cards and list rows get
an info button that opens a dialog with the full plot, duration, air
date and a Play action — card heights stay fixed and click-to-play
semantics stay intact. Also: drop the per-frame backdrop blur on the
player card and make the watch-enter scroll instant so the hero morph
stops competing for frame budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): re-run TMDB season enrichment when the show match arrives

With season tabs the first seasonSelected fires as soon as seasons
load — usually before the async show-level TMDB enrichment has written
tmdb_id — so the season fetch silently no-oped and was never retried,
leaving episode stills/plots and the season description empty. Both
detail hosts now key the fetch on (tmdb_id, selected season) in an
effect, so it runs whichever arrives last. Also adds breathing room
above the About divider in watch state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): move episode info button into the card body, compact overlay actions

Three overlay buttons crowded the thumbnail on hover and the new info
button had no styling at all. Info belongs with the content text: it
now sits as a quiet ghost button at the end of the title row (revealed
on card hover, MDC touch target clamped so it cannot swallow the
play-on-click card area). Download + watched-toggle stay on the
artwork, shrunk to 28px with a subtle edge border. List view already
used the shared action style and is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): single watched indicator-toggle on episode cards

Watched state was signalled three times on one card: the green badge,
the hover toggle in the top-right cluster, and a checkmark appended to
the title. Now one control does both jobs — a toggle at the top-left
of the artwork that looks like the old green badge when watched
(always visible) and appears as a ghost circle on hover when not.
The right cluster keeps only the download action; the title checkmark
is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): back arrow in watch mode navigates straight to the list

Leaving the theater took two clicks: back to browse, then back to the
list. Watch state already shows everything browse offers (episodes,
About, extras), so the bar's back arrow is now route-level back
(new backClicked output wired to each host's goBack), while Close
player and Escape keep exiting to browse without navigating. Replaces
PORTALS.BACK_TO_BROWSE with a generic top-level BACK key (all locales).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui): compact episode stills in the list view

When a season has genuinely distinct per-episode art (TMDB stills or
provider images), list rows show a 96x54 thumbnail with the episode
number riding on it instead of the number square. When every episode
repeats the same image (providers often send the series poster), the
plain number square stays — a column of identical posters is worse
than none.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ui): compact list-thumbnail spec under the max-lines limit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): address Greptile review — real fallback asset, untracked overflow measure

default-episode.png never existed (pre-existing in the grid card, copied
into the list thumbs) — both onerror handlers now fall back to the real
default-poster.png. The expandable-text and content-hero measuring
effects read the expanded signal via untracked so toggling show
more/less no longer rebuilds their ResizeObservers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:28:00 +02:00
4grayandClaude Fable 5 dc05a2566e feat(tmdb): opt-in TMDB metadata enrichment for Xtream and Stalker portals (#1123)
* feat(tmdb): opt-in TMDB metadata enrichment for Xtream and Stalker portals

Adds an opt-in TMDB integration (Settings > Metadata) that enriches
detail views with a field-level merge — the provider stays authoritative
for stream data, TMDB fills editorial fields when the match is confident.

Enrichment:
- Movie/series details: plot, cast (avatar chips), director, genres,
  rating, poster/backdrop, official YouTube trailers
- Confidence-gated matching: provider tmdb_id trusted; otherwise
  normalized-title search with year gate (±1; series accept earlier
  premieres), season-suffix stripping, Cyrillic search-language override,
  and language-prefix fallback variants
- Lazy season/episode enrichment: real episode names, overviews, stills
- "Similar" rail (Xtream): TMDB recommendations matched to the catalog
- Actor pages per portal with full filmography, availability filter and
  an Electron-only "All portals" scope backed by a batched DB_MATCH_TITLES
  worker op over the trigram FTS index

Infrastructure:
- SQLite cache table tmdb_metadata (details, search verdicts, seasons,
  persons; per-language, TTL-guarded), in-memory fallback for the PWA
- Settings: enable toggle, own-API-key override with a live "check key"
  button; TMDB attribution in Settings and About
- Embedded key stays an empty placeholder; CI injects TMDB_API_KEY via
  tools/tmdb/inject-tmdb-key.mjs when the secret is configured
- normalizeTitle shared between renderer and DB worker
- CSP: allow YouTube embeds (frame-src was 'none'; trailers never worked)

Fixes and refactors along the way:
- fix(stalker): Advanced Search sent bare get_ordered_list requests and
  skipped the auth handshake when isFullStalkerPortal was missing on the
  active-playlist meta — full portals answered "Authorization failed."
  and search looked empty; now mirrors the catalog request shape and
  routes through makeAuthenticatedRequest with URL-based detection
- fix(stalker): TMDB fields survive info re-normalization; detail views
  prefer the store copy patched by async enrichment over stale snapshots
- refactor(xtream): split oversized vod/serial detail components into
  component-scoped playback services; detail routes re-initialize on
  route param changes (router reuses them for detail-to-detail nav)
- i18n: all new keys translated across the 18 locales

Docs: docs/architecture/tmdb-metadata-enrichment.md + CLAUDE.md updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): provide route params observable to inline collection details, linearize regexes

The global-collection inline detail host builds a fake ActivatedRoute for
VodDetailsRouteComponent/SerialDetailsComponent with only snapshot.params.
Since the detail components now read route.params via toSignal() (detail->
detail re-init), the missing observable crashed component construction and
the content hero never rendered — broke dashboard-activation, favorites and
recent Electron E2E on all platforms. Provide the params observable
alongside the snapshot and assert it in the component spec.

Also resolves both CodeQL js/polynomial-redos alerts: bracket-stripping in
normalizeTitle now excludes opening delimiters inside the classes, and
youtubeEmbedUrl extracts watch?v= ids with a linear two-pass match instead
of "watch\?.*v=". Combining-diacritics range rewritten as explicit \u
escapes (greptile note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): surface TMDB-only VOD score in the rating badge, drop youtube.com from CSP

Review follow-ups on PR #1123: the Xtream VOD detail badge renders
rating_imdb, but the merge wrote the TMDB score only into `rating`, so a
TMDB-only score was never displayed (Codex P2) — fill rating_imdb when the
provider left it empty, mirroring the Stalker merge. All trailer iframes
are normalized to youtube-nocookie.com, so the extra youtube.com frame-src
allowance was dead surface (greptile) — removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): resolve confirmed review findings — matching correctness, race guards, cache schema

Fixes the confirmed findings from the PR #1123 code review:

- Stalker search: setSelectedContentType now runs BEFORE setSelectedItem,
  so the TMDB enrichment gate in the selection hook no longer sees the
  content type of the previously open tab (wrong/no enrichment after
  ITV -> search -> movie).
- Title normalization is now two-tier (normalizeTitleKeys): the exact
  normalized form keeps a trailing year, the base form strips it and
  remembers the tag. Year stripping is anchored to the end of the title
  ("2001: A Space Odyssey" keeps its year) and language-prefix stripping
  is UPPERCASE-only ("It: Chapter Two" is no longer amputated).
- All catalog matching (similar rail, actor pages, DB worker
  DB_MATCH_TITLES) compares exact forms first and only accepts
  year-stripped matches when the stripped tag is year-compatible (+-1)
  with the TMDB year — "Blade Runner" (1982) can no longer claim a
  catalog "Blade Runner 2049". CatalogTitleMatch carries the stripped
  trailingYear so the renderer can apply the guard to worker matches.
- mergedBackdrops tolerates a plain-string backdrop_path; enrichment
  merge+patch blocks are wrapped in try/catch so a malformed provider
  payload can no longer become an unhandled rejection.
- loadGlobalMatches (both actor routes) guards against actor->actor
  navigation races — a slow match for the previous person no longer
  overwrites the current one's results.
- tmdb_metadata media_type CHECK widened to ('movie','tv','person') and
  person rows now use the honest 'person' type (TmdbCacheMediaType).
  Pre-release dev DBs with the narrow CHECK are rebuilt in place — the
  table is a pure cache, so the migration is a self-healing
  drop-and-recreate keyed off sqlite_master.

Docs updated (tmdb-metadata-enrichment.md, CLAUDE.md). New regression
coverage: title-normalization.util.spec.ts, two-tier cases in
tmdb-similar.util.spec.ts and title-match.operations.spec.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:07:46 +02:00
4grayandClaude Fable 5 8eb0fe3261 feat(playback): embedded mpv pre-0.22 hardening, Linux parity, and localization (#1122)
* fix(playback): harden embedded mpv session handling and support detection

- guard the session controller against late startup rejections clobbering
  a newer session during fast channel zapping
- exclude the refresh timestamp from the session-update dedup key so idle
  sessions stop re-emitting IPC updates every 500 ms
- macOS: reconcile async loadfile replies by request id so a rejected
  seek/aid/speed on a live stream no longer flips the session to error
- append --ozone-platform=x11 on Linux in main.ts so direct binary and
  AppImage launches match the packaged .desktop launcher behavior
- return a sandbox-specific unsupported reason in Flatpak/Snap instead of
  asking the user to install mpv inside the sandbox
- update the stale "macOS only" embedded MPV claim in CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): populate Linux audio tracks and fix ARM Linux packaging

- Linux poller now reads track-list/count each tick and walks the scalar
  track-list/N/* sub-properties when the count changes, so the audio-track
  menu is no longer empty; selection reconciles from the aid property
- afterPack replaces the x64 embedded_mpv.node with an
  embedded-mpv-unavailable.txt marker in arm64/armv7l Linux packages, and
  package-layout verification rejects foreign-architecture addons while
  requiring the marker
- extend native source invariants for the non-fatal async-reply rule
  (macOS) and the Linux track-list polling contract
- document the Linux track-list mechanics and ARM packaging behavior in
  docs/architecture/embedded-mpv-native.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): embedded mpv player UX polish and full localization

- click on the video toggles pause (same action as Space) with a 250 ms
  grace period so double-click fullscreen cancels the pending pause; no
  DOM overlay is drawn — the dock transport icon is the feedback
- timeline scrubbing previews the drag position locally and commits a
  single seek on release instead of one IPC seek per drag pixel
- translate all player UI strings (controls, tooltips, aria-labels,
  status and recording messages) via new EMBEDDED_MPV.PLAYER.* keys,
  synced across en + 17 locales through the i18n-fill workflow
- replace the legacy @Output() EventEmitter with the signal output() API

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): react to language changes and respect ozone platform hint

Address review feedback on #1122:
- add a translationsTick signal (onLangChange/onTranslationChange/
  onDefaultLangChange) read by every computed() and template helper that
  calls translate.instant(), so labels re-evaluate on a runtime language
  switch and when the translation file finishes loading after mount
- suppress the Linux --ozone-platform=x11 fallback when the user set
  ELECTRON_OZONE_PLATFORM_HINT, matching the existing respect for an
  explicit --ozone-platform switch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:22:48 +02:00
4grayandClaude Fable 5 3659de796c chore(docs): remove external wiki-export tooling and instructions (#1121)
The wiki export to an external Obsidian vault is unused. Remove the
wiki:export/test:wiki-export npm scripts, the external-wiki-sync
architecture doc, and the IPTVNATOR_WIKI_VAULT workflow instructions
from CLAUDE.md and AGENTS.md.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:15:45 +02:00
4grayandClaude Fable 5 1c710d69ef chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps (#1116)
* chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps

Removes verified-dead components (0 class/selector references outside
their own files): EpgListComponent (+ epg-list-item), EpgViewComponent,
LiveEpgPanelComponent, StalkerCollectionChannelsListComponent,
NavigationComponent, FilterSortMenuComponent, video-player
ToolbarComponent, PortalCollectionShellComponent and
LoadingOverlayComponent, together with their barrel exports.

Alive code extracted from the deleted trees:
- LiveEpgPanelSummary -> libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts
- EpgProgramActivationEvent -> libs/ui/epg/src/lib/epg-program-activation-event.ts
- epg-list.utils.ts trimmed to the three timeline-used helpers and moved
  to libs/ui/epg/src/lib/epg-program.utils.ts
- epg-item-description/ moved up out of the deleted epg-list/ folder

Also removes 18 i18n keys now unused (from all 18 locales), dead CSS
selectors targeting the deleted elements, and unused dependencies:
lodash (+ @types/lodash), semver, @ngrx/component-store and
@videojs/http-streaming (videojs-quality-selector-hls declares no peer
dependency on it; video.js 8 bundles VHS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(coverage): move shared-portals to Tier C, fix stale doc references

The Tier A gate failed in CI because deleting the dead epg-view and
live-epg-panel components removed the only specs in libs/ui/shared-portals.
The lib now contains a single type-only interface (LiveEpgPanelSummary),
so there is no runtime code to unit test; reclassify it to Tier C with a
documented reason, matching the gate's own guidance.

Also update remaining doc references to the deleted components in
docs/architecture/stalker-epg.md, iptvnator-ui-guidelines.md and
CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:09:50 +02:00
4grayandClaude Fable 5 e14b8ae8d9 feat(ci): enforce lint, guard coverage policy, add max-lines rule (#1117)
* fix(lint): resolve module-boundary and prefer-inject errors

Retag workspace-shell-util as type:data-access to match its injectable
services that depend on @iptvnator/services, and convert
RemoteControlService to inject(HttpClient).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(lint): enforce max-lines 400 with generated baseline

Add a max-lines ESLint error (hard cap 400 raw lines per TypeScript
file) per the repo file-size rule. The 134 pre-existing offenders are
baselined in tools/eslint/max-lines-baseline.mjs, regenerable via
generate-max-lines-baseline.mjs; the list should only shrink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ci): enforce lint on PRs and guard coverage policy drift

- Add a Lint job to ci.yml running nx run-many -t lint --all, so
  module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
  with a test target is missing from coverage-policy.json; wired into
  coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
  policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document CI lint enforcement and coverage policy guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): address bot review feedback on policy guard and baseline generator

- Drive Tier B/C validation from each policy entry's validationCommand
  (falling back to nx test), skipping projects with an e2e target since
  the E2E workflow already runs them (Codex).
- Fail when a Tier A entry has no test target (Greptile, adapted:
  checking all entries against test targets would false-positive on the
  intentionally spec-less e2e/mock-server tiers).
- Guard against missing JSON array in nx show projects output (Greptile).
- Scan .tsx files in the max-lines baseline generator to match the
  ESLint rule's file patterns (Greptile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 10:06:45 +02:00
4grayandClaude Fable 5 f015ec1ca9 docs(agents): rewrite stale CLAUDE.md architecture sections, add self-maintenance rule
Rewrite architecture sections against the current filesystem: post-reorg
lib paths (libs/portal/*, libs/playlist/*, libs/workspace/*), /workspace
route tree, canonical libs/shared/database schema (10 tables,
better-sqlite3), renamed channel-list views, EPG timeline ribbon,
18 i18n locales, import flows in libs/playlist/import/feature, and drop
the unused @ngrx/component-store recommendation.

Add a rule to Documentation After Changes (mirrored in AGENTS.md):
agents must keep CLAUDE.md/AGENTS.md up to date when changes touch
anything they describe, and fix stale claims they notice in passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 00:31:25 +02:00
4gray 1badb9a057 refactor(electron): share typed preload bridge contract (#1018)
* refactor(electron): share typed preload bridge contract

* refactor(electron): tighten bridge review fixes

* refactor(electron): tighten playlist bridge returns

* refactor(electron): use bridge epg progress type

* refactor(epg): alias progress bridge types
2026-06-01 09:52:22 +02:00
4gray 4847eb5241 refactor(electron): split epg event services
Split EPG IPC orchestration, worker lifecycle, and query logic into focused services. Harden clear-worker lifecycle after review with timeout/exit handling and regression coverage.
2026-05-27 22:05:40 +02:00
4gray edaa981061 refactor(electron): split external player sessions
Split the Electron external-player IPC monolith into focused launch-context, playback-request, runtime, MPV session, and VLC session modules. Includes Greptile follow-up fixes for Homebrew Cask VLC path resolution and VLC spawn-error promise handling, with regression coverage.
2026-05-26 23:15:28 +02:00
4gray 2d5c4fa4f9 chore: tighten validation and runtime logging
* chore: tighten validation and runtime logging

* fix(i18n): localize new settings labels
2026-05-15 17:43:42 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00
4grayandClaude Opus 4.7 a74b8f1b81 fix: prevent macOS sleep during embedded MPV playback
The embedded MPV player renders via libmpv into a custom Cocoa view, which
bypasses mpv's built-in screensaver inhibition. Hold an Electron
powerSaveBlocker (prevent-display-sleep) while any session is playing and
release it on pause, dispose, or shutdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-04 23:47:33 +02:00
4gray 911ec1369c docs: add regression prevention and test updates section to AGENTS.md and CLAUDE.md
Entire-Checkpoint: c2cea9c530e6
2026-04-30 21:16:24 +02:00
4gray c0e045f45c docs: update documentation guidelines for meaningful changes and Nx usage
Entire-Checkpoint: c6e522b4276c
2026-04-17 21:36:31 +02:00
4gray f4f1993434 docs: update settings and documentation for enhanced agent guidance and radio player support 2026-04-06 22:46:24 +02:00
4gray 992b996ecd feat: general development, update core application components, and enhance build scripts. 2026-03-01 22:52:33 +01:00
4gray 61d34aef7a chore: switch to pnpm 2026-02-19 23:08:36 +01:00
4gray 5782c70a24 refactor: rename xtream-tauri references to xtream-electron in documentation 2026-02-19 21:47:38 +01:00
4gray 8b6572295a docs: enhance CLAUDE.md with M3U playlist module architecture details 2026-02-13 00:45:55 +01:00
4gray 7ee95bf606 docs: document xtream store, category management 2026-01-11 21:22:28 +01:00
4gray 892857f000 docs: add Angular signal-based coding standards 2025-12-06 21:44:25 +01:00
4gray 13c05c85b0 docs: normalize list indentation and fix formatting
Adjust documentation lists and code blocks for consistent
indentation and spacing across CLAUDE.md. Convert mixed dash
and nested list levels to use uniform two-space indentation for
subitems, reflow a few wrapped lines, and add blank lines where
needed to separate sections and examples. These changes improve
readability and maintain a consistent markdown structure for the
project documentation.
2025-11-23 18:50:46 +01:00
4gray e079d268f0 docs: add CLAUDE.md with developer guidance 2025-11-08 10:22:27 +01:00