Commit Graph
9 Commits
Author SHA1 Message Date
4grayandClaude Fable 5.1 4cce4acaad feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups (#1633)
* feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups

Follow-ups to the season posters shipped in #1628:

- The >6-seasons dropdown (`SeasonTabsComponent`) now carries a 28×42
  season thumbnail at the start of each menu row that has a poster and in
  the closed trigger for the selected season, fed by a new `seasonPosters`
  input from the season container and the fullscreen episode panel. Rows
  without a poster get no placeholder, a failed image is dropped, and the
  pill row stays text-only as the design review decided.
- The fullscreen season strip's episode count uses its own
  `PORTALS.EPISODE_COUNT_ONE/OTHER` keys instead of borrowing the download
  manager's; all 18 locales filled through the i18n merger from their
  existing `DOWNLOADS.EPISODE_COUNT_*` translations.
- The Stalker mock's serve targets no longer pin `PORT` (an nx:run-commands
  `env` entry overrides the shell), and `main.ts` resolves `PORT`, then the
  Playwright-side `MOCK_PORT` alias, then 3210 — so `MOCK_PORT=3310` now
  relocates the whole E2E run. The Xtream mock honours `XTREAM_MOCK_PORT`
  the same way.
- `resolveAutoSelectedSeason` gets a direct spec covering every branch.

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

* fix(xtream-mock): mint marketing asset URLs on the port the server bound

Greptile P1 on #1633: the listener honoured `XTREAM_MOCK_PORT`, but
`marketingAssetOrigin()` still read `PORT` alone, so a run relocated only
through the alias sent every poster/backdrop/logo/episode URL to 3211.

