Commit Graph
2741 Commits
Author SHA1 Message Date
Claude 2cea665689 fix(playlist): keep IPv6 brackets, honor written default ports, collision-safe dedupe keys
- Cleanup of prose punctuation and bracket quotes now loses to parseability:
  ']' closes both a quote and an IPv6 authority, so a strip that breaks the
  address is rolled back and 'Portal: http://[2001:db8::1]' stays importable.
- The explicit-port check reads the raw authority instead of parsed.port,
  which the WHATWG parser blanks for a protocol default — a written ':80' is
  the user's choice and a labeled port no longer overwrites it.
- Candidate dedupe keys are JSON-serialized, so two accounts whose
  credentials contain the former separator no longer collapse into one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-16 08:36:30 +00:00
Claude 36f340c907 fix(playlist): stop URL spans at inline delimiters, IPv6-safe port check, linear span scan
- '|' ends a URL span: it is the inline field delimiter of one-line handouts
  ('Server: http://host|User: alice'), and letting the span run through it
  masked the next label and dropped the account entirely.
- The explicit-port check reads parsed.port instead of matching colons in the
  authority, so an IPv6 endpoint no longer mistakes its address for a port
  and now receives the separately labeled one.
- The out-of-span label search walks the ordered spans with a forward cursor
  instead of rescanning them per match; detection runs on every keystroke and
  the rescan was quadratic on pastes full of host-shaped query keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-16 07:44:57 +00:00
Claude 34a5aa01d5 fix(playlist): mask every password parameter on cards, skip host labels inside URLs
- Card display now hides the value of EVERY password query parameter and
  recognizes percent-encoded names ('pass%77ord'), which URLSearchParams —
  and therefore the importer — reads as 'password'. The URL stays otherwise
  byte-identical so the user still recognizes their link.
- The host-label matcher keeps scanning past host-shaped query keys inside
  unrelated links ('/setup?url=guide') instead of discarding the field, so a
  real 'Server:' line further down still wins the credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-16 06:14:37 +00:00
Claude 422459e706 fix(playlist): disambiguate the MAC label in the auto-detect E2E
getByLabel matches substrings case-insensitively, so 'Mac Address' also
resolved to the 'Generate device IDs from the MAC address' checkbox and the
Stalker handoff spec failed strict-mode. Match the field exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-16 05:31:11 +00:00
Claude 70f5e41855 test(playlist): E2E coverage for the auto-detect handoff; sanitize labeled URLs
- Adds two web E2E specs for the paste -> pick candidate -> prefilled form
  workflow, exercising the real @switch/viewChild timing the dialog unit
  tests cannot reach: an Xtream handoff (including the masked password on
  the card) and a Stalker handoff whose pasted message survives a method
  switch. The repo requires E2E coverage for import workflows.
- A labeled server URL now gets the same sentence-punctuation cleanup the
  URL scanner applies, so 'Server: http://host!' no longer prefills a
  hostname DNS can never resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-16 05:23:31 +00:00
Claude 2ef6e51067 fix(playlist): port-aware portal ambiguity keys, mask URLs beyond the extraction cap
- The portal installation key is derived after labeled-port completion, so
  'http://panel:8080/c/' next to 'http://panel/c/' plus a 'Port: 8080' line
  reads as one panel instead of stripping the portal from its MAC list.
- Label matchers mask EVERY URL-shaped span, not just the capped extraction
  result: a URL past the candidate cap still carries a label-shaped query,
  and its credentials must not attach to an earlier credential-less panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 22:50:07 +00:00
Claude f3723eb8e3 fix(playlist): apply separately labeled ports to detected API and portal URLs
'Portal: http://host/…' plus 'Port: 8080' on its own line is a real handout
shape; the labeled port now completes port-less Xtream API URLs, shaped
Stalker portals, and the generic-URL fallback alike. A URL stating its own
port explicitly is never overridden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 20:21:21 +00:00
Claude 6e81d7034d fix(playlist): strip panel_api.php endpoints, path-aware portal ambiguity for tenant installs
- normalizeXtreamServerUrl strips panel_api.php like the other API
  endpoints, so a panel_api.php handout (pasted or auto-detected) prefills
  the server base instead of a URL the transport would double-suffix.
- The multi-MAC portal ambiguity key is origin plus the installation base
  path: tenant installs sharing one origin (/a/stalker_portal/c/ vs /b/...)
  no longer collapse, while alternate endpoints of one install (/c/,
  portal.php, server/load.php, Real/Panel pairs) still compare equal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 19:53:41 +00:00
Claude 526e8abc3e fix(playlist): mask query passwords on M3U cards, drop stale auto-detect prefills, doc the assembled-host exception
Addresses Copilot's suppressed review comments:

- The m3u-url candidate card no longer shows a get.php password in clear —
  the display masks the query value while the untouched URL still prefills
  the form.
- A picked candidate is dropped if the user switches to another method
  before its target form mounts, so it cannot lie in wait and prefill a
  later visit.
- The parser contract now names its one assembled-value exception
  (labeledHostUrl completing a scheme-less host with http:// and a labeled
  port).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 19:46:50 +00:00
Claude ba19f51a12 fix(playlist): lazy capped scanners and wrapping candidate headers
- URL and MAC extraction drive their regex matchers lazily and stop at the
  caps instead of materializing every match of a pathological paste first —
  detection runs on each keystroke, so allocation must stop where the cap
  stops accepting.
- The candidate card header wraps on narrow phone dialogs instead of
  overflowing horizontally past the Fill-the-form action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 19:20:53 +00:00
Claude c67f0f8eb4 fix(playlist): DOM-safety caps far beyond real handouts, port-aware portal ambiguity
- The MAC and candidate caps are raised to 64/96: they exist to keep a
  pathological paste (a log full of MAC-shaped hex) from rendering thousands
  of cards, not to bound real handouts — scanner dumps top out around a
  couple dozen accounts, so no real multi-account message is truncated.
- The multi-MAC portal ambiguity guard compares URL origins instead of
  hostnames, so two panels on one DNS name but different ports no longer
  cross-pair; URL normalization drops default ports, keeping the Real/Panel
  ':80'-vs-bare pairs of scanner dumps unambiguous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 19:12:18 +00:00
