Commit Graph
80 Commits
Author SHA1 Message Date
4grayandClaude Fable 5 7fc9380bff feat(portals): mark a full season as watched in one click (#1447)
* feat(portals): mark a full season as watched in one click

Series detail pages on both Xtream and Stalker portals get a season-level
watched toggle next to "Download season": marking writes full-progress
rows for the unwatched episodes only (real durations survive), a fully
watched season flips the action to unwatch-all.

Persistence goes through new batch IPC channels
(DB_SAVE/CLEAR_PLAYBACK_POSITIONS_BATCH, one SQLite transaction with
onConflictDoUpdate().run(); the PWA data source rewrites its
localStorage blob once). Stalker deliberately bypasses the batch IPC
and loops the existing position-mutation queue so legacy-row
reconciliation still runs and the queue coalesces to a single reload;
partial failures surface a dedicated snackbar.

Also removes the dead toggleEpisodeWatched store method, splits
season-container/serial-details-playback under the max-lines cap
(season-watch-toggle.util.ts, SerialDetailsSeasonWatchService), and
classifies *.spec-data.ts fixtures under the test max-lines ceiling
(baseline shrinks by main.preload.spec-data.ts).

Closes #1442

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

* fix(portals): guard stale season batches and split partial-unwatch feedback

Review follow-up (Codex on #1447):
- A season batch completing after the user navigated to another series
  or playlist no longer writes the old series' rows into the freshly
  reset position state (episode ids can collide across playlists); the
  Xtream host captures the playlist/series identity before awaiting and
  skips the rendered-state mutation when it changed. The DB write is
  unaffected — it carries its own playlistId.
- A partially failed "mark season as unwatched" on Stalker now reports
  a dedicated SEASON_MARKED_UNWATCHED_PARTIAL message instead of the
  watch-direction "marked" text; translated into all 18 locales.

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

* fix(portals): exclude the playing episode from season marking and count partial saves

Second review round (Codex on #1447):
- The episode currently playing (inline or in an external session, or
  with a launch in flight) is excluded from a season's mark-watched
  batch: the player persists its live position every ~15 s and would
  immediately overwrite the just-written full-progress row. The button
  count reflects the exclusion and the action disables when nothing is
  markable. Unmarking still clears such an episode — the recreated
  in-progress row reflects live playback truthfully.
- A Stalker StalkerSeriesPositionPartialSaveError (scoped watched row
  saved and published, only legacy cleanup failed) now counts as a
  watched success instead of feeding false total-failure feedback.

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

* fix(portals): gate stale season-batch snackbars on the originating page

Third review round (Codex on #1447): a batch resolving after the user
navigated away no longer shows its contextless success/error snackbar
on the newly opened detail page — the same ownership check that guards
the state mutation now guards the feedback too.

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

* fix(portals): sync catalog progress badges after toggles and gate Stalker feedback

Fourth review round (Codex on #1447):
- Any Xtream watched toggle (single episode or season batch) now
  refreshes XtreamStore.loadAllPositions after persisting — the catalog
  reads series-progress badges from the store, which otherwise loads
  positions once per playlist, so returning from the detail kept stale
  badges. Skipped when the playlist changed mid-flight (the store then
  belongs to the other playlist; its own init reloads positions).
- Stalker's season snackbars are gated on the captured playlist/series
  identity, matching the Xtream ownership guard — a batch draining after
  navigation no longer reports on the newly opened page.
- Stalker season-toggle specs moved to stalker-series-view.season-watch
  .spec.ts with their own harness; both prior spec files sat at the
  1200-line test ceiling.

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

* docs: describe the season watched toggle in CLAUDE.md

Fifth review round (Codex on #1447): the canonical Seasons entry in the
VOD/Series detail section now covers the bulk toggle, its playing-episode
exclusion, both persistence paths, catalog badge sync, and the
stale-completion contract.

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

* fix(portals): let only the latest positions load patch the Xtream store

Sixth review round (Codex on #1447): loadAllPositions is now
latest-load-wins — a fetch superseded while in flight (playlist switch
before getAllPlaybackPositions resolves) no longer patches the singleton
store with the previous playlist's position maps, which could leave the
new catalog showing the old playlist's progress badges.

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

* docs: reflect the spec-data max-lines classification in CLAUDE.md and AGENTS.md

Seventh review round (Codex on #1447): both canonical max-lines
descriptions now list **/*.spec-data.ts among the test-ceiling globs so
future agents neither treat these fixtures as production files nor
remove the exemption unknowingly.

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

* fix(portals): parse "N min" durations when marking episodes watched

Eighth review round (Codex on #1447): Stalker VOD episodes report
durations like "45 min", which parseDuration could not read — bulk (and
single) mark-watched then persisted 1/1-second rows. The minute format
now parses to seconds, matching what the removed legacy store method
already handled.

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

* fix(portals): parse compound hour durations and cover the toggle end-to-end

Ninth review round (Codex on #1447):
- parseDuration now reads the compound "1h 30min" form the Xtream
  fixtures emit (hour group optional, so "45 min" keeps working) —
  bulk-marked episodes no longer persist a minutes-only duration.
- New Playwright coverage exercises the season toggle through the real
  UI on both portals: Xtream (category → series detail → mark →
  reload-persistence → unmark) and Stalker (embedded-series flow,
  mark → unmark with the item's actual episode count).

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

* fix(portals): refresh Stalker catalog progress badges after watched toggles

Tenth review round (Codex on #1447): the Stalker mirror of the Xtream
catalog sync — StalkerCatalogFacadeService loads its position maps once
per playlist and the runtime bridge only pushes external-player updates,
so renderer-initiated toggles left grid badges stale. The series view
now calls the facade's new ownership-checked refreshPositions after the
season batch (including partial successes) and after single toggles;
the reload is latest-load-wins like the Xtream store fix. Optional
injection keeps collection-detail mounts outside the catalog working.

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

* test(portals): cover the season toggle batch IPC end-to-end in Electron

Eleventh review round (Codex on #1447): the new Electron E2E marks a
season through the real UI, asserts the eight SQLite rows written by
DB_SAVE_PLAYBACK_POSITIONS_BATCH directly through the preload bridge,
proves persistence with a full app relaunch (renderer and main process
die, so state can only come from the database file), and clears again
through DB_CLEAR_PLAYBACK_POSITIONS_BATCH back to zero rows.

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

* fix(dashboard): keep watched rows out of the series resume target

Twelfth review round (Codex on #1447): a watched position row — a
natural finish or a manual/bulk "mark watched" marker — is a completion
record, not resumable progress. Continue Watching no longer auto-plays
such an episode at its end; the handoff stays detail-only and the series
page's quick-start picks the first unwatched episode instead. Card
progress bars and SxxEyy badges keep their current source.

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

* fix(portals): fail closed on refresh reads and gate batch APIs by capability

Thirteenth review round (Codex on #1447):
- Position-cache refreshes now use a failure-propagating read
  (getAllPlaybackPositionsOrThrow through the Electron data source): a
  transient IPC failure rejects instead of masquerading as an empty
  list, so a populated store/facade cache stays stale-but-populated
  rather than being wiped. All load/refresh call sites handle the new
  rejection (init loads may retry on the next activation; post-toggle
  refreshes log and keep the snackbar flow).
- The season-batch bridge methods joined playbackPositionStorageMethods,
  so a bridge lacking them degrades to the in-memory path wholesale
  instead of throwing mid-action.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 21:03:14 +02:00
4grayandClaude Opus 5 e94cc029eb fix(portals): time out and fast-fail PWA proxy requests to dead hosts (#1424)
* refactor(portals): hoist the connectivity guard into libs/shared/host-health

The breaker was written dependency-free so both processes that talk to
portals could share it. Move the part that has no Electron in it — the
state machine, the failure classification, the redirect-attribution
helpers and the fast-fail error — into `@iptvnator/shared/host-health`
(`scope:shared` / `domain:shared-runtime` / `type:util`).

What stays in `apps/electron-backend` is the genuinely main-process part:
one guard for the whole process, so both portal IPC handlers see each
other's evidence, and the console warning that announces it. Every call
site is unchanged; the wrapper re-exports the two types they import.

The spec splits the same way — the state machine moves with the class,
the singleton and its redirect attribution stay with the wrapper.

Register the new project in the coverage policy, which every project with
a test target must declare a tier for.

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

* fix(portals): time out and fast-fail PWA proxy requests to dead hosts

The web backend's proxy routes were bare `axios.get()` calls with no
`timeout`, so a provider that accepted a connection and then went silent
held the request until the OS gave up on the TCP connection — minutes,
rather than the 15/30 s budget the Electron handlers use. Add the same
per-route timeouts (Xtream 30 s, Stalker 15 s / 30 s for `create_link`,
playlist and XMLTV 30 s).

Those numbers are safe for large downloads: on axios' default transport
`timeout` bounds the time to response headers and then continues as the
socket's inactivity timeout, so a multi-megabyte XMLTV file that keeps
delivering bytes is never cut off — only a stalled one is.

With requests bounded, run `/xtream` and `/stalker` through the shared
breaker, injected via `WebBackendAppOptions.hostGuard` so specs drive it
with a clock they own. Playlist and XMLTV downloads keep the timeout but
no breaker, matching Electron: a download is one request rather than a
catalog fan-out, it is usually the direct result of the user asking for
it, and it can outlive the half-open trial window.

The breaker is checked before the Xtream URL revalidation, which resolves
the hostname — a dead host is where DNS is slow too, and a request
admitted and then abandoned by the URL policy hands its token back rather
than holding the trial slot.

`resetHostConnectivityGuard()` no longer no-ops in the PWA: the breaker
lives in the backend process, so it travels to a new
`POST /connectivity-guard/reset`, which reads only the origin and never
logs the credential-bearing URL. `skipConnectionGuard` now survives the
PWA transport too, so Stalker endpoint discovery keeps the exemption it
has on the desktop instead of tripping the breaker with its own probes.

A fast-fail keeps each route's HTTP 200 `{message, status}` envelope. The
Stalker path needs one extra step: `forwardStalkerRequest` turns that
envelope into `HTTP Error <code>: …` with a numeric `status`, and the
renderer reads both as "the endpoint answered" — which would make
discovery walk every candidate and fire lazy repair at a host just
declared dead. A prior branch keyed on the shared
`isHostConnectivityFastFailMessage` rethrows it bare instead.

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

* fix(portals): report exempt Stalker probe responses to the PWA breaker

Flagged by the author of #1421 as one of the twelve fixes that landed
there after this branch cherry-picked the pre-review commit: an exempt
discovery probe must still REPORT, it just must not COUNT.

The web backend was skipping the report entirely for a probe, which loses
the case that matters. A failure carrying an HTTP response proves the
endpoint answered, and this route sets no `validateStatus`, so axios
rejects every non-2xx with `error.response` attached — a probe answered
with 404 or 500 was therefore dropped instead of clearing the record.
Two counted failures either side of it then read as consecutive and
opened the breaker in the middle of discovery, which is exactly what the
exemption exists to prevent.

`reportProviderRequestFailure` now takes `countFailures`, matching the
Electron reporter, and both routes always report.

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

* fix(portals): read the redirect hop from the transport that followed it

`failedAfterRedirect` decided whether a failure belonged to a redirect
destination by reading `error.config.url`. That is right for the Electron
transport, which sets `maxRedirects: 0` and reissues every hop as its own
request, so the hop IS the config URL. It is blind on the web backend,
which uses axios' default transport: follow-redirects walks the chain
inside one request and `config` is built once, so `config.url` stays the
URL we asked for.

Verified against the installed axios 1.19.0 with a live server that 302s
to a dead port:

    asked for          : http://127.0.0.1:63953/player_api.php
    config.url         : http://127.0.0.1:63953/player_api.php
    request._currentUrl: http://127.0.0.1:1/dead

So the comparison was original-vs-original, found no redirect, and
charged two dead destinations to the provider that had answered both
times with a 302 — then fast-failed it. Read `request._currentUrl` first
and fall back to `config.url`, which covers both transports; Electron's
native per-hop requests expose no `_currentUrl` and are unaffected.

Also check redirect attribution BEFORE suppressing failure counting for
an exempt probe. A 3xx from the guarded endpoint is an answer, so a probe
that observed one must clear the record; otherwise a timeout, a probe
redirected to a dead destination, and another timeout still read as two
consecutive failures.

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

* fix(portals): stop axios query params reading as a redirect

Codex found that the Xtream breaker never opened at all, and it was
right. The web backend passes credentials and the action through axios'
`params`, so axios sends `…/player_api.php?username=…&action=…` while the
baseline handed to `failedAfterRedirect` is the query-less URL the route
built. Verified against axios 1.19.0 with a plain ECONNREFUSED and no
redirect anywhere in sight:

    baseline           : http://127.0.0.1:1/player_api.php
    request._currentUrl: http://127.0.0.1:1/player_api.php?username=demo&…

The two normalized URLs differ, so every ordinary failure looked like a
post-redirect failure, credited the endpoint, and the breaker could never
trip.

Compare origin and path, not the whole URL. That keeps what the check is
for — an endpoint that answered and sent us elsewhere, including the
same-origin `/player_api.php` → `/slow/player_api.php` case — and gives
up only a redirect that changes nothing but the query, which is then
counted as an ordinary failure. Erring towards counting is the safe
direction here.

The reason 57 tests passed over a dead feature is the real lesson:
`StubHttpClient` threw bare `Error`s, so the guard's redirect check saw
neither `config.url` nor `request._currentUrl` and quietly did nothing.
The stub now shapes its rejections like axios does, including the query
axios appends. With that alone, four existing tests fail against the old
comparison.

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

* fix(portals): count a hostname that stops resolving, and fix a stale docblock

Two from review, one behavioural and one documentation.

A name that will not resolve is the host failing to answer — the same
evidence as the ENOTFOUND the transport would have raised a moment later.
But the SSRF validation turns a lookup failure into a 400 "host could not
be resolved", and the release path added earlier handed the token back as
inconclusive, so the breaker could never open for a host whose DNS died
and every request kept paying for the same dead lookup.

`ProviderUrlError` now carries the underlying lookup error internally.
A refusal that has one is counted; a genuine policy refusal — private
address, bad scheme, credentials in the URL — still only releases the
half-open slot, because that says nothing about reachability. The field
is internal: `providerUrlErrorBody()` strips it at both call sites, so
the client sees exactly the body it saw before, which the test asserts.

The docblock on `resetHostConnectivityGuard` still said the PWA channel
is unknown and the call no-ops. That stopped being true when this branch
implemented `CONNECTIVITY_GUARD_RESET` over HTTP, and a stale contract
there is how the next caller silently skips the PWA path. (The edit was
in an earlier commit and was lost when the branch was rebuilt on master.)

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

* docs(portals): record the transport-specific redirect contract

The redirect section still described one transport: hop-by-hop requests,
`error.config.url`, whole-URL comparison. Two of those three are now
wrong for the web backend, and this document is the canonical contract —
leaving it stale is how the attribution bugs fixed in the last two
commits get reintroduced.

Says what is actually true: which field holds the failed hop on each
transport and why the helper reads both, and that the comparison is
origin + path because the web backend's credentials ride in axios'
`params` and a whole-URL comparison therefore reported a redirect for
every ordinary failure.

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

* docs(portals): scope the guard summary to both processes

The opening line still defined the breaker as an Electron main-process
concern, which contradicted the ownership section below it and is the
part a reader skims to decide whether the document applies to them.

Names both processes, and records that the web backend had the worse
version of the problem first — no request timeout at all — since that is
why the timeouts and the breaker had to land there together.

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

* fix(portals): declare the shared-interfaces dependency of host-health

The new library's manifest listed only `tslib`, but its emitted JavaScript
does `require('@iptvnator/shared/interfaces')` — the guard builds its
fast-fail message with `buildHostConnectivityFastFailMessage`. Anything
resolving the built artifact from its own manifest would have failed with
MODULE_NOT_FOUND.

The manifest was copied from `shared/logging`, which imports nothing
across libraries and therefore needs nothing beyond `tslib`.
`shared/m3u-utils` is the right precedent: it imports the same library
and declares `"@iptvnator/shared/interfaces": "0.0.1"`.

Verified against the build output rather than by inspection — the emitted
`host-connectivity-guard.js` requires the module, and the generated
`dist/libs/shared/host-health/package.json` now declares it.

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

* fix(portals): bound the PWA connectivity-guard reset

The reset was a bare `fetch` with no timeout, which is the exact failure
this change exists to remove, reintroduced one layer up. Every caller
awaits the reset BEFORE issuing the request it is clearing the way for —
`retryContentInitialization` awaits it first by design — so a backend or
reverse proxy that accepts the POST and then goes quiet would leave
Retry doing nothing at all, for as long as the socket stayed open.

Bound it with an AbortController and a 5 s timer. The abort rejects,
`resetHostConnectivityGuard` swallows it as it already does for any
other failure, and the caller proceeds to its real request — which is
what "best effort" was supposed to mean. The timer is cleared in a
`finally`, and it covers the body read as well as the headers.

Five seconds because this talks to the user's own backend rather than a
provider: it should answer immediately, and a slow one must not hold up
the retry that asked for it.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 22:27:05 +02:00
4grayandClaude Opus 5 0f7d529b40 refactor(portals): split the Stalker collection detail component (#1433)
StalkerCollectionDetailComponent was 529 non-blank lines, well over the
400-line production maximum, and passed lint only because it sat in
tools/eslint/max-lines-baseline.mjs — a list CLAUDE.md says must only
shrink.

Extract four cohesive units into siblings in the same folder:

- stalker-collection-playback.controller.ts — playback ownership, inline
  playback and the external-player fallback
- stalker-collection-detail-mode.ts — pure item / detail-mode / category
  resolution (movie vs regular series vs embedded VOD series vs lazy
  is_series)
- stalker-collection-favorites.controller.ts — favorites resource, sync
  and toggle
- stalker-collection-store-snapshot.ts — store snapshot capture/restore

Both controllers follow the existing StalkerVodPlaybackController model —
plain classes taking a config object, constructed in a component field
initializer — rather than DI services. The playback controller needs the
component's `item` input signal and the favorites controller's rxResource
needs an injection context; a field initializer supplies both for free,
where a DI service would have needed an extra effect to feed the input
across.

Public template bindings and the component's public API are unchanged:
the signals are re-exported by reference, so neither the template nor the
spec sees the split. The spec is untouched and its 11 tests, plus the
rest of the project suite (272), pass exactly as before.

Regenerate the max-lines baseline so the file drops off it, and update
the three doc references that named resolveDetailMode() and
resolveSelectedCategory() as methods on the component.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 17:44:33 +02:00
4grayandClaude Opus 5 ea4214a0d8 ci(embedded-mpv): add pinned mirrors to the Linux runtime source download (#1427)
The "Build pinned Linux Embedded MPV runtime" job failed twice on 2026-08-11
because www.freedesktop.org answered GitHub runners with HTTP 418 for the
fontconfig tarball. The Linux builder curled a single pinned URL with no
fallback, so upstream rate-limiting reddened the build.

Route downloadArchive() through the shared downloadPinnedSource() helper the
macOS builder already uses, and pin a mirror for each single-host source:
fontconfig and libdisplay-info (freedesktop-hosted) plus freetype, which the
macOS builder already mirrors. Each mirror was downloaded and verified to hash
to the existing pin. The curl hardening flags and assertArchiveMatchesPin are
unchanged, and the helper verifies every candidate against the same SHA-256,
so a mirror serving different bytes is rejected rather than used.

Unlike macOS, the Linux manifest keeps sourceUrl at the canonical pinned value
even when a mirror served the bytes: notice generation and the Snap publication
boundary compare that field against the immutable pin. A used mirror is logged
instead.

build-linux-runtime.mjs now imports the downloader, so download-pinned-source.mjs
joins the released source-archive tooling set (otherwise the archive would ship
a build script it cannot run) and the Linux runtime cache key.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:57:54 +02:00
dependabot[bot]and4gray 73f6eb9b17 chore(deps): bump the actions-minor-patch group with 2 updates (#1403)
* chore(deps): bump the actions-minor-patch group with 2 updates

Bumps the actions-minor-patch group with 2 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `pnpm/action-setup` from 6.0.9 to 6.0.10
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v6.0.9...v6.0.10)

Updates `github/codeql-action` from 4.37.4 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.4...v4.37.6)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-patch
- dependency-name: github/codeql-action
  dependency-version: 4.37.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* test(packaging): allow updated pnpm action

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-08-11 02:43:53 +02:00
4gray 728df1a68c fix(packaging): restore Snap desktop runtime (#1406)
* fix(packaging): restore Snap desktop runtime

* docs(packaging): publish Snap launch repair note

* fix(packaging): declare Node 22.12 floor

* docs(architecture): update SQLite pin rationale

* fix(tooling): align Node engine floor

* fix(tooling): constrain supported Node releases

* docs(architecture): correct node-abi consumer
2026-08-11 02:08:30 +02:00
4gray 87dc45957b chore(deps): align Angular packages on 21.2.19 (#1383)
* chore(deps): align Angular packages on 21.2.19

* docs(deps): align Vite patch references
2026-08-09 09:49:38 +02:00
dependabot[bot]and4gray 7103f7e734 chore(deps): bump pnpm/action-setup from 4 to 6.0.9 (#1372)
* chore(deps): bump pnpm/action-setup from 4 to 6.0.9

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 6.0.9.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4...v6.0.9)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.9
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* test(packaging): allow pnpm action setup v6

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-08-08 09:39:52 +02:00
4gray d9a763e77d fix(build): prevent Vite dev transform overflow (#1379)
* fix(build): prevent Vite dev transform overflow

* fix(build): preserve commented Vite URL imports
2026-08-08 08:03:09 +02:00
4gray fd96b85c19 feat(playback): recommend recovery actions (#1374)
* docs(playback): design recovery recommendations

* docs(playback): plan recovery recommendations

* refactor(playback): extract diagnostic utilities

* feat(playback): define recovery recommendation contracts

* feat(playback): rank recovery recommendations

* feat(playback): track session recovery attempts

* feat(playback): identify content recovery sessions

* feat(ui): add ranked playback diagnostic panel

* feat(playback): switch temporarily to recommended players

* test(playback): cover temporary player recommendation

* test(playback): verify recommendation capability guards

* docs(playback): document recovery recommendations

* fix(playback): keep recovery keys credential-free

* fix(playback): remove derived tracking ownership

* fix(playback): preserve distinct recovery fallbacks

* fix(playback): reset resume for new sources

* fix(playback): preserve desktop recovery guidance

* docs(playback): clarify recovery policy exceptions

* fix(playback): reject stale progress updates

* fix(playback): keep protected recovery guidance neutral

* test(playback): cover stale progress output

* fix(playback): neutralize protected diagnostic copy

* fix(playback): harden runtime guidance ownership

* fix(playback): stabilize recovery application ownership

* fix(ci): classify playback util coverage

* fix(e2e): preserve playback fixture bytes
2026-08-08 01:04:39 +02:00
4gray d2a83164ec feat(stalker): protocol-correct auth lifecycle (#1354) 2026-08-03 23:06:47 +02:00
4gray 96facd6f49 feat(downloads): queue season episode downloads (#1357)
* docs(downloads): specify season queueing

* docs(downloads): plan season queue implementation

* feat(downloads): define episode queue identity

* fix(downloads): align episode identity contract

* feat(downloads): coordinate season queue submissions

* fix(downloads): keep queue coordination provider neutral

* fix(downloads): reconcile legacy episode identities

* fix(downloads): fail closed on invalid stored coordinates

* refactor(downloads): adapt Xtream episode requests

* fix(downloads): use canonical Stalker episode ids

* test(downloads): cover Stalker adapter reactivity

* feat(downloads): add selected season queue action

* refactor(downloads): extract season download presenter

* feat(downloads): localize season queue feedback

* test(downloads): cover series batch queue flow

* test(downloads): harden series queue fixtures

* docs(downloads): describe season queueing

* docs(downloads): clarify season queue IPC contract

* fix(downloads): isolate season header build warnings

* fix(downloads): label season view toggles

* fix(downloads): preserve Xtream episode headers

* fix(downloads): fail closed on stale episode state

* fix(downloads): align renderer queue safeguards

* fix(downloads): block ambiguous episode actions

* fix(downloads): accept nullable legacy coordinates

* fix(downloads): preserve scoped episode ownership

* fix(downloads): probe restored files asynchronously

* fix(downloads): bound restored file probes

* fix(downloads): release timed out file probes

* fix(downloads): bound file probe callers

* fix(downloads): refresh stable season skips

* fix(downloads): fail closed before provider prep

* fix(downloads): preserve retained partial ownership

* fix(downloads): reconcile partial cleanup completion

* fix(downloads): await authoritative list refresh

* fix(downloads): coalesce list refreshes

* fix(downloads): preserve specials season identity

* fix(stalker): preserve specials season mapping

* fix(downloads): distinguish missing Xtream seasons
2026-08-03 08:53:44 +02:00
4grayandClaude Opus 5 c741815b97 fix(build): include shared UI stylesheets in Nx cache inputs (#1360)
* fix(build): include shared UI stylesheets in Nx cache inputs

`libs/ui/styles` held shared SCSS partials but had no `project.json`, so its
files belonged to no Nx project and were absent from every task hash. Editing
a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and
shipped the previous CSS — a silent wrong build rather than a failure.

Nx derives its project graph from TypeScript imports only, so a relative Sass
`@use` that crosses a project root creates no edge. Verified directly: after
adding the project but before declaring anything, `ui-styles` still had zero
dependents in the graph.

Make it the `ui-styles` project (no targets — it exists to be hashed) and
declare `implicitDependencies` on the 8 consumers. Chosen over adding the path
to `sharedGlobals`, which would put shared styles into every project's hash and
make a one-line SCSS tweak mark the whole workspace affected. A styles edit now
marks 15 projects affected and leaves electron-backend, website, the mock
servers and the shared libs alone.

`libs/ui/styles` was the only projectless directory holding files under `libs/`
or `apps/`.

Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every
relative stylesheet import against Nx's real project graph and fails when one
escapes the input closure of a build that compiles it, naming the project to
declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of
`apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes
that file, and a lib -> app edge would make the graph cyclic.

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

* fix(build): spawn git without a shell in the stylesheet check

`execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where
single quotes are literal characters rather than quoting. Git received the
pathspec with the quotes intact, matched nothing and exited 0, so
`styles:inputs:validate` reported success after checking zero stylesheets —
silently disabling the check for Windows developers while staying green.

Spawn with `execFileSync` so no shell is involved and git expands its own
pathspec; verified to return the identical 133 files.

Both this and the eslint glob trap next to it in the docs report success while
covering nothing, so also make an empty scan fail rather than pass: the
workspace always contains SCSS, and a listing that returns none means the scan
broke.

Reported by Codex review on #1360.

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

* refactor(styles): move nav-list partial into ui-styles (#1361)

* fix(build): count every target of a comma-separated Sass @import

`@import` is the only rule that takes a list, and the scan read just its
first target. A later entry crossing an Nx project boundary escaped the
cache key while the check still reported success — the same silent-pass
failure the tool exists to prevent.

Parse every target of an `@import` list. The obvious "read all quoted
strings" fix trades one silent gap for a phantom one, so the rule decides:
`@use`/`@forward` load exactly one module and a quoted string after it is
`with (...)` configuration, and `url(...)` stays a plain CSS import the
browser resolves at runtime. Neither is a module Sass compiles.

The workspace has no relative `@import` at all today, so the scan still
finds the same 42 imports across 133 files; this closes the gap before
someone writes one.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 23:18:37 +02:00
4gray 011f322807 ci(nx): enforce synchronized dependency updates (#1343)
* ci(nx): enforce lockstep dependency versions

* ci(deps): group Nx updates explicitly

* docs(nx): document coordinated dependency updates

* fix(nx): validate peer dependency versions

* fix(nx): validate duplicate root declarations
2026-08-02 12:52:37 +02:00
4gray 760099358b feat(downloads): redesign download manager (#1313)
* docs(downloads): specify manager MVP redesign

* docs(downloads): plan manager MVP implementation

* docs(downloads): tighten manager validation plan

* fix(downloads): keep renderer download state global

* fix(downloads): make active count accessible

* feat(downloads): derive queue and library view model

* test(downloads): close view model coverage gaps

* fix(downloads): stabilize malformed view model data

* refactor(downloads): isolate library navigation

* fix(downloads): report library navigation failures

* feat(downloads): add ready-to-watch library

* feat(downloads): add active download queue

* feat(downloads): finish manager MVP

* docs(downloads): clarify detail-first offline behavior

* docs(downloads): plan detail navigation follow-up

* fix(downloads): open completed movies in details

* test(downloads): cover pending series navigation

* fix(downloads): honor the global cover size

* fix(downloads): prefer local playback in shared details

* fix(downloads): preserve external launch priority

* fix(downloads): prefer local playback in Xtream details

* test(downloads): cover offline detail journey

* docs(downloads): document offline detail behavior

* docs(downloads): format detail navigation plan

* fix(downloads): open Stalker items in provider details

* docs(downloads): clarify Stalker navigation fallback

* fix(xtream): isolate reused detail identities

* fix(xtream): ignore stale VOD positions

* fix(downloads): keep offline Xtream playback available

* docs(downloads): clarify provider playback availability

* docs(downloads): design missing-file recovery

* docs(downloads): plan missing-file recovery

* feat(downloads): derive completed file availability

* feat(downloads): recover missing completed files

* feat(downloads): refresh missing local files

* feat(downloads): separate missing files from ready media

* feat(downloads): surface missing files for recovery

* refactor(downloads): simplify ready cards

* test(downloads): cover missing-file and series journeys

* feat(downloads): finish missing-file recovery

* docs(downloads): design offline detail views

* docs(downloads): plan offline detail views

* feat(downloads): persist offline metadata snapshots

* fix(downloads): complete metadata snapshot bridge contract

* feat(downloads): manage offline metadata snapshots

* fix(downloads): harden metadata snapshot updates

* fix(downloads): restrict snapshot artwork

* fix(downloads): guard restart artwork URL

* fix(downloads): refine artwork URL checks

* feat(downloads): expose offline metadata updates

* fix(downloads): keep metadata service change focused

* fix(downloads): preserve metadata error conventions

* feat(downloads): derive offline detail content

* fix(downloads): preserve unknown episode coordinates

* feat(downloads): add focused offline detail routes

* fix(downloads): ignore fragments in shell route state

* fix(downloads): normalize fragments before queries

* feat(downloads): open ready cards in offline details

* fix(downloads): use native disabled card styles

* feat(downloads): enrich offline detail metadata

* fix(downloads): harden offline metadata resolution

* fix(downloads): preserve stalker provider titles

* fix(downloads): distinguish stalker metadata seeds

* fix(downloads): stabilize offline metadata refresh

* fix(downloads): throttle sparse metadata refreshes

* fix(downloads): type metadata language settings

* feat(downloads): render offline movie and series details

* fix(downloads): harden offline detail interactions

* fix(downloads): close offline detail edge cases

* feat(downloads): hand off to provider-only details

* fix(downloads): preserve stalker provider handoff

* feat(downloads): capture metadata at download time

* fix(downloads): preserve snapshot source semantics

* fix(downloads): preserve episode snapshot identity

* docs(downloads): document offline details flow

* docs(downloads): clarify stalker provider fallback

* test(downloads): cover offline detail journeys

* test(downloads): stabilize offline detail selectors

* style(downloads): format changed files

* docs(downloads): clean design spec formatting

* fix(downloads): preserve offline library ownership

* test(downloads): fix Windows workspace navigation

* test(database): preserve Electron tsconfig resolution

* perf(downloads): avoid blocking file availability probes
2026-08-01 18:09:31 +02:00
4gray b14ce2452b fix(packaging): add verified source mirror fallback (#1325) 2026-08-01 15:15:09 +02:00
4gray 2ac0de752f fix(skills): align repository guidance with implementation (#1315)
* docs(skills): design implementation synchronization

* docs(skills): plan implementation synchronization

* fix(release): filter internal notes from public body

* docs(release): synchronize release workflow guidance

* fix(stalker): normalize catalog series flags

* fix(stalker): preserve progress with scoped episode IDs

* fix(playback): expose strict position persistence

* docs(stalker): record series position compatibility

* test(skills): validate repository skill contracts

* fix(database): keep SQL trace values private

* docs(skills): refresh Nx and SQLite ownership

* docs(skills): align provider and UI guidance

* docs(skills): tighten validated guidance

* docs(release): require exact release pushes

* style(electron): remove trailing blank line

* fix(ci): classify repository skills coverage
2026-07-31 08:00:59 +02:00
4grayandClaude Opus 5 9b7776a901 chore(lint): hold tests to their own max-lines ceiling (#1306)
* chore(lint): hold tests to their own max-lines ceiling

The flat 400-line cap treated a spec like a component. A spec is a flat
list of independent cases, so hitting the cap there produces arbitrary
`-2.spec.ts` splits and hides coverage instead of surfacing design debt —
65 of the 138 files over the limit were tests.

Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and
everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no
longer count, so a docblock can't be the reason a file must be split.

Both limits now live in tools/eslint/max-lines-config.mjs, imported by
eslint.config.mjs and the baseline generator alike. The generator decides
who belongs on the list by running ESLint's own max-lines rule instead of
counting lines itself — a private reimplementation would disagree with the
rule the moment either side changed (a `//` inside a template literal is
enough) and yield a baseline that turns CI red while looking correct.

The baseline drops 126 -> 68 entries with nothing added, and six now-dead
`eslint-disable max-lines` directives are removed. A new eslint-tools test
asserts the committed baseline still matches what the generator produces,
so a stale entry or a forgotten regeneration fails CI.

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

* chore(lint): classify eslint-tools in the coverage policy

A project with a `test` target must be assigned a coverage tier, so
adding eslint-tools broke `coverage:policy:check` before the unit suite
even ran. Tier B alongside packaging and release-tools: these are Node
tests over lint tooling, and a coverage percentage across a generated
list would not mean anything.

CI runs Tier B/C through its own `--run-non-tier-a` step, so the
baseline-consistency test executes there rather than being skipped.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 08:08:04 +02:00
4grayandClaude Opus 5 99a85da6b0 feat(packaging): register IPTVnator as the .m3u/.m3u8 handler (#1301)
* feat(packaging): register IPTVnator as the .m3u/.m3u8 handler

Every runtime path for an OS-supplied playlist existed, but no packaging
metadata claimed the file types — so the OS never offered IPTVnator as a
handler and `open-file` could not fire from Finder.

`fileAssociations` declares one entry per extension. Electron Builder derives
all three platform registrations from it: macOS `CFBundleDocumentTypes` (the
prerequisite for `open-file`), the NSIS registry entries, and, on Linux, the
desktop entry's `MimeType` plus `/usr/share/mime/packages/iptvnator.xml` for
deb/rpm/pacman. Neither platform needs a dedicated icon — both fall back to the
app icon.

Declaring `MimeType` under `linux.desktop.entry` would not have worked:
Electron Builder assigns the association-derived value *after* spreading that
object, so an explicit key there is silently overwritten. The per-association
`mimeType` fields produce the same entry through the supported path.

Registering the types also exposes a gap in the delivery side. The generated
Linux `Exec` ends in `%U`, so file managers hand over a percent-encoded
`file://` URI rather than a path, which `createPlaylistOpenRequest` would have
resolved into a bogus relative path. It now decodes a `file://` candidate
before the extension check. Suppressing the `%U` instead would mean putting an
exec code in `linux.executableArgs`, which also passes it to the app as a real
argument.

Verified on macOS against a signed packaged bundle: Launch Services lists the
app as a `public.m3u-playlist` handler, and an LS-initiated open imports the
playlist both on a cold launch and against the already-running process.

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

* fix(playlist): open every playlist of a multi-file selection

`%U` is the plural exec code, so selecting several playlists in a Linux file
manager is one launch carrying one argument per file. Both argv paths called
`extractPlaylistOpenRequestFromArgv`, which returned at the first match, so
everything after the first playlist was silently discarded — a gap this PR
itself opened by making the desktop entry reachable in the first place.

The extractor is now plural and returns every match in argument order, and the
queue gained `enqueueAll` so a selection is pushed under a single flush: a
delivery that fails partway leaves the untouched remainder queued in arrival
order rather than interleaved.

Covered by unit tests over a mixed argv (percent-encoded `file://` URI, a
non-playlist argument, a second URI) and by a new Electron E2E that launches
with two playlist arguments and asserts both are imported.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:53:43 +02:00
c637a0520e chore(deps): bump softprops/action-gh-release from 2 to 3 (#1284)
* chore(deps): bump softprops/action-gh-release from 2 to 3

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(ci): allow softprops/action-gh-release v3 in the Snap workflow policy

The Snap supply-chain policy test pins the exact major of every action
the build workflow may use, so bumping softprops/action-gh-release in
the workflow without updating BUILD_ACTION_ALLOWLIST fails
publish-snap-workflow.test.mjs.

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 08:13:12 +02:00
55f68e73c8 chore(deps): bump actions/setup-node from 4 to 7 (#1285)
* chore(deps): bump actions/setup-node from 4 to 7

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(ci): allow actions/setup-node v7 in the Snap workflow policy

The Snap supply-chain policy test pins the exact major of every action
the build workflow may use, so bumping actions/setup-node in the
workflow without updating BUILD_ACTION_ALLOWLIST fails
publish-snap-workflow.test.mjs.

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 08:13:00 +02:00
553f45dedc chore(deps): bump actions/cache from 4 to 6 (#1281)
* chore(deps): bump actions/cache from 4 to 6

Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(ci): allow actions/cache v6 in the Snap workflow policy

The Snap supply-chain policy test pins the exact major of every action
the build workflow may use, so bumping actions/cache in the workflow
without updating BUILD_ACTION_ALLOWLIST fails
publish-snap-workflow.test.mjs.

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 08:12:18 +02:00
4grayandClaude Opus 5 a2d678bdda fix(packaging): stop the Linux frame-copy probe timing out on cold sandboxes (#1294)
The packaging verifier bounded `iptvnator_mpv_helper --runtime-probe` with
RUNTIME_PROBE_TIMEOUT_MS (3s) — a constant it shares with the application's own
startup capability gate. Three seconds is a tight budget for a helper that
dlopens libmpv plus EGL/GL/GBM, and the Flatpak profile is closest to that edge
because the helper runs inside the sandbox against its bundled closure: on
#1277 the job failed three consecutive reruns and passed on the fourth with no
code change, while the concurrent master job passed.

Give the verifier its own budget rather than raising the shared one. The app's
probe is a blocking spawnSync on the Electron main process, so a hung helper
must not stall window creation, and a timeout there degrades gracefully to the
native-view fallback. Nothing waits on the packaging probe but the CI job,
which already has its own 120-minute bound, while a premature kill reports a
healthy package as broken.

- PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS (15s) and
  PACKAGE_VERIFICATION_PROBE_MAX_ATTEMPTS (2) join the frozen probe contract;
  RUNTIME_PROBE_TIMEOUT_MS stays at 3s for the application gate.
- runBoundedRuntimeProbe() retries only on ETIMEDOUT, repeating the identical
  bounded launch (same command, args, env, maxBuffer, killSignal) and
  announcing the retry on stderr so a degrading trend stays visible.

Fail-closed behaviour is unchanged. A hard timeout is the one probe outcome
that says nothing about the payload; spawn errors (a missing helper, a wrapper
launched instead of the real ELF), termination by signal, nonzero exits and
malformed or wrong-protocol lines all still fail on the first attempt, and a
helper that keeps hanging still fails once both attempts are spent.

The four new/extended verifier tests cover retry-then-success (asserting the
second launch is identical to the first), exhausted timeouts still rejecting,
four non-timeout verdicts each probing exactly once, and the attempt bound
itself. Setting MAX_ATTEMPTS to 1 fails four of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:34:31 +02:00
4gray e2300bea11 test(settings): split the settings spec along the facade seams (#1277)
settings.component.spec.ts was 1516 lines and the last settings file in the
max-lines baseline. The behaviour that moved into facades now has its own
specs, driven directly instead of through the rendered page.

- settings-app-update.facade.spec.ts: status polling/retry, bridge actions,
  release notes dialog, version messaging, dispose
- settings-epg.facade.spec.ts: refresh, clear flow, post-save re-fetch
- settings-playlist-reset.facade.spec.ts: summary, dialog, Electron progress,
  browser fallback, failure snackbar
- settings-backup.facade.spec.ts: desktop export, browser download fallback
- settings.component.spec.ts keeps the page shell, the facade lifecycle seam
  and runtime capabilities; settings.component.form.spec.ts takes hydration,
  section outputs, dashboard controls and submit
- settings-section-scroll.directive.spec.ts gives the directive its first spec
- shared TestBed fixtures live in settings/test-stubs/, kept out of both the
  app build (.stub.ts) and the coverage ratchet (test-stubs/)

105 settings tests, up from 94; every file is under the 400-line limit, so
settings.component.spec.ts leaves the baseline.
2026-07-27 08:31:14 +02:00
4gray e55d55b47f feat(mock-data): add shared screenshot-safe poster catalog (#1271)
Moves the fictional movie catalog into `libs/shared/marketing-fixtures` so the
Xtream and Stalker mocks describe the same titles, and adds 20 rendered posters
plus the shared fixture types behind them.

Supporting changes made while getting it green:

- `shared-marketing-fixtures` is classified Tier B in the coverage policy. Not
  Tier A: it is fictional fixture data, so a statement percentage over it means
  nothing, and a Tier A entry would pull it into the merged coverage map and the
  ratchet. Tier B still runs its spec in CI. `stalker-mock-server` needs no entry
  of its own — it is already Tier C and the Tier B/C runner falls back to
  `pnpm nx test <project>`, so its new `marketing-poster-url.spec.ts` runs.
- Two release-capture defects the catalog reorder introduced, both fixed in
  `tools/release/capture-app-driver.ts`:
  - VOD stream ids are `MARKETING_VOD_STREAM_ID_BASE + index` and the generator
    now lists the showcase movies first, so 62000-62002 became Black Harbor, The
    Paper Astronaut and Summer Static while the dashboard seeding still mapped
    those ids to the previous titles' backdrops.
  - the raw `tsx` spawn of the Xtream mock lacked `--tsconfig
    tsconfig.base.json`, so the mock could not resolve
    `@iptvnator/shared/marketing-fixtures` and the capture never started. Both
    mock projects' own serve targets already passed the flag.
2026-07-27 08:10:57 +02:00
4gray 636545cbb7 refactor(electron-backend): split four files under the max-lines limit (#1278)
Split epg.events.ts (514), the embedded MPV frame-copy adapter (428) and two of its specs (547, 539) below the 400-line hard limit, and teach the baseline generator to skip files that already carry a justified file-wide eslint-disable max-lines.

The generated baseline list is unchanged: 128 entries before and after. No behavior change.
2026-07-27 02:16:02 +02:00
4gray 02b966895d docs(release): backfill curated 0.23.0 release notes, fix the notes CLI -- trap (#1263)
Backfills the 0.23.0 release notes the .changes/ pipeline missed: it landed
after most of the release was already merged, leaving 4 notes for 79 commits.

Adds 22 curated notes (26 total: 14 features, 11 fixes, 1 perf). Curated
rather than exhaustive — the GitHub release body renders these above
GitHub's own list of every merged PR, so related PRs are folded into one
note per user-facing story: shared player controls (8 PRs), embedded MPV
frame-copy (4), manual EPG mapping (3), plus five more pairs. Tooling-only
scopes get no note. No screenshot slugs: none of the five manifest shots
depicts a 0.23 headline feature, and the capture run asserts TMDB
enrichment stays disabled.

Two tooling fixes found while writing them:

- parseArgs now ignores a bare `--`. npm needs it to forward arguments past
  the script name; pnpm hands it to the script verbatim, so
  `pnpm run release:notes:github -- --version 0.24.0` died on the very
  separator typed to make forwarding work. Unknown flags and missing values
  still fail as before. Covered by new subprocess CLI tests wired into the
  release-tools target.
- .changes/README.md claimed every non-consume mode was a safe dry run;
  --format changelog and --format blog write their target file.

No app or lib code, no version bump, no --consume, no CHANGELOG.md or
website changes — those stay owned by release-cut.
2026-07-27 01:36:11 +02:00
4gray f9ea3070ee refactor(settings): split the settings page into per-section facades (#1274)
settings.component.ts had grown to 819 lines — past the CLAUDE.md target (<300)
and hard maximum, passing lint only because it sat in the max-lines baseline.

The behaviour moves into facades the template binds to directly, following the
precedent already in this folder: new app-update (218), form (197), epg (123),
embedded-mpv (74) and remote-control (37) facades, with playlist-reset extended
to 143 and settings-options to 200. The component is now a 259-line coordinator
holding capability flags, section nav, players() and the cross-facade flows.
settings.component.ts is removed from the max-lines baseline.

No behaviour change. One ordering detail: applyChangedSettings now applies
language/theme before kicking off the EPG re-fetch; changeTheme only touches DOM
theme sync and translate.use does not touch the form, so the two are
independent.
2026-07-26 22:57:16 +02:00
4gray 08b868d6c1 test(electron): harden runtime boundary coverage (#1267)
Adds contract-focused regression coverage for the Electron HTTP server,
remote-control events, settings events, and managed download paths, and makes
Tier A coverage fail closed when instrumentation fails or a runtime-owning
production file disappears from a project or from the merged Istanbul report.

The old `coverage:ci` exited 0 despite a `Failed to collect coverage`
diagnostic: libs/m3u-state/src/lib/effects.ts was simply absent from the merged
map. All 30 Tier A reports are now required, the merged map covers 710 files,
and effects.ts is reported as 0/159 instead of silently disappearing.

Also fixes remote static-file path containment for encoded, malformed, NUL,
POSIX and Win32-style traversal inputs, with behavior-preserving testability
seams.

Statements 69.27% -> 69.54%; http-server.ts 0% -> 90.21%,
remote-control.events.ts 0% -> 96.55%, settings.events.ts 59.25% -> 96.29%.
2026-07-26 22:27:50 +02:00
4gray f193232dab ci(deps): bump checkout/upload-artifact/download-artifact majors (#1264)
Supersedes #1249, #1245 and #1247, which each rewrote the full-commit pins in
publish-snap.yaml while the same SHAs are asserted in three packaging test
files — merged separately, every one of them left those tests red.

actions/checkout v4 -> v7 (docker.yml from v6), actions/upload-artifact
v4 -> v7, actions/download-artifact v4 -> v8. New pins verified against the
upstream tag refs: checkout 3d3c42e5 = v7.0.1, upload-artifact 043fb46d =
v7.0.1, download-artifact 3e5f45b2 = v8.0.1.

download-artifact v8 changes two things on the Snap publish path, both in our
favour: a digest mismatch now fails the run instead of logging a warning, and
decompression is skipped for non-zip Content-Types (our artifact is a normal
upload-artifact zip, so unchanged). checkout v7's fork-PR block only applies to
pull_request_target/workflow_run, neither of which exists here.
2026-07-26 19:54:39 +02:00
4grayandClaude Opus 5 6c946978b4 chore(release): drop the superseded v0.20 screenshot script (#1262)
#1261 replaced this one-off capture with a manifest-driven script, so the
v0.20 version is dead weight: hard-coded slugs, paths and output directory,
none of the fail-closed guards, and two `RegExp`-from-string constructions of
the kind CodeQL flags (one of which it flagged on the replacement before that
was rewritten to use predicates).

Removing it also drops its `tools/eslint/max-lines-baseline.mjs` entry, so the
baseline no longer carries a file that does not exist.

Not a pure dead-code deletion, and worth stating: two capabilities go with it,
neither reachable from the new pipeline — `createDesignedCopy` (title/kicker
overlays on captured frames) and `createHeroImage` (a 1600x900 canvas collage
built from three screenshots). The v0.20 assets they produced are already
committed under apps/website/public/blog/v0-20/, so nothing published breaks;
a future release wanting the same collage needs it ported deliberately rather
than resurrected here.

Docs: docs/architecture/xtream-mock-server.md now points at
capture-release-screenshots.ts and notes its mock-identity check.

Verified: no references to the removed file remain anywhere in the repo, and
`pnpm run lint` passes for all 42 projects with the shortened baseline.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 01:25:56 +02:00
4grayandClaude Opus 5 b4ec68c1fa feat(release): manifest-driven screenshot capture with fail-closed mock-data guards (#1261)
Third slice of the release-notes pipeline (#1256 format+generator, #1257 CI
gate): release screenshots become reproducible and provably mock-only.

The v0.20 capture script was single-use (hard-coded slugs, paths, hero) and
fail-open: a lost IPTVNATOR_E2E_DATA_DIR silently fell back to the user's
real ~/.iptvnator database, `...process.env` leaked ambient TMDB keys and
proxies, nothing gated network access, and no frame content was ever
validated. Each hole leaks real playlists, credentials, or copyrighted
artwork into published screenshots without a single signal.

New pipeline:

- tools/release/screenshots.manifest.json — declarative shots (slug, title,
  named setup steps, themes). Adding a feature shot = one manifest entry.
- capture-release-screenshots.ts — orchestrator; output goes to
  apps/website/public/blog/<release>/screenshots/<slug>-<theme>.png, release
  slug derived from package.json (or --release), --only/--theme filters.
- capture-app-driver.ts / capture-navigation.ts — launch, seeding, theme,
  and the named-action vocabulary; actions are order-independent (every
  portal action starts from the dashboard).
- screenshot-guards.mjs — the fail-closed policy, pure and unit-tested:
  G1 the real database is snapshotted (sha256+mtime) before launch and must
     be byte-identical after; the isolated DB must actually exist
  G2 the app receives an allowlisted environment, never ...process.env
  G3 deny-by-default network gate; known app-level calls (GitHub update
     check) are answered by local stubs; any other blocked request fails
     the run — a silently-blocked TMDB call would leave a frame that looks
     broken rather than unsafe
  G4 every frame is scanned before capture: external img/background URLs,
     credential-shaped text, MAC addresses, non-localhost m3u8 references
  G5 TMDB enrichment asserted disabled via the renderer's IndexedDB
  Any violation deletes every frame captured in the run and exits non-zero.

The guards paid for themselves on the first live run: G3 caught the mock
server redirecting stream endpoints to a public demo HLS
(test-streams.mux.dev) — meaning earlier hand-run captures could embed
third-party video frames. The M3U shot now deliberately captures the groups
layout without starting playback.

`.changes` validation now cross-checks `screenshot:` slugs against the
manifest, so a note cannot reference an image the capture run never
produces.

Verified end-to-end: 10/10 shots (5 slugs × dark/light) captured against
dist build + xtream-mock-server, frames visually inspected (fictional
titles/artwork only), guard-violation paths exercised live. 67 unit tests
in release-tools, lint green, script files within the repo size limit.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 00:19:02 +02:00
4grayandClaude Opus 5 4e5132cbb5 ci(release): gate PRs on an authored release note (#1257)
* ci(release): gate PRs on an authored release note

Second slice of the release-notes pipeline (#1256 landed the format and
generator): make the .changes/ habit survive contact with reality.

- "Release note gate" job in ci.yml, PR-only: validates every .changes/*.md,
  then requires an added note (or the no-release-note label) when the PR
  touches runtime code under apps/ or libs/. Tests, e2e projects, the
  website, mock servers, shared testing helpers, snapshots and docs are
  auto-exempt.
- Policy lives in tools/release/check-release-note-gate.mjs as a pure
  function fed PR files+labels as JSON — unit-tested (10 cases) instead of
  encoded in workflow bash. The failure message lists the triggering files
  and names the exact fix.
- Labels are fetched live rather than from the stale event payload, so
  applying the label and re-running the check works without a new push.
- The job is dependency-free Node: no pnpm install, runs in seconds.
- release-notes and release-cut skills added under .claude/skills/ and
  mirrored to .codex/skills/; CLAUDE.md/AGENTS.md sections updated to point
  at the gate and the skills.

The no-release-note label itself was created in the repository.

Tests: 47 passing in release-tools (10 new gate cases); gate-step shell
verified with shellcheck at the CI severity; ci.yml YAML-parse checked.

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

* chore(agents): make the release skills discoverable by Claude Code too

`.codex/skills/**` was un-ignored so Codex picks up repository skills in any
clone, but `.claude` was ignored wholesale — and Claude Code only discovers
skills under `.claude/skills/`. The release-notes and release-cut skills
therefore existed only on whichever machine authored them.

Mirror both skills into `.claude/skills/` and opt them in by name rather than
un-ignoring the directory: contributors keep personal skills there
(i18n-fill, website, …) which must stay local and out of `git status`.

CLAUDE.md/AGENTS.md updated so the "skills live under .codex/skills/" claim
does not go stale, including the requirement to keep mirrored copies in sync.

The CI gate and the CLAUDE.md/AGENTS.md section remain the load-bearing
enforcement; skills only carry the detail.

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

* fix(ci): only a note this PR authored satisfies the release-note gate

Review follow-ups on #1257 (Codex P2 ×2, Greptile P1).

- Drop `renamed` from the accepted statuses. The PR files API compares
  base…head, so a note created and then renamed inside the same PR still
  reports as `added`; a `renamed` entry means the file already existed on the
  base branch. Accepting it let a runtime-code PR pass by moving another
  PR's unconsumed note, which documents nothing and gives the generator no
  adding commit to resolve a PR link from.
- Require a direct child of `.changes/`. `loadNotes()` reads only the
  immediate directory, so `.changes/sub/note.md` satisfied the old prefix
  check while never being validated or rendered into any release surface.

Tests: renamed and nested notes now assert a failing gate (12 gate cases).

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:51:04 +02:00
4grayandClaude Opus 5 21e55e4bb4 fix(ci): classify release-tools in the coverage policy (#1260)
#1256 added the release-tools project with a test target but never listed it
in tools/coverage/coverage-policy.json, so `coverage:ci` fails with "Coverage
policy is missing projects that have a test target: - release-tools". Master
has been red since that merge (270350c2) and every PR rebased onto it inherits
the failure.

Tier B, mirroring the sibling `packaging` project: the tests validate
release-note parsing, rendering and gate policy, but the scripts are release
tooling rather than shipped source, so a percentage-coverage baseline would
measure nothing useful.

Verified: `check-coverage-policy.mjs` reports 36 projects classified
(30 A / 6 B / 6 C), and `--run-non-tier-a` runs the release-tools suite green.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:17:35 +02:00
4grayandClaude Opus 5 270350c2e1 chore(release): author release notes in .changes instead of reconstructing them (#1256)
* chore(release): author release notes in .changes instead of reconstructing them

CHANGELOG.md has been frozen at 0.12.0 since 2023 while the app shipped
0.23.0, semantic-release sat in devDependencies with no config, and the real
user-facing notes were a 280-line MDX post written from memory at release
time. The gap was never version math — it was authored notes captured while
the context is still fresh.

Add a `.changes/*.md` note format (type, area, issues, screenshot; no version
field, since the release version is chosen deliberately) plus a generator that
composes the GitHub release body, the CHANGELOG.md section and a blog-post
scaffold from the accumulated notes.

Changesets was considered and rejected: it versions multiple published
packages, and this repo has exactly one private package. Its `version` step
would also rewrite CHANGELOG.md into a flatter format than the blog post and
fight the deliberate, updater-constrained version choice.

- hand-rolled frontmatter parser over a YAML engine: the schema is closed, so
  it can reject unknown keys, which is what catches typos
- PR numbers are resolved from the commit that added the note, never written
  by the author
- MDX-significant characters in note bodies are escaped so a stray `<` cannot
  break the website build
- blog scaffold ships `draft: true` with explicit TODO headings; the prose is
  editorial work, only the inventory is mechanical
- revive CHANGELOG.md with an honest pointer for 0.13.0-0.23.0 rather than
  fabricating the missing history
- drop the five unused semantic-release/conventional-changelog packages

Docs: `.changes/README.md`, plus a "Release Notes For User-Visible Changes"
section mirrored in CLAUDE.md and AGENTS.md, and a PR template checkbox for
contributors who never read either.

Tests: 26 unit tests in tools/release/release-notes.test.mjs covering parsing,
validation, grouping and all three renderers.

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

* fix(release): default the notes version to package.json and harden alt escaping

Review follow-ups on the release-notes generator.

- `--version` now defaults to the root package.json version, so the
  `release🎶*` package scripts run bare instead of failing on a missing
  argument. Bumping package.json is the deliberate act that starts a release,
  which makes it the right single source of truth; `--version` remains as an
  override for dry runs before the bump. The notice goes to stderr so
  `--format github` keeps a pipeable stdout.
- Escape backslashes before apostrophes when building the MDX `alt` string
  literal. A note body ending in a backslash previously produced an
  unterminated string and would have broken the website build.
- Document that release posts are one per minor version, in the slug helper,
  the overwrite error, and `.changes/README.md` — a patch release edits the
  existing post rather than creating a second one.

Tests: +1 regression test for the alt escaping, verified to fail without the
fix (27 total, all passing).

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

* feat(ci): put authored notes into the tag release body, fail-closed

Wires the .changes pipeline into the release workflow (Codex review P1 on
#1256). Calling the generator from the tag build cannot work — --consume
deletes .changes/ before the tag exists — so the tag build reads what the
generator already wrote: release-meta now fills BODY from the CHANGELOG.md
section matching the tag's version via tools/release/extract-changelog-section.mjs.

generate_release_notes stays on, so GitHub's commit list renders below the
authored notes; the existing draft-metadata repair step already concatenates
RELEASE_BODY with the generated notes, so the rare duplicate-draft path keeps
the same layering unchanged.

The extractor exits non-zero when the section is missing or empty, failing
the release instead of silently shipping PR-title-only notes. A hotfix tag
cut without running release:notes:changelog therefore fails at create-release
by design; the error message names the exact commands to run.

Tests: 5 new extractor tests (32 total in release-tools, all passing);
packaging suite (247) re-run green since build-and-make.yaml is one of its
inputs.

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

* fix(release): escape all regex metacharacters in the changelog extractor

CodeQL flagged the version-to-RegExp interpolation in
extract-changelog-section.mjs (regex injection + incomplete escaping): only
dots were escaped, and while the CLI validates its argument as bare semver
before calling, the exported extractSection() carries no such guarantee on
its own. Escape the full metacharacter set so no caller can inject pattern
syntax, with tests covering wildcard dots, alternation, `.*` and backslashes.

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

* fix(release): make changelog generation idempotent per version

Codex review P2 on #1256: rerunning `release:notes:changelog` for the same
version — the normal move after correcting a note before --consume —
prepended a second section instead of replacing the first, leaving duplicate
release entries.

Extract the marker insertion into upsertChangelogSection(): it removes any
existing section for the version, then rebuilds around the marker rather than
string-replacing into it, so the blank-line count on both sides stays exact
on both the fresh-insert and replace paths. The CLI reports when a section
was replaced.

Tests: 4 new cases (insert, replace-not-duplicate, neighbours untouched,
missing marker); 37 total passing. End-to-end rerun verified: one heading,
latest date wins, extractor output unchanged.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 18:22:43 +02:00
4gray cfa602d5b1 ci: cut PR runner waste and harden workflow permissions (#1226)
Pipeline audit follow-up: reduce wasted runner time on PRs and tighten CI
security, without reducing what actually gets validated.

Runner-time waste:
- Concurrency with PR-only cancel-in-progress on CI, E2E, and docker-build,
  so a new push cancels the previous commit's still-running checks. Non-PR
  runs use the unique run_id as the group, because GitHub keeps at most one
  pending run per group even with cancel-in-progress: false — a shared ref
  group could silently drop a queued master run.
- paths-ignore for docs-only changes (Markdown, docs/, .plans/, .codex/,
  .claude/) on the Electron build matrix and the E2E suites; E2E also skips
  apps/website/**. The build workflow keeps apps/website/** because its Linux
  job builds the website to verify AppStream assets. Tag pushes are
  unaffected: GitHub does not evaluate paths filters for tags.
- PRs lint affected projects only; master pushes keep the full run-many.
  Lint-global inputs (eslint.config.mjs, tools/eslint/**) now mark all 41
  lint projects affected, including the run-commands targets database and
  packaging, so the max-lines baseline cannot be widened without lint.

Hardening:
- Explicit least-privilege permissions on CI, E2E, and build-and-make; the
  create-release job keeps its job-level contents: write. The repository
  default workflow token was switched to read-only.
- New actionlint job (image pinned by digest, shellcheck at warning+), with
  the shared-anchor false positive suppressed in .github/actionlint.yaml.
  Fixed one real finding: unquoted $GITHUB_OUTPUT.
- .github/dependabot.yml: weekly cadence, minor+patch grouped per ecosystem
  (npm, GitHub Actions, Docker), majors stay individual PRs.

Docs updated: CLAUDE.md, docs/architecture/nx-workspace-boundaries.md, and
docs/architecture/validation-map.md now describe affected-lint on PRs and the
E2E path-filter exceptions.
2026-07-25 14:37:40 +02:00
4grayandClaude Fable 5 ec09778f36 feat(about): show build commit next to the app version (#1208)
* feat(about): show build commit next to the app version

Settings > About now renders "<version> (<short-sha>)" with the full
SHA in the tooltip, so bug reports from test and nightly builds
identify the exact commit. The commit is injected at CI build time into
apps/web/src/environments/build-commit.ts (same placeholder pattern as
the TMDB key inject); PR builds use the real head SHA instead of the
ephemeral merge commit. Local/dev builds keep the plain version.

The semver version itself deliberately stays untouched: a "-sha"
suffix would flip electron-updater into prerelease mode and leak into
installer/artifact version fields.

Requested by WolfganP in #1202.

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

* style(settings): keep relative import after monorepo alias imports

Addresses Greptile feedback on #1208.

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

* feat(docker): inject build commit into published PWA images

The Docker/PWA build path bypassed the Electron workflow's inject step,
so published images showed the plain version in About. Pass the commit
as a build arg and run the inject script before the PWA build; the
script no-ops when BUILD_COMMIT is empty, leaving local docker builds
unchanged.

Addresses Codex feedback on #1208.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:44:08 +02:00
4gray 5cae310430 fix(logging): redact sensitive portal and Electron diagnostics (#1182)
* fix: redact sensitive log data

* fix(ci): keep logging preload self-contained

* fix(logging): preserve shared diagnostics

* fix(logging): close trace redaction gaps

* fix(logging): redact Xtream path credentials

* fix(logging): close credential redaction gaps

* fix(logging): suppress external player arguments

* fix(logging): harden URL and date redaction

* fix(logging): redact map keys and URL fragments

* fix(logging): redact sensitive map values

* fix(logging): redact credentials in diagnostic text

* fix(logging): close remaining credential leaks
2026-07-19 08:30:21 +02:00
4gray c266eaa680 fix(packaging): preserve Flatpak Electron ELF for Zypak (#1205)
Fixes the launcher/Zypak regression reported in #1203. The additional GPU/video.js behavior remains tracked separately in that issue.
2026-07-18 22:42:45 +02:00
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

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

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00
4grayandLars Emig 4e572c60ca feat(videojs): add feature-flagged shared controls (#1195)
* feat(videojs): add feature-flagged shared controls

Rebuild the Video.js shared-controls integration on the current player lifecycle with Tech rebinds, source-scoped tracks, reset ordering, volume preservation, diagnostics gating, and default-off compatibility.

Credits and supersedes the stacked implementation proposed in #1153.

Co-authored-by: Lars Emig <lars.emig@pickware.de>

* fix(videojs): harden MPEG-TS reset lifecycle

---------

Co-authored-by: Lars Emig <lars.emig@pickware.de>
2026-07-16 23:08:18 +02:00
4gray c49ea2f6a8 feat(html-player): add feature-flagged shared controls (#1194)
* feat(html-player): bridge engine state to shared controls

* fix(html-player): avoid HLS subtitle event reentry

* fix(html-player): restore delayed HLS default subtitles

* refactor(html-player): split controls bridge collaborators

* feat(html-player): add feature-flagged shared controls

* test(html-player): cover shared-controls source ownership

* feat(html-player): pass shared-controls playback metadata

* docs(player-controls): describe HTML5 shared-controls bridge

* docs(player-controls): clarify HTML5 rollout effect

* fix(html-player): reveal diagnostics from fullscreen

* fix(html-player): defer HLS event resolution

* refactor(html-player): isolate video element session
2026-07-16 21:30:41 +02:00
8f597b44cf refactor(embedded-mpv): split session controller into focused collaborators [2/7] (#1149)
* refactor(embedded-mpv): split session controller into focused collaborators

Mechanical decomposition of the embedded-MPV session controller into
focused collaborators under embedded-mpv-player/:

- embedded-mpv-command-runner.ts: transport/track/recording IPC
  delegators with guarded snapshot reconciliation
- embedded-mpv-session-factory.ts: pure placeholder-session factories
  (loading/attaching/error) and the startup-paint wait
- embedded-mpv-stalled-tracker.ts: loading-stall timer and stalled flag
- embedded-mpv-compositor.ts: host bounds measurement (measureBounds),
  re-exported from embedded-mpv-format.utils for existing imports

No behavior change. The existing embedded-mpv-player component is kept
untouched and keeps working against the controller's unchanged public
API (commands are now bound fields delegating to the runner).

Test coverage extended per Codecov patch report.

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

* refactor(embedded-mpv): drop superseded overlay hooks

* fix(embedded-mpv): guard async session races

* docs(embedded-mpv): document renderer collaborators

* fix(embedded-mpv): abort stale recording startup

* docs(embedded-mpv): clarify renderer safety details

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-07-16 14:13:13 +02:00
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 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 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