One resolver (`resolveXtreamMockPortString` in `mock-port.ts`: `PORT`,
then `XTREAM_MOCK_PORT`, then 3211) now feeds the environment parser, the
marketing asset origin and the demo-guide origin fallback. A spec pins the
precedence and that `marketingAssetUrl` follows the bound port.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:54:54 +02:00
4gray bad8a0991e feat(downloads): download completed Xtream catch-up programmes as TS (#1572)
* feat(epg): copy catch-up programme URLs without changing playback

* feat(downloads): save completed Xtream archive programmes as TS

* fix(epg): let newer archive copy requests supersede pending work

* fix(downloads): protect archive partials and independent submissions

* fix(downloads): verify archive identity through finalization

* fix(downloads): bound archive storage and capture cleanup entries

* fix(downloads): preserve archive ownership across failure paths

* fix(downloads): recover explicitly verified archive completions

* fix(downloads): journal archive promotion before publishing files

* fix(downloads): reset archive proof before an explicit restart

* fix(downloads): preserve archive recovery ownership and interruption

* fix(downloads): verify durable archive identity at resume open

* fix(downloads): fence archive commands during completion commit

* fix(downloads): persist archive ownership throughout its lifecycle

* fix(downloads): protect archive removal and missing-file recovery

* fix(downloads): journal private cleanup captures for recovery

* fix(downloads): journal active archive cleanup before removal

* fix(downloads): clean settled archives before deleting stale rows

* fix(downloads): preserve archive ownership on removal and resubmission

* fix(downloads): recover proven archive completions before retry

* fix(downloads): recover local archives before remote transfer checks

* test(downloads): resolve archive fixture from workspace root

* fix(downloads): distinguish reused archive inodes by creation time

* fix(downloads): bind fresh archive reservations to owned files

* fix(downloads): clean reservations when ownership writes fail

* fix(downloads): commit archive reservation and ownership atomically

* fix(downloads): retain captures until replacement restoration succeeds

* fix(downloads): require durable ownership before cleanup relocation

* fix(downloads): preserve 64-bit archive file identities on Windows

* refactor(release): keep capture fixture constants in their shared module

* fix(downloads): preserve the last link of captured foreign files

* fix(downloads): expose retained archive recovery files

* fix(downloads): keep recovery instructions open while copying
2026-09-08 20:33:05 +02:00
4grayandClaude Opus 5 186497bf42 feat(website): add the EPG troubleshooting post, fix the mock identity check
New post at /blog/epg-wrong-program-fix/: a table separating the three
symptoms (an empty row, a channel showing another station's schedule, and
every programme shifted by a fixed amount), how the tvg-id → tvg-name → name
lookup chain produces the first two, the manual "Map EPG channel" flow, and
the display-only EPG time offset with the sign to use. Seven FAQ entries.

The three screenshots are mock-backed. The Xtream mock gains /demo/guide.xml,
an XMLTV guide for its marketing live channels whose ids deliberately match no
playlist tvg-id, which is what makes the manual mapping worth showing; the
capture imports it through the settings, accepting the private-network
confirmation a loopback source raises, then right-clicks a channel of the M3U
fixture and searches the dialog. The new actions live in
capture-navigation-epg-actions.ts, alongside the setup, portal and download
modules, and borrow one entry point from each of its two neighbours instead of
duplicating their navigation.

Also fixes assertMockServerIdentity: it checked both expected categories
against get_vod_categories, but "Urban Drama" is a series category, so the
reuse path rejected every already-running mock and a capture could only run
when the port happened to be free. Each category is now checked against its
own endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 07:28:33 +02:00
4grayandClaude Fable 5.1 98da686cea feat(website): add the phone remote control guide
New guide at /blog/remote-control-guide/: enabling the remote in Settings,
opening it on a phone from the QR code, what each control does and which
list it navigates, a checklist for a page that does not load, and why the
remote must stay on the local network. Eight FAQ entries. The remote-control
feature page now links to it instead of the M3U guide.

Both screenshots are mock-backed. The phone view is the first "browser" shot:
a manifest entry names a loopback URL and a mobile viewport, and the capture
frames it in a separate Chromium page behind the same network and content
guards, with the manifest validator accepting loopback origins only. The
setup saves the remote-control setting so the app's own server answers, then
selects a live channel; the Xtream mock's marketing scenario now serves live
stream URLs from local bytes so that selection never leaves the machine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 22:06:23 +02:00
4grayandClaude Fable 5.1 0d03140661 feat(website): add the offline downloads guide with a reusable content disclaimer
New guide at /blog/offline-downloads-guide/: what the desktop download manager
can save, choosing the folder, downloading a movie, episodes and seasons,
following the queue (pause, resume, automatic reconnects), the offline
library, and the Needs attention states, with a nine-question FAQ. The prose
frames the feature as offline viewing of content the reader already streams
and defers legality to the provider's terms and local law.

ContentDisclaimer.astro carries that notice in a general and an offline
variant so later guides reuse it instead of rewording it.

The three screenshots are mock-backed captures. The Xtream mock's marketing
scenario now serves movie and episode stream URLs from generated local bytes
(downloadStreamFixture: 'local-media'), because the capture's network gate
rejects the public HLS stub every other scenario redirects to; the capture
stubs Electron's folder dialog so "Change Folder" authorizes a folder inside
the isolated data dir rather than the real OS Downloads folder; two new setup
actions queue a movie and two episodes and open the manager and the offline
detail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:41:10 +02:00
4gray 436825bdec fix(xtream): try advertised TS after initial web HLS HTTP failure (#1558)
* fix(xtream): try advertised TS after initial web HLS HTTP failure

* refactor(playback): extract fullscreen channel panel state

* test(xtream): keep synthetic media within the mock project
2026-09-06 11:00:09 +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 Fable 5 3dbfefa3d8 test(stalker): enforce portal auth in the mock and cover the full-portal flow (#1324)
* test(stalker): enforce portal auth in the mock and cover the full-portal flow

The mock server implemented neither get_profile nor get_events and
validated no auth at all, and the e2e suite imported the portal through
/portal.php — which the app classifies as a *simple* portal. The entire
authenticated branch (handshake, token, watchdog, re-auth) therefore had
zero coverage, right before a series of PRs that reworks exactly that.

Mock server:
- serve the canonical /stalker_portal/server/load.php endpoint, which
  enforces the Bearer token and the Infomir MAC format like the real
  middleware; /portal.php stays tolerant so the existing suite keeps
  covering the simple-portal branch
- auth-store.ts models the parts of Stalker 4.9.35 a client can get
  wrong: plain-text auth failures with HTTP 200, a handshake that is not
  yet a session, idempotent token re-presentation, and permanent
  device_id pinning (including the blank-after-pinned lockout)
- add get_profile (status 0/1/2, device conflict, block_msg) and the
  get_events watchdog; profile advertises watchdog_timeout/timeslot
- new login-required scenario MAC and POST /invalidate-session so tests
  can force a mid-session token loss
- the /stalker proxy route now forwards the token as a Bearer header and
  wraps auth failures in the { payload } envelope, matching web-backend

Also moves extractMac into request-mac.ts: importing it from the
categories handler dragged the whole data generator into any consumer,
which broke unit tests on the workspace alias.

E2E: new stalker-auth.e2e.ts asserts handshake precedes get_profile
precedes content, that content requests carry the token while the
handshake does not, that the plain-text failure body is never rendered,
and that the client re-authenticates after the portal drops the session.

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

* fix(mock): address CodeQL findings in the new portal auth code

Two genuine defects in the code this PR added:
- readBearerToken's /Bearer\s+(.*)$/ backtracks polynomially on
  "bearer" followed by a long run of spaces; require the token to start
  with a non-space character instead
- the /stalker proxy route read query params as strings without
  narrowing, so a repeated key (?url=a&url=b) arrives as an array and
  String.prototype.includes silently changes meaning

The remaining three alerts (missing rate limiting x2, sensitive data in
a GET query) are web-service hygiene rules aimed at internet-facing
services. The mock servers bind to localhost, serve fabricated data,
ship in no artifact, and deliberately mirror the real backend proxy's
token-in-query contract; a rate limiter would break the E2E suite that
hammers them. Exclude only those two apps from analysis via a documented
CodeQL config; every shipped path keeps full coverage.

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

* fix(mock): tighten portal-auth fidelity per review

Review follow-up on #1324 (Greptile 2xP1, Codex 3xP2), all valid:

- adoptToken only accepts tokens the mock actually issued (or the
  already-bound one). The stock server pins any presented Bearer —
  handshake is stateless there — but a fixture that does the same
  cannot catch a client with a broken token pipeline; documented as a
  deliberate strictness divergence.
- /invalidate-session clears tokens but keeps pinned device identity:
  losing a token never unpins device_id on a real portal, so changed
  identity after re-auth must still hit the device-conflict branch.
- The login-required scenario gates on actual do_auth completion
  instead of auth_second_step: the app sends auth_second_step=1 on its
  very first get_profile, so the parameter check was trivially
  bypassed and the status-2 flow never exercised. do_auth is now the
  faithful boolean step (non-empty credentials -> {js:true}, recorded;
  empty -> {js:false}).
- /server/load.php — the second URL shape isFullStalkerPortal
  recognizes — is now served and enforced, directly and through the
  /stalker proxy predicate, so full-portal tests cannot silently fall
  into the tolerant branch.

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

* test(stalker): prove content actually reloads after re-authentication

Review follow-up on #1324 (Codex P2, valid — the "passes for the wrong
reason" class): the re-auth test only polled for a fresh handshake and
a negative body-text assertion, both of which pass even if the original
content request is never replayed or stays unauthorized. Capture the
content token from the initial import, then assert a post-invalidation
CONTENT request goes out under a DIFFERENT token and that the ITV
categories actually render — the mock only answers content for an
adopted token, so this proves the new token round-tripped through
get_profile. Verified against a live mock that the token genuinely
rotates (old token -> "Authorization failed.", new token -> content).

Also documents the second Codex P2: the mock is deliberately strict on
/server/load.php (a real portal enforces auth there); the import dialog
vs session predicate divergence is a separate app bug the strict
endpoint will let a later PR cover.

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

* test(stalker): serialize the portal specs and bind mocks to loopback

Review follow-up on #1324 (Codex, 4xP2):

- Parallel-reset race: under the workspace `fullyParallel` preset the new
  auth file ran concurrently with stalker.e2e.ts against one shared mock
  process, and each `beforeEach` wiped global state (sessions, favorites)
  mid-assertion in the other. Reproduced locally: both suites green in
  isolation, two failures when run together. Merged the auth tests into
  stalker.e2e.ts and pinned the file to `mode: 'serial'`, which also
  removes the pre-existing race between that file's own tests. 19/19
  green across three consecutive runs.
- Watchdog was recorded but never asserted, so the suite would stay green
  if the full-portal workflow stopped pinging or dropped its token —
  `sendWatchdogPing` swallows failures. Now polls for an authenticated
  `get_events`.
- Both mock servers listened on every interface (stalker: `listen(PORT)`
  with no host; xtream: an explicit `0.0.0.0` default), which made the
  CodeQL exclusion's "binds to localhost" rationale untrue. Both now
  default to `127.0.0.1` with a `HOST` opt-in, and the config comment
  states plainly what the directory-wide ignore trades away.
- Documented that the login-required scenario is HTTP-level only for now:
  the client's `do_auth` path is dormant and sends empty credentials, so
  the fixture is waiting on that client-side work rather than claiming
  end-to-end coverage.

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

* test(stalker): force a real auth failure before asserting it stays hidden

Review follow-up on #1324 (Codex 2xP2 + 1xP3), all valid:

- The "never surfaces the plain-text auth failure" test only performed a
  successful import, so its negative body assertions were vacuous. It now
  imports with a MAC outside the Infomir OUI: the strict endpoint answers
  get_profile with a bare {status:1}, no token is ever adopted, and every
  content request keeps returning "Authorization failed." Unlike an
  invalidated session this cannot be repaired by the client retry, so the
  failure is genuinely observed (asserted directly against the proxy) and
  only then checked for not leaking into the UI.
- docs/architecture/xtream-mock-server.md still documented the wildcard
  bind that 4b31f7167 replaced with a loopback default; it now states the
  new default and the HOST=0.0.0.0 opt-in needed for phone/STB/container.
- Removed a dangling "Known app-side gap: the" fragment left in the
  stalker mock README.

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

* fix(mock): scope /reset by MAC so parallel specs stop wiping each other

The re-authentication test passed locally but failed all three CI
attempts: no request carried a token, because self-hosted.e2e.ts issues
a GLOBAL `POST /reset` against the same mock from a parallel Playwright
worker, destroying the session mid-import. Running only stalker.e2e.ts
locally never triggered it.

Serializing within one file (4b31f7167) could not fix this — the
interference is between files. Mock state is per-MAC, so `/reset` now
accepts `?macAddress=` and clears only that MAC's data, favorites,
session and watchdog counters; the unscoped form is kept for callers
that own the whole server. Both spec files now reset only the MACs they
own, so no worker can disturb another.

Verified: a scoped reset of one MAC leaves another MAC's session intact
(and its own dies), and stalker.e2e.ts + self-hosted.e2e.ts run together
23/23 green — the combination that reproduced the CI failure.

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

* test(e2e): scope the last global Stalker reset in sources-pwa helpers

Completes 3a93fef0f: that commit scoped self-hosted.e2e.ts but missed
resetPwaMockServers, which still wiped the whole Stalker fixture from a
third spec file. Scope it to the two MACs this suite owns.

The auth tests use dedicated MACs no sibling touches, so portal sessions
— the fragile state — can no longer be cleared by a parallel worker.
Content MACs still overlap between files, which is harmless: that data is
regenerated deterministically from the same seed.

Verified with the full interfering set running together:
stalker.e2e.ts + self-hosted.e2e.ts + sources-pwa.e2e.ts, 26/26 green.

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

* test(stalker): await the first authenticated content request

The re-auth test kept failing on CI (3/3 attempts) with an undefined
token while passing locally. My earlier diagnosis — a sibling spec's
global /reset — was wrong: the failure survived the scoped-reset fix.

Real cause is a race in the test itself. `addFullStalkerPortal` only
awaits the route change, so on a slower runner the first authenticated
content request has not been recorded yet when the token is read; the
sibling test that passes happens to await `.category-item` first. Poll
for a content request carrying a token before capturing it.

The scoped-reset work stands on its own merits (cross-file resets were
a real hazard), it just was not what broke this test.

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

* test(stalker): drop serial mode, batch resets, cover the auth handlers

Review round on a44f8135f plus a stability regression I introduced.

Codex, both valid:
- The proxy route stripped `token` from the forwarded query, so
  `handshake` never saw a presented token and the idempotent-handshake
  behaviour I documented was unreachable through the PWA path. The real
  backend forwards every param except `targetId` *and* sets the header;
  match it. Verified through the proxy: re-handshake now returns the
  same token with not_valid 0.
- The login-required scenario had no committed test, so the README claim
  was unbacked. Added auth-handlers.spec.ts (status 2 -> do_auth ->
  profile, MAC-format rejection, device conflict, idempotent handshake,
  watchdog). Handlers are called directly because the dispatcher pulls in
  the faker-based generator, which this project's Jest cannot transform.
- Sibling suites now own disjoint MACs (00:1A:79:5F:*) instead of
  sharing the Stalker suite's, so no reset can reach another suite's
  state at all.

Stability: a baseline run of master passed 23/23 first try while this
branch failed a different test each run, so the flakiness was mine.
`mode: 'serial'` was a stand-in for isolation that per-MAC scoping now
provides properly, and it amplified every flake by aborting the rest of
the file; removed. `beforeEach` also fired seven sequential resets — the
endpoint now accepts repeated `macAddress` params so a suite clears all
of its MACs in one request. Added a retrying POST helper after an
ECONNRESET on a control call.

Verified: three consecutive runs of stalker + self-hosted + sources-pwa,
26/26 each; 28 mock unit tests; lint clean.

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

* test(stalker): restore serial mode for the shared-scenario file

Review follow-up (Codex P2), valid: the previous commit removed
`mode: 'serial'` while every `beforeEach` still resets all OWNED_MACS,
so under fullyParallel one test in this file could clear another's data
or session mid-run.

Of the two suggested fixes, serialize rather than give each test its own
MAC: the tests here are written against scenario fixtures (default,
minimal, embedded-series) whose shapes the assertions encode, so a MAC
per test would mean inventing a scenario per test and rewriting
pre-existing assertions. Cross-file isolation stays with the disjoint
sibling MAC range, which is what serial was wrongly standing in for
before.

The header now states both levels explicitly so the next reader does not
undo one of them.

Verified: three consecutive runs of stalker + self-hosted + sources-pwa,
26/26 each.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 21:52:09 +02:00
4gray a2fafcfc08 test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark

* feat(xtream-mock-server): add deterministic 100k fixture

* style(xtream-mock-server): apply repository formatting

* fix(xtream-mock-server): harden performance fixture data

* feat(xtream-mock-server): add performance control plane

* docs(performance): correct Xtream capture plan

* fix(xtream-mock-server): harden performance controls

* fix(xtream-mock-server): harden control lifecycle

* feat(performance): add Xtream preload markers

* feat(performance): trace Xtream main phases

* feat(performance): mark Xtream store publications

* feat(performance): trace Xtream database phases

* feat(performance): trace Xtream delete cancellation

* feat(performance): capture Xtream phase attribution

* feat(performance): mark Sources Xtream refresh

* test(performance): define Xtream benchmark evidence contracts

* test(performance): add Xtream benchmark runner

* test(performance): surface failure evidence writes

* test(performance): align database read clock

* test(performance): preserve capture failure contracts
2026-07-28 08:08:07 +02:00