Claude c0df0dfabd ci: retrigger checks after category-management macOS E2E flake
One predicate timeout in category-management.e2e.ts on the macOS runner
(115 passed / 1 failed); the same suite is green on Ubuntu and Windows in
the same run, and nothing in this PR touches category management. The
earlier frame-copy smoke flake passed on this run, confirming both as
runner timing flakes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 12:43:27 +00:00
Claude 3248c54c1e fix(playlist): cover the generic-URL fallback in the multi-MAC portal ambiguity guard
The round-2 guard judged ambiguity over portal-shaped URLs only, while the
portal picker falls back to a generic URL when no shaped one exists — two
root-URL panels with several MACs still cross-paired through that fallback.
The guard now inspects the exact pool the picker draws from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 12:07:07 +00:00
Claude 9b58ed505a ci: retrigger checks after embedded-mpv frame-copy smoke flake
The packaged frame-copy smoke on the linux portable build timed out waiting
for the first rendered frame (llvmpipe runner); the same job passed on the
previous commits of this branch and nothing in this PR touches playback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 10:59:27 +00:00
Claude 1d599322f4 fix(playlist): omit ambiguous portals for multi-MAC pastes, align scanner and candidate caps
- Several MACs meeting portal-shaped URLs on different hosts now yield
  MAC-only candidates: there is no deterministic owner for either portal, and
  prefilling half the accounts with the wrong panel only fails later at
  authentication. Same-host Real/Panel pairs and one portal shared by a MAC
  list stay unambiguous.
- The URL scanner cap is raised to match the candidate cap (16), so a
  nine-link handout is no longer truncated to eight before assembly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 10:30:24 +00:00
Claude 98c3b442f0 fix(playlist): address bot review — no identity cross-pairing across MACs, honest caps, password in dedupe key
- Multi-MAC messages no longer attach globally scanned serial/device-ID/
  signature/credential labels to every MAC: with several accounts in one
  message there is no reliable owner for a first-match label, and a device ID
  submitted with the wrong MAC is pinned by the portal permanently. Identity
  fields now attach only when exactly one MAC is present.
- MAC and candidate caps raised (12/16) so real multi-account lists are never
  silently truncated; the caps remain only as pathological-paste guards.
- The candidate dedupe key includes the password, so two accounts sharing a
  server and username (e.g. a rotation message) both stay selectable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 10:18:10 +00:00
Claude e17a4ba641 feat(playlist): extend auto-detect with bare handouts, spaced serials, decorated dual device IDs
Second corpus batch (14 real reseller messages; 11 already parsed). New:

- Bare 'URL, token, token' handouts with no labels at all become a
  low-confidence Xtream guess — only when the message contains exactly those
  three lines, a single generic URL, and no label/MAC produced anything.
- Separator-less serials ('SN 38415545307A3') are accepted when the value is
  hex-shaped, so prose cannot match.
- The dual device-ID marker now allows any symbol between 1 and 2 (¹💥², 1/2)
  and whitespace-only label separators, again guarded by the hex value shape.
- A separately labeled port completes a port-less 'Portal: http://host' URL.

Nine messages from the batch join the test corpus verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 09:55:22 +00:00
Claude b8af7284fa fix(playlist): do not fold real flag emoji in auto-detect label folding
Regional-indicator runs of exactly two are real flag emoji (🇳🇱); folding
them glued letters onto adjacent text and could break a following label's
word boundary. Only runs of three or more — fancy-text words like 🇺🇸🇪🇷 —
fold to ASCII now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-15 07:44:15 +00:00
Claude 7b57b22e88 feat(playlist): fold negative-circled labels and parse dual device IDs in auto-detect
Extends the deterministic detector for three real reseller message shapes:

- Negative-circled / squared / regional-indicator 'font' capitals and circled
  digits (🅤🅢🅔🅡, 🅜➌🅤) that NFKC leaves alone are folded to the ASCII they
  depict before scanning, so decorated USER/PASS/URL labels are recognized.
- 'DEVICE ID=> 1&2 <hex>' hands one hex value to both device-ID slots.
- 'S N', 'S/N', 'S.N' serial labels, the Turkish 'MAC ADRESİ' label, and a
  guard so 'ADULT PASS' (the parental PIN) is not read as the account password.

All three pasted messages are covered verbatim by new parser tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-11 20:38:58 +00:00
Claude de6d012a57 feat(playlist): recognize decorated reseller labels in auto-detect import
Reseller messages dress labels in Unicode math alphabets and arrow
separators to slip past chat spam filters. The detector now NFKC-normalizes
the pasted text before scanning, so decorated labels fold back to the plain
ASCII the vocabulary matches, and accepts arrow/geometric/dingbat glyphs,
'>' and a space-surrounded dash as label separators alongside ':' and '='.
A hyphen glued inside a word stays prose, so hyphenated words are still not
read as labeled values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-11 20:19:14 +00:00
Claude c26596abb2 fix(playlist): keep the pasted provider message when switching import methods
The auto-detect surface is destroyed by the dialog's method switch, so
inspecting a prefilled form and coming back lost the paste. The text now
lives on the dialog and is fed back into the surface on re-entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-11 19:40:35 +00:00
Claude 3dee1978f6 feat(playlist): auto-detect import method that parses pasted provider messages
Adds a deterministic parser (detectProviderImportCandidates in
libs/shared/interfaces) that scans a pasted provider message for URLs, MAC
addresses, and labeled credentials/identity fields (username, password,
serial, device IDs, signatures), classifies the source as Xtream, Stalker,
or an M3U link/body, and returns ranked candidates. Every extracted value is
a verbatim substring or a canonical form from an existing shared normalizer;
detection only proposes — the existing forms and their behavioral probes
(portal discovery, connection test) stay authoritative.

The Add playlist dialog gains a sixth "Auto-detect" method with a textarea
that runs the parser on every edit and renders candidate cards; picking one
switches to the matching import form with the fields prefilled.

Detection runs fully locally; pasted text never leaves the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JE1De2dfyRZMYh4H2iGXb4
2026-08-11 11:41:47 +00:00
dependabot[bot] 5c9411869a chore(deps): bump the npm-minor-patch group across 1 directory with 11 updates (#1416)
Bumps the npm-minor-patch group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [hls.js](https://github.com/video-dev/hls.js) | `1.6.16` | `1.6.17` |
| [marked](https://github.com/markedjs/marked) | `18.0.7` | `18.0.9` |
| [video.js](https://github.com/videojs/video.js) | `8.23.9` | `8.24.0` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.62.0` | `1.62.1` |
| [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core) | `1.15.46` | `1.15.47` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.65.0` | `8.66.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.65.0` | `8.66.0` |
| [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.65.0` | `8.66.0` |
| [ng-mocks](https://github.com/help-me-mom/ng-mocks) | `14.15.3` | `14.16.1` |
| [tsx](https://github.com/privatenumber/tsx) | `4.23.1` | `4.23.11` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.65.0` | `8.66.0` |



Updates `hls.js` from 1.6.16 to 1.6.17
- [Release notes](https://github.com/video-dev/hls.js/releases)
- [Changelog](https://github.com/video-dev/hls.js/blob/master/docs/release-process.md)
- [Commits](https://github.com/video-dev/hls.js/compare/v1.6.16...v1.6.17)

Updates `marked` from 18.0.7 to 18.0.9
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.7...v18.0.9)

Updates `video.js` from 8.23.9 to 8.24.0
- [Release notes](https://github.com/videojs/video.js/releases)
- [Changelog](https://github.com/videojs/video.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/videojs/video.js/compare/v8.23.9...v8.24.0)

Updates `@playwright/test` from 1.62.0 to 1.62.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.0...v1.62.1)

Updates `@swc/core` from 1.15.46 to 1.15.47
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.15.47/packages/core)

Updates `@typescript-eslint/eslint-plugin` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/parser)

Updates `@typescript-eslint/utils` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/utils)

Updates `ng-mocks` from 14.15.3 to 14.16.1
- [Release notes](https://github.com/help-me-mom/ng-mocks/releases)
- [Changelog](https://github.com/help-me-mom/ng-mocks/blob/main/CHANGELOG.md)
- [Commits](https://github.com/help-me-mom/ng-mocks/compare/v14.15.3...v14.16.1)

Updates `tsx` from 4.23.1 to 4.23.11
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.1...v4.23.11)

Updates `typescript-eslint` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: "@swc/core"
  dependency-version: 1.15.47
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
- dependency-name: hls.js
  dependency-version: 1.6.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: marked
  dependency-version: 18.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: ng-mocks
  dependency-version: 14.16.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
- dependency-name: tsx
  dependency-version: 4.23.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor-patch
- dependency-name: typescript-eslint
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
- dependency-name: video.js
  dependency-version: 8.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 11:28:33 +02:00
4gray 10e8187b16 chore(deps): update epg-parser to 0.5.0 (#1413) 2026-08-11 03:29:05 +02:00
4gray de77c6d467 fix(playback): update mpegts.js to 1.8.1 (#1412) 2026-08-11 03:15:08 +02:00
4gray 77842b9d04 fix(playback): update Shaka Player to 5.2.4 (#1411) 2026-08-11 03:14:03 +02:00
4gray 861c6798ee ci(deps): split sensitive dependency updates (#1409) 2026-08-11 02:56:50 +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
dependabot[bot] 2a7d7c315e chore(deps-dev): bump the nx-version-updates group across 1 directory with 11 updates (#1401)
Bumps the nx-version-updates group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@nx/angular](https://github.com/nrwl/nx/tree/HEAD/packages/angular) | `22.7.7` | `22.7.8` |
| [@nx/devkit](https://github.com/nrwl/nx/tree/HEAD/packages/devkit) | `22.7.7` | `22.7.8` |
| [@nx/esbuild](https://github.com/nrwl/nx/tree/HEAD/packages/esbuild) | `22.7.7` | `22.7.8` |
| [@nx/eslint](https://github.com/nrwl/nx/tree/HEAD/packages/eslint) | `22.7.7` | `22.7.8` |
| [@nx/eslint-plugin](https://github.com/nrwl/nx/tree/HEAD/packages/eslint-plugin) | `22.7.7` | `22.7.8` |
| [@nx/jest](https://github.com/nrwl/nx/tree/HEAD/packages/jest) | `22.7.7` | `22.7.8` |
| [@nx/js](https://github.com/nrwl/nx/tree/HEAD/packages/js) | `22.7.7` | `22.7.8` |
| [@nx/playwright](https://github.com/nrwl/nx/tree/HEAD/packages/playwright) | `22.7.7` | `22.7.8` |
| [@nx/web](https://github.com/nrwl/nx/tree/HEAD/packages/web) | `22.7.7` | `22.7.8` |
| [@nx/workspace](https://github.com/nrwl/nx/tree/HEAD/packages/workspace) | `22.7.7` | `22.7.8` |
| [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) | `22.7.7` | `22.7.8` |



Updates `@nx/angular` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/angular)

Updates `@nx/devkit` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/devkit)

Updates `@nx/esbuild` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/esbuild)

Updates `@nx/eslint` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/eslint)

Updates `@nx/eslint-plugin` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/eslint-plugin)

Updates `@nx/jest` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/jest)

Updates `@nx/js` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/js)

Updates `@nx/playwright` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/playwright)

Updates `@nx/web` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/web)

Updates `@nx/workspace` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/workspace)

Updates `nx` from 22.7.7 to 22.7.8
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/22.7.8/packages/nx)

---
updated-dependencies:
- dependency-name: "@nx/angular"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/devkit"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/esbuild"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/eslint"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/eslint-plugin"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/jest"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/js"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/playwright"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/web"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: "@nx/workspace"
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
- dependency-name: nx
  dependency-version: 22.7.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: nx-version-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 23:05:20 +02:00
4grayandClaude Fable 5 d73551d780 fix(pwa): tolerate broken IPv6 routes and surface provider error codes (#1404)
* fix(pwa): tolerate broken IPv6 routes and surface provider error codes

Node's happy-eyeballs racing gives each address attempt only 250 ms, so a
dual-stack provider hostname behind an IPv4-only VPN namespace (Gluetun,
WireGuard) exhausts every attempt and the web backend answered with a bare
502. Raise the per-attempt budget to 2500 ms at startup — keeping the
IPv6->IPv4 fallback automatic — while an explicit
--network-family-autoselection-attempt-timeout from NODE_OPTIONS still wins.

Provider proxy failures now log the target hostname plus the underlying
Node error codes (never the URL query, which carries credentials) and
return the primary code in the error body, so the app shows
"Bad Gateway (ETIMEDOUT)" instead of an unexplained 502.

Closes #1400

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

* fix(pwa): surface proxy network codes in import/refresh toasts, document runtime contract

Codex: /parse connection failures arrive as HTTP 500 whose body carries the
new code field, but fetchFromUrl()/refreshPlaylist() mapped only the HTTP
status, so the toast stayed generic. Append the code to the translated
message (regression-covered for both flows).

Greptile: record the web-backend happy-eyeballs/diagnostics runtime contract
in CLAUDE.md's monorepo structure section.

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

* fix(pwa): drop provider reason phrases from logs, honor underscore flag spellings

Codex round 2: the HTTP reason phrase is provider-controlled and can echo
the credential-bearing request URL, so the failure log now carries only the
numeric status; and Node treats underscores and dashes interchangeably in
flag names, so the explicit-override check normalizes spelling before
matching (verified live: --network_family_autoselection_attempt_timeout
applies in both CLI and NODE_OPTIONS forms).

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

* fix(pwa): match the timeout flag as a complete NODE_OPTIONS token

Codex round 3 (P3): a raw substring search also fired on the flag text
embedded in another option's value, silently skipping the 2500 ms default.
Tokenize NODE_OPTIONS on whitespace and match the normalized option name
exactly or with '='.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:09:05 +02:00
4grayandClaude Opus 5 5ad86e094c refactor(stalker): drop unused limit state from selection feature (#1402)
* refactor(stalker): drop unused limit state from selection feature

The `limit` field and `setLimit` method lost their last consumers when
catalog pagination was replaced by infinite scroll (#1392/#1395): the
facade no longer calls setLimit and getTotalPages is gone.

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

* docs(stalker): retire pagination API from the store baseline

The compatibility baseline still told future refactors to preserve
`limit`/`setLimit` and `getTotalPages`, all three of which are gone with
the catalog pagination removal. Record them in the doc's existing
Removed section instead.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:02:41 +02:00
4gray a9e07d696f chore(deps): align Nx packages on 22.7.7 (#1396) 2026-08-10 08:19:43 +02:00
4grayandClaude Fable 5 5b211faf73 feat(remote-control): cover live collections, honest volume, status resets (#1399)
* feat(remote-control): cover live collections, honest volume, status resets

Remote control previously worked only on the three routed live layouts
(M3U player, Xtream live, Stalker ITV); playing live TV from favorites,
recently viewed, or the global collections left the mobile remote inert.

- Wire channel up/down, number select, and status publishing into the
  unified live tab, covering per-portal and global favorites/recent for
  M3U, Xtream, and Stalker; navigation follows the search-filtered,
  sorted list exactly as rendered (shared deriveVisibleFavoriteChannels)
- Treat non-live status updates as snapshots in the main process so
  stale now-playing fields are cleared instead of merged forever
- Publish a reset snapshot from every integration on destroy, so
  leaving a live view clears the remote instead of freezing it
- Report M3U supportsVolume only for built-in inline playback and no-op
  volume commands while MPV/VLC/Embedded MPV owns the audio
- Publish live status for Stalker radio (same layout, same handlers)
  and fix its channel-number lookup for non-numeric radio ids

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH

* fix(remote-control): review-loop hardening for status honesty

- Make the non-live status update an authoritative reset in the main
  process: only portal survives, supportsVolume is forced false, stray
  now-playing fields from callers are dropped (Copilot review)
- Stop Stalker radio status from leaking an unrelated TV channel's EPG:
  the ITV-keyed bulk cache survives itv->radio navigation and Ministra
  ids collide across the two lists, so EPG fields publish for itv only
- Publish the reset snapshot when the M3U active channel clears in
  place (e.g. quitting external MPV), not only on route destroy
- Consider a live external session in the M3U volume gate: a
  diagnostic-recovery MPV/VLC launch owns the audio even while a web
  player is configured; republish capability on session start/end
- Share one REMOTE_CONTROL_RESET_STATUS constant across all four
  integrations instead of four hand-copied literals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH

* fix(remote-control): external session outranks DASH in M3U volume gate

The managed clear-DASH MPV/VLC fallback (Shaka browser-support preflight
failure) leaves activeChannelIsDash() true while the external session
owns the audio, so the DASH shortcut bypassed the session check and kept
advertising remote volume support. The live-session check now precedes
the DASH branch; radio stays first because its inline audio element is
always mounted and remains audible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 08:18:20 +02:00
4grayandClaude Fable 5 9aeb83e515 fix(m3u): forward playlist-level custom headers to external players (#1397)
* fix(m3u): forward playlist-level custom headers to external players

The custom User-Agent/Referer/Origin stored on an M3U playlist only
reached the built-in web players (via the Electron webRequest override).
MPV/VLC and the embedded MPV player make their own HTTP requests and
received only the per-channel #EXTVLCOPT values, so a playlist-wide
custom User-Agent was silently dropped for UA-locked providers (#1221).

External launch payloads now resolve each header independently: the
channel-level #EXTVLCOPT value wins, the playlist-level value is the
fallback, blank values count as absent — matching the semantics the
unified favorites/recent stream resolver already had. Covers the
auto-launch and catch-up effects in m3u-state, the manual MPV/VLC
fallback and the embedded MPV payload in VideoPlayerComponent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL

* fix(playback): send Origin as a real VLC header and cover header IPC in E2E

Review follow-ups: VLC only used the Origin value as an :http-referrer
fallback while MPV already sent it via --http-header-fields; both VLC
paths (fresh spawn and RC enqueue) now emit the same
buildHttpHeaderFields list, so a real `Origin: ...` header reaches the
provider, deduplicated against an explicit headers-map Origin. The
legacy origin-as-Referer fallback stays.

The dash-clearkey Electron E2E now asserts the new IPC contract (blank
channel-level headers arrive as undefined, not empty strings) and gains
a scenario that sets a playlist-level User-Agent through the source
editor and verifies the captured MPV fallback launch carries it across
the renderer/main IPC boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-09 23:34:03 +02:00
4grayandClaude Fable 5 e5bb127ede fix(playback): make playback keyboard shortcuts work without shared controls (#1398)
* fix(playback): make playback keyboard shortcuts work without shared controls

With the default configuration (Video.js, webPlayerSharedControls off) the
playback shortcuts advertised in the in-app help and README — Space/K, F,
arrow seek/volume, M — silently did nothing: ControlsShortcuts only exists
inside app-player-controls, which never renders on the preference-off path.

Attach a LegacyPlayerShortcuts wrapper (same arbitration and ignore rules)
in the vendor-chrome HTML5, Video.js, and ArtPlayer players, forwarding the
commands to each engine's own API. Seek stays gated on authoritative VOD
metadata plus a finite positive duration, and a visible playback diagnostic
disables the keys. The legacy ArtPlayer chrome now passes hotkey:false —
its focus-scoped vendor hotkeys ignore defaultPrevented and would
double-handle every key — with its Escape-exits-web-fullscreen behavior
restored by the new wiring.

The playback entries in the in-app shortcut help and README drop their
embedded-MPV-only qualifier, since the keys now work in every runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h2cZi5DcFSbcmV7WgB6qB

* fix(playback): restore audible volume when M unmutes at zero volume

Addresses the Codex review finding on #1398: after arrowing the volume
down to zero (which mutes), M flipped muted off while leaving the volume
at 0, so the player looked unmuted but stayed silent — in all three
legacy engine adapters.

Mirror the shared controls' ControlsVolume semantics with a per-adapter
LegacyMuteMemory: muting remembers the audible volume, and unmuting while
the volume sits at zero restores it, with the same 0.5 fallback when
nothing was remembered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014h2cZi5DcFSbcmV7WgB6qB

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-09 23:33:12 +02:00
4gray 6ad9f3ff8a feat(stalker): discover portal endpoints on add and edit (#1391)
* feat(stalker): discover portal connection on edit

* docs(stalker): document smart endpoint discovery

* fix(stalker): make edited connection persistence atomic

* fix(stalker): serialize edit discovery

* fix(stalker): fence all edit authentication

* fix(stalker): serialize overlapping edits

* fix(stalker): hydrate playlist identity before edit

* test(stalker): await edit hydration

* fix(stalker): release abandoned edit fences

* fix(stalker): reject stale repairs before discovery

* fix(stalker): fence stale portal modes

* test(stalker): align simple portal session guard

* fix(stalker): reject superseded portal responses

* test(stalker): await settled append failure

* fix(stalker): retain abandoned auth fences

* fix(stalker): fence abandoned discovery retries

* fix(stalker): retire restored repair overrides

* fix(stalker): verify repair override retirement

* fix(stalker): preserve edit-owned repair tokens

* fix(stalker): defer repair retirement during edits

* fix(stalker): fence repair history reads

* fix(stalker): fingerprint portal URL credentials

* fix(stalker): persist submitted identity after navigation

* fix(stalker): merge late connection saves

* fix(stalker): keep edits off Xtream save path

* fix(stalker): preserve concurrent edit state

* fix(stalker): reject replaced late edit targets

* fix(stalker): guard every resolved edit write

* fix(stalker): make pwa edit guard transactional

* fix(stalker): migrate pwa flags transactionally

* fix(stalker): reserve pwa edits across tabs

* fix(stalker): coordinate playlist replacements with edit

* fix(stalker): reserve lazy repairs across tabs

* fix(stalker): drain local repair before edit lock

* fix(stalker): block queued repairs during edit drain
2026-08-09 22:34:49 +02:00
4gray ae375e0e8f fix(settings): protect unsaved edits on window close, quit, and reload (#1394) 2026-08-09 18:45:48 +02:00
4grayandClaude Fable 5 cf74f7e4a0 feat(stalker): append portal pages on scroll and drop pagination everywhere (2/2) (#1395)
* feat(stalker): append portal pages on scroll and drop pagination everywhere

Second and final PR of the pagination removal (plan:
.plans/2026-08-09-infinite-scroll-catalog.md). Stalker VOD/series grids now
feed the shared infinite-scroll contract from server-paged appends: portal
pages (server-side size, typically 14) accumulate into one deduplicated
paginatedContent list, page 1 replaces it for the skeleton, hasMoreContent
derives from accumulated length vs total_items (portals that ignore
requested page sizes still terminate), and a failed page > 1 keeps the
accumulated pages on screen with a tail retry (retryContentPage reloads the
same page; loadMore refuses to skip past an unresolved append error). The
facade splits the resource's loading flag by page — skeleton for page one,
tail spinner for appends — and keeps per-identity scroll offsets for
Stalker's INLINE detail round trips; the shared view re-arms its one-shot
restore when a detail opens in the same component instance.

The transitional supportsInfiniteScroll flag and every paged member are
deleted from PortalCatalogFacade; the shared catalog view loses the
mat-paginator, the ?page= round-trip, and the paged query-param branch. The
ITV all-channels grid becomes a client-side render window over the cached
full list (the app's last paginator), and Stalker search pages past its
first capped request via the layout's nearEnd, with a progress guard for
portals that report no usable total.

Validation: 1600 unit tests across 7 projects green (new: vod/series
append + failed-append retry, facade loading split/loadMore guards/scroll
snapshots, ITV window model, compat selector update); catalog-sorting e2e
5/5 (Stalker spec rewritten to scroll model with p>=2 network asserts and
an inline-detail spot-restore round trip; one unrelated nav-timeout flake
reproduced only under parallel machine load), search e2e 16/16, web
stalker e2e green (all-channels grid asserts the windowed count instead of
a paginator range label); lint clean; release note added and validated;
stalker-portal.md, CLAUDE.md, and ui-guidelines updated.

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

* fix(stalker): reset paging on content-type switch and never skip failed search pages

Round-1 review findings on #1395:

1. Codex P1: switching /vod -> /series with the same category id ('*' on
   both section roots) left page > 1 in place — setSelectedContentType did
   not touch paging and setSelectedCategory('*') no-ops on an unchanged id
   — so the new type's FIRST response was treated as an append onto the
   old type's accumulated list. The type setter now resets the page (and
   no-ops entirely when the type repeats, keeping detail round-trip
   restores intact).

2. Greptile P1 + Codex P2: a failed search append left searchHasMore true,
   so the next near-end advanced to page N+1 and permanently omitted the
   failed page. The search now tracks searchAppendError: a failed append
   keeps the accumulated pages and the next near-end RETRIES the same
   page; a failed fresh search (page 1) clears the previous query's cards
   instead of rendering them under the new term (Codex P2).

The page-merge/failure logic moved into applySearchPageSuccess/Failure
methods: Angular resource() never re-fires on params changes in this
repo's template-less jest harnesses (store-hosted resources do), so the
extracted methods carry the unit coverage — accumulation + dedupe,
no-total progress guard, retry-not-skip, fresh-failure clear — plus a
selection spec for the type-switch page reset. portal-stalker-feature
260, portal-stalker-data-access 464, lint clean; catalog-sorting e2e 5/5
and web stalker e2e green. search.e2e shows machine-load nav-timeout
flakes on unrelated M3U/live specs (a runaway third-party process pegs
the host CPU); CI provides the clean independent run.

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

* fix(stalker): include the portal in the search paging identity

Round-2 Codex P1 on #1395: Angular reuses the search route across
/stalker/A/search -> /stalker/B/search, and the paging identity covered
only term + filter — the page number and accumulator survived the portal
change, so the next near-end fetched portal B at the OLD page number and
appended it onto portal A's results while skipping B's first page.

The active playlist id now joins the page-reset identity, the resource
params, the stale-response guard, and the layout's near-end reset key.
Regression spec: switching the active playlist on a reused route resets
the page to 1 and rotates the scroll reset key.
portal-stalker-feature 261, lint clean.

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

* fix(stalker): end paging on no-progress appends even with a reported total

Round-3 Codex P2s on #1395 (same defect in both accumulators): the
no-progress guard only applied when the portal reported no usable
total_items. After a mid-list portal mutation, deduplication can leave
the unique list permanently shorter than the claimed total — hasMore then
stayed true forever and every scroll crossing kept requesting pages past
the end of the data.

An append that adds no unique items now ends paging in both places: the
catalog clamps totalCount to the accumulated length (hasMoreContent turns
false and the count badge reflects what is actually reachable), and the
search requires append progress in the total-backed branch exactly like
the no-total branch. Regression specs cover a duplicate page under a
larger claimed total for both. portal-stalker-data-access 465,
portal-stalker-feature 262, lint clean.

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

* fix(stalker): explicit search retry control and per-portal scroll identities

Round-4 findings on #1395:

1. Greptile P1: with the results pane parked at the bottom, repeated
   append failures exhausted the scroll auto-fill budget while the
   near-end latch stayed armed — the retry path was reachable only
   through another nearEnd event that could never fire. The search page
   now renders an explicit retry control under the results whenever an
   append has failed (same wording as the catalog grid tail), wired to
   the existing retry-same-page path, so recovery never depends on
   producing another scroll event.

2. Codex P2: the facade's saved-scroll map survives a same-config portal
   switch (the vod/series route provider is reused across /stalker/A ->
   /stalker/B), and its identity lacked the playlist — portal A's offset
   could restore onto portal B's unrelated catalog. The playlist id now
   leads the scroll identity; regression spec covers the cross-portal
   non-restore and the return restore.

portal-stalker-feature 263, lint clean.

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

* fix(stalker): restore the search results scroll after an inline detail

Round-5 Codex P2 on #1395: the search layout destroys the results
container while an inline detail is shown (showDetails) and recreates it
at offset zero — with the new multi-page accumulation a user could load
several pages, open a result far down the list, and land back at the top
on close even though the accumulated results survived.

SearchLayoutComponent now exposes a scroll handoff for hosts whose
details replace the results (getResultsScrollTop /
restoreResultsScrollTop on the container it owns), and the Stalker search
captures the offset when a detail opens and restores it one-shot after
the container is recreated on close. Regression specs cover the layout
handoff methods and the capture/restore round trip.
portal-shared-ui 90, portal-stalker-feature 264, lint clean.

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

* fix(stalker): clear accumulated search results for unsearchable portals

Round-6 Codex P2 on #1395: the loader's early returns (deleted or
malformed playlist on a reused route) predate the accumulator and
returned [] without touching it — the previous portal's cards kept
rendering under the new context once loading settled.

Every no-portal early return now goes through resetSearchAccumulator(),
which empties the accumulated list and both paging flags; the short-term
path uses it too (and now also clears a stale append error). Regression
spec covers the full reset. portal-stalker-feature 265, lint clean.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 13:36:59 +02:00
4gray d73acd6bfc fix(playback): clarify external player launch feedback (#1388) 2026-08-09 13:33:07 +02:00
4grayandClaude Fable 5 8442747c37 feat(xtream): replace catalog pagination with infinite scroll (1/2) (#1392)
* feat(xtream): replace catalog pagination with infinite scroll

Xtream movie/series/live catalogs now load continuously while scrolling
instead of paging. The selection store keeps a growing visibleCount render
window over the in-memory catalog (initial 50, +50 per load) plus a saved
scroll state, so opening a title and going back restores the exact spot. A
shared InfiniteScrollDirective (portal/shared/ui) fires loadMore near the
bottom (edge-triggered, mirroring search-layout) and auto-fills viewports
taller than the initial window by measuring container overflow — capped at
10 self-initiated loads per list identity, with a ResizeObserver re-check.

The shared CategoryContentViewComponent branches on the transitional
PortalCatalogFacade.supportsInfiniteScroll flag: Xtream scrolls, Stalker
keeps its server-driven paginator and ?page= round-trip untouched until its
append lands (PR 2), after which the paged facade members and the flag are
deleted. grid-list loses its dead built-in paginator and gains tail states
(append spinner, retry-on-error) plus content-visibility on cards. The
in-portal search results reuse the search layout's nearEnd hook to window
their full result set instead of rendering it unbounded.

Validation: portal-xtream-data-access (234), portal-xtream-feature (357),
portal-catalog-feature (22), portal-shared-ui (77, incl. new directive
spec), portal-stalker-* (253) unit tests green; catalog-sorting e2e 5/5
(new scroll-growth + spot-restore test against the large 200-item mock
scenario, Stalker paged spec unchanged); search e2e 16/16; lint green;
release note added and validated.

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

* fix(xtream): auto-fill search results and refresh the near-end latch

Review findings from #1392: the in-portal search window could stall at its
first 60-item chunk when the rendered cards did not overflow the container
— nearEnd only fired on real scroll events (Greptile P1), the search
layout's edge latch survived a result-set replacement (Codex), and the
shared directive's latch went stale after appended content moved the
bottom out of the threshold (Codex).

The search layout now drives its results container through the shared
InfiniteScrollDirective instead of a bespoke scroll handler: the measured
auto-fill reveals further chunks on tall viewports without any scroll, the
reset key (search term) and item-count changes refresh the latch, and new
nearEndHasMore/nearEndAppending inputs let consumers gate emissions.
Xtream search wires them for both modes — this also fixes the same latent
tall-viewport stall in the global search's 100-item pages — and the
Stalker search page (single capped request until PR 2) sets hasMore=false.
The directive's fill check now refreshes the latch from the measured
state, so an End-key jump straight to the new bottom is a genuine crossing
again.

New coverage: directive stale-latch regression, search-layout auto-fill +
hasMore gating, in-portal window reveal/reset in search-results. Reruns:
portal-shared-ui 80, portal-xtream-feature 357, portal-stalker-feature
green; search e2e 16/16 (fresh Playwright report verified); lint clean.

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

* fix(xtream): re-measure search auto-fill on the rendered window, not the total

Round-2 review finding on #1392 (Greptile P1 + Codex P2, same defect): the
search layout bound the constant result-set total to the infinite-scroll
directive's item count, so once the in-portal window grew 60 -> 120 no
tracked input changed, no further overflow check was scheduled, and
results beyond 120 stayed unreachable on tall viewports.

The layout now takes an explicit nearEndRenderedCount (falling back to
resultsCount for consumers that render everything they report) and feeds
THAT to the directive. Xtream search passes the windowed slice length for
in-portal mode and the loaded-set length for global mode. Regression
specs: layout re-measures when the rendered window grows while the total
stays constant; the component exposes the rendered count following the
window. portal-shared-ui 81, portal-xtream-feature 357, lint clean.

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

* fix(xtream): include filter state in the search reset identity

Round-3 Codex P2 on #1392: the near-end latch and auto-fill budget were
keyed on the search term alone, so a filter-only transition (type filters
or the hidden-categories toggle) replaced the result set without resetting
them — a jump straight back into the threshold could be swallowed. The
search layout now accepts an explicit nearEndResetKey (defaulting to the
term); Xtream search supplies term + type filters + excludeHidden.
Regression specs: layout latch resets on an identity change without a new
term; the component identity changes on filter-only and hidden-toggle
transitions. portal-shared-ui 82, portal-xtream-feature 358, lint clean.

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

* fix(xtream): refuse global-search appends while an edited query debounces

Round-4 Codex P2 on #1392: after the reset-identity change, the layout's
auto-fill can request more results inside the 300ms search debounce. The
append then ran with the freshly edited term but the old result count as
offset, interleaving a page of the new query into the old query's visible
results until the offset-zero search landed.

An append now only continues the LAST EXECUTED search: the append guard
additionally requires the effective term to equal lastGlobalSearchTerm,
so pagination stays suppressed from the first keystroke until the fresh
search replaces the result set. Regression spec covers the mid-debounce
refusal; the two existing append specs state their precondition
explicitly. portal-xtream-feature 359, lint clean.

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

* fix(xtream): per-selection scroll snapshots and progress-based auto-fill stop

Round-5 Codex P2s on #1392:

1. The single saved-scroll slot lost the first tab's position on a
   VOD -> Series -> VOD round trip — the series view's destroy hook
   overwrote it with series coordinates. Snapshots are now kept per
   selection identity (bounded to the 8 most recent), so a detour's save
   can never destroy another list's spot. Store API is unchanged.

2. The fixed 10-load auto-fill budget could strand items on a viewport
   large enough that ten chunks still do not overflow — with no
   scrollbar, no real scroll event can ever fire. The auto-fill now
   terminates on lack of progress instead: loads continue while they
   grow scrollHeight (until genuine overflow hands off to scroll
   events) and stop after three consecutive loads without growth, which
   only a source that reports more but renders nothing can produce.

Regression specs: VOD/Series round trip keeps both snapshots; growth
keeps filling past the old cap and stops at overflow; no-growth stalls
stop at three; reset key clears the stall guard. portal-shared-ui 83,
portal-xtream-data-access 235, catalog-sorting e2e 5/5 re-run, lint
clean. CLAUDE.md wording updated.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:33:56 +02:00
4grayandClaude Fable 5 d5f84fb130 feat(xtream): catch-up badge for live channels with archive (#1341)
* feat(xtream): show a catch-up badge on live channels that have archive

Live channels whose provider declares playable catch-up (tv_archive=1
with a positive tv_archive_duration) now show a small history badge in
the channel sidebar next to the name and on the all-channels grid cards,
with the archive window (days) in the tooltip.

Closes #1128

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

* fix(xtream): expose the catch-up badge status to assistive technology

The mat-icon is aria-hidden and the tooltip is pointer-only, so the
badge status was invisible to keyboard and screen-reader users. Both
badge surfaces now also render the translated status as visually-hidden
text (Codex review, P2).

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

* feat(xtream): show the catch-up badge in favorites and recent lists

Carries tvArchive/tvArchiveDuration through UnifiedFavoriteChannel so
the shared favorites list (portal favorites/recent tabs and global
favorites) renders the same catch-up badge as the live sidebar.
Requested in PR feedback by the issue author.

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

* feat(ui): show the programme-info button in portal sidebars, stacked vertically

Adds the (i) programme-info button to the Xtream and Stalker live
sidebars and reworks the row action column: buttons stack vertically
(favorite on top, info below), so the second button costs no horizontal
space — the column is actually narrower than the previous single-button
row. The info slot is reserved (inert, visibility:hidden) while the row
has no programme, so the star never shifts when EPG data arrives.
Requested by the issue author in PR feedback.

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

* test(ui): move the archive passthrough spec out of the budget-capped file

CI lints the merge with master, where unified-live-tab.component.spec.ts
grew (#1374) to one line under the 1200 max-lines test budget — the
archive passthrough test added here tipped the merged result over. The
test moves to a focused template-less spec (plus a null-normalisation
case), leaving the main spec at master's size.

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

* fix(ui): constrain Material touch targets to the stacked button bounds

mat-icon-button keeps a 48px touch target; stacked 28px buttons
overlapped by 20px and the later sibling (programme info) stole clicks
from the lower third of the favorite star. Verified via
document.elementFromPoint before/after: the star's visual bounds now hit
the star, and clicks left of the column reach the row again instead of
the button's oversized target (Codex review).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:47:44 +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
4grayandClaude Fable 5 1a6af75761 feat(settings): per-section pages with unsaved-changes bar (#1384)
* feat(settings): split settings into per-section pages with an unsaved-changes bar

Replace the single scrolling settings page with routed section pages
(/workspace/settings/:section): the context-panel rail links each section,
only the active section renders, and unknown or capability-gated sections
redirect to General. The shared form lives on the parent component, so
staged edits survive section switches; a floating unsaved-changes bar
(Save/Discard) replaces the always-visible footer Save button. Rail links
navigate with replaceUrl so Back still leaves settings in one step.

Along the way:
- delete the unreachable settings dialog mode and the dead
  AppPortalNavigationActionsService with both of its never-injected DI
  tokens (PORTAL_NAVIGATION_ACTIONS, PLAYLIST_PLAYER_ACTIONS)
- delete the scroll-spy directive and pendingScrollTarget plumbing
- revive the EPG panel's "Open EPG settings" empty-state button as a deep
  link to /workspace/settings/epg; the M3U player now reports
  m3u-needs-setup only when the channel has no programmes and no EPG
  source exists in settings or on the playlist itself
- load TMDB cache stats when the Metadata page opens (the section
  component now only exists while its page is open)
- add SETTINGS.UNSAVED_CHANGES / SETTINGS.DISCARD_CHANGES to all 19 locales

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

* feat(settings): confirm before leaving with unsaved changes

Add settingsUnsavedChangesGuard (canDeactivate on the :section route) with
a three-action dialog: save and leave, leave without saving, keep editing.
The guard only intercepts leaving the settings AREA — section switches
share the one settings form and pass unconditionally, so the dialog can
never nag while moving between pages. A failed save cancels the navigation
instead of silently dropping the edits it promised to keep; leaving
without saving also reverts the live theme preview. Save-and-leave is
disabled while the form is invalid, with a hint explaining why.

New SETTINGS.UNSAVED_DIALOG_* keys in all 19 locales.

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

* fix(settings): stage cover size and EPG view mode; adapt e2e to section pages

Cover size and EPG view mode were the only two controls that persisted
eagerly on click, which made Discard (and leave-without-saving) unable to
revert them: hydrateFromStore() faithfully reloaded the just-persisted
edit. They now stage in the form like every other setting and reach the
store on Save. Review finding by Greptile (P1) and Codex.

E2E suites that walk through settings are updated for one-section-page
rendering (epg, backup-roundtrip, xtream-epg, remote-control) and for the
staged cover size (downloads asserts the dataset after Save); the EPG icon
fallback test saves before leaving settings so the new unsaved-changes
dialog does not block its navigation.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 09:34:03 +02:00
4grayandClaude Fable 5 c95f826739 fix(playback): keep Video.js controls on the live MPEG-TS path (#1385)
Video.js was constructed without the controls option — the component relied
on a [controls] template binding on the original <video> element instead.
player.reset(), which every raw MPEG-TS/live source change goes through,
replaces the tech <video> element; the binding's target is disposed, so live
playback ended up with no native controls and a vjs-controls-disabled control
bar: no visible controls at all.

Enable controls through the Video.js constructor options in legacy mode and
drop the template binding. The Video.js control bar is a player-level child
that survives loadTech_, so it stays across resets — and the quality selector
and aspect-ratio panel buttons it hosts become reachable again.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:43:05 +02:00
4grayandClaude Fable 5 f58460380f fix(stalker): fall back to short EPG when bulk EPG misses the current programme (#1386)
* fix(stalker): fall back to short EPG when bulk EPG misses the current programme

Some portals' bulk get_epg_info returns only future programmes — the one
airing now is absent. The ITV channel-list previews read exclusively from
that bulk map, so every row showed 'No program information available', and
the EPG panel preferred any non-empty bulk list over the short-EPG fallback,
so it showed upcoming shows with no 'on now' entry. Recently Viewed uses
get_short_epg per channel, which is why the same channel worked there.

Panel: merge the short-EPG fallback into the bulk list instead of either/or,
and trigger the fallback whenever the bulk list has no currently airing
programme (not only when it is empty).

Rows: new throttled StalkerEpgPreviewQueue (mirroring Xtream's
EpgQueueService — bounded concurrency, inter-request spacing, 5-minute cache
including empty results, reset on playlist switch) fetches get_short_epg for
rendered channels the settled bulk guide cannot answer.

Docs: stalker-epg.md fallback contract updated accordingly.

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

* fix(stalker): bound the preview-queue burst and keep mapped channels portal-free

Review follow-up (Codex P1/P2 on #1386):

P1 — request volume: each queue sync is now capped at 30 channels (top of
the list first, where a freshly opened category is scrolled to), and the
sidebar scroll handler re-syncs (throttled, 300 ms) to fill the next gaps.
Request count now tracks how far the user scrolls instead of how many rows
are rendered; caching (including empty results) and 200 ms pacing remain.

P2 — manual mappings: a channel whose bulk record comes from a manual XMLTV
mapping never falls back to the portal short EPG. The panel path resolves
the channel's mapping before falling back and bails when an override owns
the channel; the row path excludes overridden channels from both the queue
and its cache. New store query hasItvEpgMappingOverride() exposes override
ownership; merging portal data into a mapped schedule could otherwise
surface the portal's programme — the exact thing the mapping replaces.

Docs updated (stalker-epg.md); regression tests for the cap, the mapped-
channel suppression, and the override query.

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

* fix(stalker): scope the panel EPG fallback by channel and revalidate queued previews

Review follow-up (Codex round 2 on #1386):

The panel's short-EPG fallback is now stored with the channel id it was
fetched for, and activeEpgPrograms merges it only while that channel is
still selected. A channel switch moves the selection synchronously but the
old fallback is replaced only after the new channel's EPG load runs, so the
unscoped merge mixed the previous channel's programmes into the new panel
during slow playback resolution — and left them there when resolution
failed.

The row-preview queue's completion callback now revalidates ownership: a
row claimed while the fetch was in flight — by a manual mapping override or
by bulk data — is never overwritten by the late portal response.

Both races covered by a new focused spec (verified to fail on the pre-fix
component); stalker-epg.md updated.

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

* fix(stalker): supersede the preview backlog on view exit and own empty mappings

Review follow-up (Codex round 3 on #1386):

The preview queue is now superseded when the rendered channel list empties
(a legacy-paged category switch clears it before the new channels arrive)
and when the view leaves ITV for radio — an abandoned view's backlog no
longer keeps issuing get_short_epg requests for rows that are gone.

Mapping ownership is now tracked separately from the mapped guide's
programs: a saved mapping whose XMLTV channel currently has no entries
still owns its channel, so hasItvEpgMappingOverride() keeps the portal
short-EPG fallback out — consistent with a mapping's purpose of replacing
portal data.

Both covered by regression tests (verified to fail pre-fix; the ITV-exit
test re-arms the backlog after init because the playlist effect's first run
resets the queue); stalker-epg.md updated.

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

* docs(stalker): reconcile every EPG contract reference with the row fallback

Review follow-up (Codex round 4 on #1386): the overview, get_short_epg API
notes, and data-mapping sections of stalker-epg.md still stated that rows
never issue per-row requests, and the stalker-portal skill instructed that
only the active channel may fall back — contradicting the contract this PR
establishes. All references now describe the bulk-first row previews with
the throttled short-EPG fallback queue. skills:validate passes (the skill
stays within its 500-word budget).

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

* fix(stalker): publish empty-mapping ownership reactively

Review follow-up (Codex round 5 on #1386): when the row-preview fetch
finishes before the mapping lookup, a portal programme is already rendered.
An empty mapped guide then recorded ownership only in a plain Set — no
state was patched, the preview effect never reran, and the stale portal row
survived. applyMappedItvEpg now re-patches bulkItvEpgByChannel (identical
content, new reference — deliberately) whenever it establishes new
ownership, even without programs, so the rerun sync removes the fallback
row. Store regression test extended (fails pre-fix); stalker-epg.md updated.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 21:42:42 +02:00
4grayandClaude Fable 5 92be39ef66 fix(xtream): drop URL-only season overviews and fall back to TMDB (#1382)
Xtream panels routinely fill get_series_info seasons[].overview with a
bare cover-image URL, which rendered verbatim under the season tabs.
URL-only overviews are now treated as absent (sanitizeProviderOverview),
and the lazy season enrichment stores the TMDB season overview on the
selection (tmdb_season_overviews) as the fallback description - same
cached /tv/{id}/season/{n} payload, so no extra requests. Provider text
keeps priority when it is real prose.

The enrichment write is also convergent now: the serial detail re-fires
season enrichment after every selection write, and the previous
unconditional rewrite scheduled the next cache-served run indefinitely.
A repeat run that changes nothing no longer writes.

buildSeasonDescriptions is extracted from SerialDetailsComponent, which
would otherwise cross the 400-line max-lines limit.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:12:59 +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
dependabot[bot] fd29362d44 chore(deps-dev): bump electron from 41.7.2 to 41.10.3 (#1377)
Bumps [electron](https://github.com/electron/electron) from 41.7.2 to 41.10.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v41.7.2...v41.10.3)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 41.10.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 08:47:26 +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