mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
77458b393194258e37340ff5e111eaa51affcdf3
3014
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e86e988e72 |
feat(ui): turn the phone context panel into an off-canvas drawer (#1332)
* feat(ui): turn the phone context panel into an off-canvas drawer On ≤640px viewports the workspace context panel (categories, filters, settings sections, collection filters) no longer stacks above the route content capped at 30vh — it is a hidden-by-default drawer that slides in from the left over a backdrop, opened via a new header toggle (phone-only, CSS-gated) and closed by selection, backdrop tap, Escape, or any navigation. State lives in the new WorkspaceShellContextDrawerService provided by the shell component; panels close it explicitly after selections that do not navigate (Stalker ITV/radio categories, settings sections, sources filters, collection filters), since NavigationEnd alone cannot cover those. Desktop behavior is untouched, including the ResizableDirective inline width. Closes the drawer follow-up deferred from #1100 / PR #1326. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): make the phone context drawer modal for keyboard users Addresses Greptile P1 and Codex P2 review feedback on #1332: - CdkTrapFocus on the sidebar captures focus into the drawer on open and contains it while the drawer is modal; the shell restores focus to the header toggle on close, since the closed drawer is visibility: hidden and focus left inside it would silently drop to <body>. - The drawer service closes the drawer when the viewport leaves the phone breakpoint (matchMedia), so the trap can never hold the in-flow desktop sidebar after a resize. - The toggle's tooltip and aria-label are now variant-aware — categories on portal routes, filters on sources/collection routes, settings sections on the settings route — instead of a fixed 'Categories & filters' that misdescribed two of the three; the two generic i18n keys are replaced by six variant keys across all 19 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): remove background content from the a11y tree while the drawer is open Round-2 review feedback on #1332 (Greptile P1, Codex P2): - The rail, header, route content and playback footer are marked inert while the phone drawer is open — CdkTrapFocus constrains Tab focus, but a screen reader's virtual cursor could still reach and activate the visually obscured controls behind the backdrop. - The drawer panel itself is the trap's initial focus target (tabindex=-1 + cdkFocusInitial), so focus capture still works when a category list is loading, empty, or failed and renders no focusable rows. - Focus restore on close is deferred one tick: the toggle lives in the inert header, and focus() on a still-inert element is silently ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): gate global shortcuts and Escape behind the open phone drawer Round-3 review feedback on #1332 (Codex P2s): - The shell consumes Escape while the drawer is open: downstream Escape consumers (the portal detail shell's inline player close, the shared controls shortcuts) check defaultPrevented, so one keypress no longer closes both the drawer and the obscured playback surface. - inert does not silence document-level keydown listeners, so players opt out themselves while inside an inert region: ControlsShortcuts gains an optional hostElement handler and ignores every shortcut (including Escape) when that host has an inert ancestor, and the radio audio player applies the same check to its volume/mute keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): close control, Cmd+F gate, and Embedded MPV inert guard for the drawer Round-4 review feedback on #1332 (Greptile P1, Codex P2s): - The drawer carries its own phone-only close button: touch screen-reader users have no hardware Escape and cannot reach the inert header toggle or the aria-hidden backdrop, so the trapped surface must offer dismissal itself — even when a category list is loading or empty and renders no actionable entries. - Ctrl/Cmd+F no longer opens global search while the drawer is modal; the shortcut would have navigated and focused an input inside the inert header. - EmbeddedMpvShortcuts (native-view legacy dock) gains the same hostElement/inert-ancestor guard as the shared controls shortcuts, so the obscured player cannot react to Space/arrows/M/Escape behind the drawer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-5 drawer feedback + update phone-layout e2e for the drawer Merges master (#1333 landed mobile-layout.e2e.ts pinning the #1326 stacked-panel behavior this PR replaces) and updates that spec to pin the drawer contract instead: panel hidden by default with full-width content, header toggle opens it over a backdrop, category selection and backdrop tap close it. Verified locally on Chromium, Firefox and WebKit (12/12). The spec's getByTestId calls needed plain [data-test-id=...] locators — the web-e2e Playwright config never mapped testIdAttribute. Also addresses Codex round-5 P2s: - Focus restore now reports whether the toggle received focus; when a drawer selection navigated to a route without a context panel (toggle gone), focus falls back to the route content instead of dropping to <body>. - The Xtream and Stalker live layouts' Ctrl/Cmd+B sidebar shortcut opts out while their host sits inside an inert region, matching the other document-level listeners. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): suppress command palette and shortcuts dialog behind the open drawer Greptile round-6 finding on #1332: the document-level Ctrl/Cmd+K handler in WorkspaceShellFacade and the '?' help-key handler in WorkspaceKeyboardShortcutsService still opened their dialogs while the phone context drawer was modal, stacking a second focus-trapped surface on top of it. Both now check the drawer service (injected optionally, same shell-component providers) and stay quiet while it is open, like the Ctrl/Cmd+F global-search gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-7 drawer feedback — Cmd+R gate and native MPV surface hiding Addresses the two Codex round-7 P2s on #1332: - WorkspaceShellContextDrawerService moves to @iptvnator/workspace/shell/util and becomes root-provided, so AppComponent's document-level Ctrl/Cmd+R global-recent shortcut can observe the modal drawer without pulling the lazy shell chunk into the eager bundle. Cmd+R is now suppressed while the drawer is open, like Cmd+F/Cmd+K/'?'. - The shell registers the open drawer with a new EmbeddedMpvOverlayVisibilityService.acquireExternalModalSurface() API: the native-view video surface is composited outside DOM stacking and would paint straight over the drawer regardless of z-index. The service treats registered external modal surfaces exactly like open Material dialogs. - The service's recompute no longer reads overlayActive back before setting it: signals already skip notification on equal values, and that hidden read registered overlayActive as a dependency of any reactive context calling into the service — the shell's acquire/release effect looped forever on exactly that (caught by a live browser probe; the unit suite mocked the service). The effect also wraps the acquire in untracked() for caller-side hygiene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): expose the phone drawer as a named modal dialog Round-8 review feedback on #1332 (Codex P2s): - While open, the drawer carries role=dialog, aria-modal=true, and a variant-appropriate accessible name (categories / filters / settings sections) — assistive technology now hears that a named modal surface opened instead of an unnamed complementary landmark. Closed (and the always-visible desktop sidebar) stays a plain landmark. - The UI-guidelines drawer section no longer claims the drawer service is component-provided; it is root-provided from workspace/shell/util since the round-7 move, and the stale claim could have led a future change to re-scope it and silently break the AppComponent shortcut gate and the Embedded MPV overlay observer. Matching code comments updated everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-9 — gate M3U player keys behind the drawer, raise drawer stacking Greptile round-9 P1 + Codex round-9 P2 on #1332: - The M3U video player's document-level digit-key channel switching and Ctrl/Cmd+B sidebar toggle now apply the same inert-ancestor guard as every other routed-content key listener. A codebase sweep confirms this closes the class: every document-level key listener on routed content is now either gated by the shell (Escape, Cmd+F/K/R, '?') or opts out via closest('[inert]'); the guidelines now require the guard for any new listener. - The drawer moves from z-index 99/98 to 951/950: above the settings action bar (100) and the root EPG/update panels (900/901), which inert removes from interaction but not from paint order — below the CDK overlay container (1000), since dialogs opened from inside the drawer (Manage categories) must stack on top of it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
297e9fbef8 |
fix(stalker): send cmd in the reference MAG wire format (#1334)
* fix(stalker): send cmd in the reference MAG wire format
A real MAG sends cmd unencoded and the portal decodes its query exactly
once, so a cmd that already contains percent sequences (%3A tokens,
pre-encoded path segments) must pass through untouched. The previous
encodeURIComponent transport (
|
||
|
|
04b2f9b82e |
test(e2e): pin the phone-layout invariants from #1326 (#1333)
PR #1326 fixed the workspace on phone-sized screens (issue #1100) with SCSS-only changes and no automated coverage. This adds a mobile-layout smoke spec asserting the invariants that regressed before: no horizontal overflow on dashboard/Xtream/settings, rail links inside the 52px top bar, the context panel stacking above full-width content on portal routes, the settings section list ending above the Back footer, and the 640x360 landscape live route keeping the channel sidebar >= 72px with the player container inside the viewport. The Xtream tests import the portal at desktop width and then shrink the viewport, so the persisted inline rail widths from ResizableDirective — the exact #1100 regression scenario — are present when the phone rules must win. Run: pnpm nx run web-e2e:e2e-ci--src/mobile-layout.e2e.ts Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aba89d64cf |
fix(downloads): resume interrupted Xtream VOD transfers (#1329)
* fix(downloads): resume interrupted Xtream VOD transfers * fix(downloads): validate partials before resuming * fix(downloads): propagate headers to episode transfers |
||
|
|
d44045de31 |
test(playlist): fix flaky refresh-preparation worker-event spec (#1331)
The spec raced a fixed 160ms sleep against the service's internal rAF + 120ms paint delay that runs before deleteXtreamPlaylistContent is called. Under parallel jest load the sleep could win, asserting before the mocked worker event was ever delivered. Await a deferred resolved by the mock right after it fires onEvent instead, so the assertion is causally ordered after the signal update. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
94efd7d379 |
fix(workspace): restore playlist info entry in header playlist dropdown (#1328)
The context-actions section of the playlist switcher lost its "Playlist
info" button when playlist actions moved into the per-row menu
(
|
||
|
|
8f861a3a1b |
fix(ui): make the workspace usable on phone-sized screens (#1326)
* fix(ui): make the workspace usable on phone-sized screens
The shell was half-adapted below 640px: the rail flipped to a horizontal
bar but the link lists inside it kept stacking downwards, so the navigation
was drawn outside the bar and over the header (#1100).
Three resizable rails — the shell context panel, the live-layout channel
sidebar and the M3U channel drawer — kept their persisted desktop width,
which left the content around 50px on a 375px screen. They now span the
full width and stack above the content. The inline width written by
ResizableDirective is why these rules need `!important`.
Found while walking the rest of the UI at 375px and 768px:
- The detail hero kept poster and details side by side, squeezing the
action row below its own labels until "Play" was clipped to its icon.
- The settings section list did not scroll and painted over the footer,
which also affected short desktop windows.
- Hiding the M3U channel list on a phone was one-way: the restore handle
was hidden and only Cmd/Ctrl+B could bring it back.
- The live header drew the channel count and the paginator on top of each
other up to tablet width, because the paginator does not shrink and the
meta collapsed to zero width and overflowed its box.
- The search scope checkbox was pushed off the right edge.
Live TV states a floor for the player instead of a ceiling for the lists,
so the video keeps a usable share of the screen under the categories panel
and the channel list.
Closes #1100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — keep the palette reachable and the video visible
Two findings from the Codex review on #1326.
Hiding the command-palette trigger on phones removed the only pointer-driven
way to open it: the rail renders route links plus Settings and emits nothing,
so `commandPaletteRequested` had exactly one source. The button stays and its
keyboard-shortcut label is swapped for an icon instead. Doing that exposed a
latent flex trap in the same row — an <input> keeps an intrinsic min-width
from its `size`, and `min-width: auto` honours it, so the field refused to
shrink and pushed the trigger out onto the buttons beside it.
The M3U drawer released the shared player floor, which on a short landscape
phone (600-640px wide) left the content container at half the shell body.
The inline guide inside it is `flex: 0 0 <basis>` and took its full 180px out
of a container that no longer had it, so the video could reach zero height.
The floor is restored and now yields on short viewports, the video states its
own minimum, and the guide is what gives way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the channel list keep its height on a landscape phone
Follow-up to the review: the player floor added in the previous commit was
measured against the viewport, not against what the shell had left. On a
640x360 landscape phone the stacked categories panel already takes 30vh, so
claiming another 50vh here drove the channel sidebar to zero height while it
was still marked expanded — no way to pick another channel — and pushed the
layout past the viewport.
The floor now applies only where the screen can afford it (`min-height:
600px`), the sidebar states a floor of its own so it cannot be squeezed out,
and the collapsed rule clears that floor so hiding the list still works.
Below that height the two panes simply share what is left.
Portrait is unchanged: categories 244px, channel list 220px, player 240px on
a 375x812 screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — settings nav on landscape, poster dead space
Two more findings from the Codex review.
The stacked settings context panel capped itself at 30vh, which on a 360px
screen is 108px — less than the panel's own title and footer, so the seven
section rows collapsed to nothing behind an overlapping footer. On short
screens the caption gives way (the rail already labels the page), the footer
sheds its tall-screen padding, and the settings variant gets a slightly
larger cap: unlike the live routes there is no player below competing for
height, only a scrollable form.
The poster kept a 330px minimum from the skeleton fallback at the bottom of
the file — sized for the 220px desktop poster — while the stacked phone hero
renders it 140px wide with a ~210px aspect-ratio height. Every loaded detail
page carried ~120px of empty space between the poster and the title. The
override sits after that rule because it wins on source order, not
specificity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the playlist switcher yield to the search field on narrow phones
Codex review of
|
||
|
|
760099358b |
feat(downloads): redesign download manager (#1313)
* docs(downloads): specify manager MVP redesign * docs(downloads): plan manager MVP implementation * docs(downloads): tighten manager validation plan * fix(downloads): keep renderer download state global * fix(downloads): make active count accessible * feat(downloads): derive queue and library view model * test(downloads): close view model coverage gaps * fix(downloads): stabilize malformed view model data * refactor(downloads): isolate library navigation * fix(downloads): report library navigation failures * feat(downloads): add ready-to-watch library * feat(downloads): add active download queue * feat(downloads): finish manager MVP * docs(downloads): clarify detail-first offline behavior * docs(downloads): plan detail navigation follow-up * fix(downloads): open completed movies in details * test(downloads): cover pending series navigation * fix(downloads): honor the global cover size * fix(downloads): prefer local playback in shared details * fix(downloads): preserve external launch priority * fix(downloads): prefer local playback in Xtream details * test(downloads): cover offline detail journey * docs(downloads): document offline detail behavior * docs(downloads): format detail navigation plan * fix(downloads): open Stalker items in provider details * docs(downloads): clarify Stalker navigation fallback * fix(xtream): isolate reused detail identities * fix(xtream): ignore stale VOD positions * fix(downloads): keep offline Xtream playback available * docs(downloads): clarify provider playback availability * docs(downloads): design missing-file recovery * docs(downloads): plan missing-file recovery * feat(downloads): derive completed file availability * feat(downloads): recover missing completed files * feat(downloads): refresh missing local files * feat(downloads): separate missing files from ready media * feat(downloads): surface missing files for recovery * refactor(downloads): simplify ready cards * test(downloads): cover missing-file and series journeys * feat(downloads): finish missing-file recovery * docs(downloads): design offline detail views * docs(downloads): plan offline detail views * feat(downloads): persist offline metadata snapshots * fix(downloads): complete metadata snapshot bridge contract * feat(downloads): manage offline metadata snapshots * fix(downloads): harden metadata snapshot updates * fix(downloads): restrict snapshot artwork * fix(downloads): guard restart artwork URL * fix(downloads): refine artwork URL checks * feat(downloads): expose offline metadata updates * fix(downloads): keep metadata service change focused * fix(downloads): preserve metadata error conventions * feat(downloads): derive offline detail content * fix(downloads): preserve unknown episode coordinates * feat(downloads): add focused offline detail routes * fix(downloads): ignore fragments in shell route state * fix(downloads): normalize fragments before queries * feat(downloads): open ready cards in offline details * fix(downloads): use native disabled card styles * feat(downloads): enrich offline detail metadata * fix(downloads): harden offline metadata resolution * fix(downloads): preserve stalker provider titles * fix(downloads): distinguish stalker metadata seeds * fix(downloads): stabilize offline metadata refresh * fix(downloads): throttle sparse metadata refreshes * fix(downloads): type metadata language settings * feat(downloads): render offline movie and series details * fix(downloads): harden offline detail interactions * fix(downloads): close offline detail edge cases * feat(downloads): hand off to provider-only details * fix(downloads): preserve stalker provider handoff * feat(downloads): capture metadata at download time * fix(downloads): preserve snapshot source semantics * fix(downloads): preserve episode snapshot identity * docs(downloads): document offline details flow * docs(downloads): clarify stalker provider fallback * test(downloads): cover offline detail journeys * test(downloads): stabilize offline detail selectors * style(downloads): format changed files * docs(downloads): clean design spec formatting * fix(downloads): preserve offline library ownership * test(downloads): fix Windows workspace navigation * test(database): preserve Electron tsconfig resolution * perf(downloads): avoid blocking file availability probes |
||
|
|
46c58f4f57 |
fix(stalker): keep session headers on same-host redirects (#1322)
* fix(stalker): keep session headers on same-host redirects Since 0.22 requestWithValidatedRedirects stripped Cookie/Authorization whenever a redirect changed the *origin*, so a portal answering with an http->https upgrade or a port move lost the MAC cookie and Bearer token mid-session. Real Stalker/Ministra servers then reply with a plain-text "Authorization failed." body: categories fail to load, create_link never resolves, and no player receives a stream URL (#1158 regression window). Scope credential stripping to the host instead: same-host scheme/port redirects keep headers, basic auth, params, and request bodies; a redirect to a different host still drops all of them, preserving the original hardening intent (no credential leaks to third-party hosts). Also adds the Stalker API compatibility roadmap produced by the 2026-08-01 protocol audit (.plans/, force-added like earlier plans). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(electron): strip credentials on same-host https-to-http downgrades Review follow-up (Greptile P1 + Codex on #1322): the host-only check kept Authorization/Cookie/basic auth/params/body when an https request was redirected to http on the same host, replaying a TLS-obtained session in cleartext. Treat that downgrade like a host change: strip credentials and refuse to replay request bodies. Scheme upgrades and port moves on the same host keep headers — the actual #1158 scenarios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0c59aace71 |
fix(playback): structure MPEG-TS diagnostics (#1327)
* docs(playback): design structured mpegts diagnostics * docs(playback): plan structured mpegts diagnostics * fix(playback): structure mpegts error evidence * fix(playback): structure HTML5 mpegts errors * fix(playback): share mpegts evidence across players * fix(playback): render structured mpegts evidence * docs(playback): document structured mpegts diagnostics * chore(playback): keep diagnostics lint clean * docs(playback): complete mpegts diagnostics plan |
||
|
|
9bb1811984 |
fix(playback): escape commas in mpv header fields on the TS paths (#1323)
* fix(playback): escape commas in mpv header fields on the TS paths PR #1321 fixed the comma truncation of --http-header-fields inside the native embedded-mpv addon, but the same OPT_STRINGLIST parsing bites three TypeScript call sites that join header fields with ',': - external MPV CLI launch (--http-header-fields=...) - external MPV instance reuse (set_property http-header-fields) - embedded MPV frame-copy loadfile options (opt.http-header-fields); the helper's %len% quoting protects only the option-list level, mpv still stringlist-parses the value afterwards The Stalker MAG user agent contains "(KHTML, like Gecko)", so strict portals received a truncated X-User-Agent and rejected live streams with HTTP 400 (#910). Escape backslashes and commas per field with the same scheme as the native fix, via a shared util. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(playback): cover the reused-instance IPC header escaping Review follow-up (Greptile on #1323): the regression coverage only exercised the CLI spawn path. Capture the JSON IPC traffic of a second, reused mpv launch and assert the escaped MAG user agent survives the set_property http-header-fields transport, so later serialization changes cannot silently reintroduce truncated headers. net is mocked with a passthrough default because the VLC specs bind a real ephemeral port via createServer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
814161274b | fix(playback): escape commas in mpv http header fields (#1321) | ||
|
|
b14ce2452b | fix(packaging): add verified source mirror fallback (#1325) | ||
|
|
9f4e11d6de |
fix(playback): structure Shaka diagnostics (#1318)
* docs(playback): design structured Shaka diagnostics * fix(playback): structure Shaka diagnostics * docs(playback): document Shaka evidence boundary * docs(playback): fix Shaka validation commands * fix(playback): preserve Shaka fallback evidence * fix(playback): preserve Shaka text error evidence |
||
|
|
9a50e7385b | fix(playback): structure Video.js diagnostics (#1317) | ||
|
|
46c7713841 |
fix(ui): preserve EPG in narrow channel rows (#1312)
Preserve current-program context and enabled actions in narrow channel rows while aligning loaded rows, skeletons, and virtual-scroll geometry across M3U, Xtream, Stalker, Favorites, and Recent views. |
||
|
|
2ac0de752f |
fix(skills): align repository guidance with implementation (#1315)
* docs(skills): design implementation synchronization * docs(skills): plan implementation synchronization * fix(release): filter internal notes from public body * docs(release): synchronize release workflow guidance * fix(stalker): normalize catalog series flags * fix(stalker): preserve progress with scoped episode IDs * fix(playback): expose strict position persistence * docs(stalker): record series position compatibility * test(skills): validate repository skill contracts * fix(database): keep SQL trace values private * docs(skills): refresh Nx and SQLite ownership * docs(skills): align provider and UI guidance * docs(skills): tighten validated guidance * docs(release): require exact release pushes * style(electron): remove trailing blank line * fix(ci): classify repository skills coverage |
||
|
|
99d167993d |
fix(playback): structure HLS diagnostics (#1316)
* docs(playback): design structured HLS diagnostics * docs(playback): plan structured HLS diagnostics * fix(playback): structure HLS diagnostics * docs(playback): document structured HLS evidence * fix(playback): keep HLS startup logs private |
||
|
|
bf13849d69 |
fix(playback): avoid false codec diagnostics (#1314)
* docs(playback): design accurate native diagnostics * docs(playback): plan accurate native diagnostics * fix(playback): classify native source errors from evidence * fix(playback): preserve Video.js HTTP error context * fix(playback): show explicit HTTP playback errors * docs(playback): document native error evidence |
||
|
|
32ba209b63 |
fix(portals): restore fresh-import pins atomically (#1311)
* fix(portals): restore fresh-import pins atomically * fix(portals): preserve Xtream restore retry state * fix(portals): serialize Xtream restore revisions |
||
|
|
78df3e7dbb |
fix(portals): match Greek titles whichever sigma the provider typed (#1310)
Greek Σ has two lowercase forms — medial σ and word-final ς — and neither the candidate query nor the confirmation treated them as one letter. The GLOB scan built each character's class from a one-way reach that only arrived at ς when it started from ς, so a request for "ΑΣ" never admitted a stored "Ας". Classes are now built from a fold group — every character sharing an uppercase form — derived by scanning the cased ranges at module load the way ACCENTED_BY_BASE already is. It generalises past sigma on its own: dotless ı folds with i, long ſ with s, historic Cyrillic letterforms with В Д О С Т Ъ Ѣ. Only the 24 groups of 767 that a per-character fold would miss are kept. Admitting the row was only half of it. normalizeTitleKeys then compared "ασ" against "ας" and discarded it, because toLowerCase picks the sigma form by position. Both SQL tiers already folded them together — SQLite's trigram tokenizer does full Unicode folding natively, unlike LOWER() — so the JS confirmation was the only tier that did not, making this a pre-existing gap on the FTS path as well. Normalization now rewrites ς to σ after lowercasing, which is what Unicode case folding does. Guards unchanged: a case mapping that changes length (ß → SS, İ) or a GLOB metacharacter still returns null rather than a partial pattern. |
||
|
|
063662028a |
feat(portals): find the same movie in your other playlists (#1286)
* feat(portals): find the same movie in your other playlists A movie that exists in several imported Xtream playlists now shows a "Sources N" chip on its detail page and in the player. Switching playlist mid-film keeps the timecode, a preferred source can be pinned per movie, and a failed stream offers the alternatives instead of a dead end. The governing rule is that a guess is never presented as a fact. Every metadata value carries where it came from — `api` (the provider said so), `parsed` (inferred from the title) or `probe` (we contacted the stream). Facts render as plain tags, guesses are prefixed `~` in a warning colour, and an unknown value renders no tag at all plus a "check" affordance. Ranking and failover read through `factualOnly()`, so a filename claiming 4K is structurally unable to outrank a source that was actually reached. A probe that could not complete reports "unknown", never "unavailable". Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only. Stalker never reaches the `content` table and M3U is a JSON blob whose search forces live content; both are additive later, since the candidate type already carries all three portal kinds. In the PWA every entry point is gated off and the chip renders nothing. Auto-failover is opt-in and off by default. Each source is tried at most once per session, so it terminates structurally, and the switch is never silent — the toast names the new playlist, offers an undo, and warns that the dub may differ only when both sides state an audio track as fact. Notable details: - Playlist names are routinely the pasted URL, credentials included. They are never rendered raw; a short host-only label is derived instead. - Quality is derived from pixel width, not height: a 2.39:1 1080p master is 1920x800, and bucketing that by height would publish "720p" as a fact. - Switching is a single `inlinePlayback.set()` so the player and engine survive and re-seek; the carried position is read before the 15s persistence throttle so it does not rewind. - Sources from one playlist collapse into a group, since the same film often appears there several times under different stream ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): stop stale source resolutions from committing Addresses three defects Greptile found in the multi-source review. **Concurrent switches committed out of order.** Selecting a second source before the first resolution returned let the slower request overwrite the newer selection and repoint Undo at itself. `switchTo` now takes a sequence number and drops its result if a newer switch already committed. **Stale switches crossed movie sessions.** Navigating to another film while a resolution was in flight let the continuation activate the old film's source inside the new controller — and restart it from that session's zero resume position. The controller is now snapshotted per operation and the movie session is revalidated after every await. `check()` had the same hazard across its two awaits and is guarded the same way. **Short titles skipped discovery entirely.** The trigram tokenizer cannot index tokens under three characters, so "Up", "It" or "Us" produced an empty MATCH expression and the query was discarded before SQLite was consulted — the chip could never appear for those films. Discovery now falls back to a bounded scan when FTS structurally cannot serve the title; the existing two-tier normalized confirmation still rejects loose hits like "Upgrade". Each fix carries a regression test; all three were mutation-checked by removing the guard and confirming exactly those tests fail. The previous test asserting that short titles return nothing encoded the bug and has been replaced. The host spec passed 400 lines, so its fixtures moved to a shared module and the race suite into its own file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(portals): make the pin decide playback and keep failover going Second round of Greptile review findings. **A pin had no behavioural effect.** Loading a stored pin only decorated the row: Play still started the route's playlist and failover ranking ignored `isPinned`, so "make this the main source" survived a restart as an icon and nothing else. The primary action now starts from the pinned source when one is set, and the pin outranks everything else in failover ranking. **Failover stopped at the first unresolvable candidate.** An expired account or a failing `get_vod_info` on the top-ranked source ended the attempt, and since production calls `failover()` only once — on the original playback failure — a healthy lower-ranked source was never reached. It now continues through untried candidates. `switchTo` reports why it stopped so the loop can tell "could not resolve, try the next one" from "something newer owns the screen"; without that distinction a superseded switch would have spun forever, because only the former marks the candidate tried. **Identity ignored enrichment.** The key was `playlistId:contentId:title`, so when `get_vod_info` added a TMDB id and release year to an unchanged title the host saw no change, never reloaded, and kept yearless discovery and title-only pin keys — a `tmdb:`-keyed pin could never be found. The key now covers every field that affects matching. **A server refusing HEAD read as unavailable.** Some stream hosts answer 405 or 501 to HEAD yet serve the media over GET. The probe now retries once with the ranged GET the main process already supported, instead of caching a working source as failed and penalising it during failover. Greptile also flagged a missing token check after the resolve await in `switchTo`; that guard landed in |
||
|
|
b1f77c678e |
test(performance): prevent renderer heartbeat omission (#1308)
* test(performance): normalize sub-ms IPC clock skew * test(performance): prevent heartbeat coordinated omission |
||
|
|
deae0a2a4d |
fix(xtream): keep sparse VOD details playable (#1303)
* fix(xtream): keep sparse VOD details playable * fix(xtream): scope VOD fallback to active playlist * fix(xtream): render sparse VOD before recovery * fix(xtream): recover Similar VOD provider categories |
||
|
|
9b7776a901 |
chore(lint): hold tests to their own max-lines ceiling (#1306)
* chore(lint): hold tests to their own max-lines ceiling The flat 400-line cap treated a spec like a component. A spec is a flat list of independent cases, so hitting the cap there produces arbitrary `-2.spec.ts` splits and hides coverage instead of surfacing design debt — 65 of the 138 files over the limit were tests. Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no longer count, so a docblock can't be the reason a file must be split. Both limits now live in tools/eslint/max-lines-config.mjs, imported by eslint.config.mjs and the baseline generator alike. The generator decides who belongs on the list by running ESLint's own max-lines rule instead of counting lines itself — a private reimplementation would disagree with the rule the moment either side changed (a `//` inside a template literal is enough) and yield a baseline that turns CI red while looking correct. The baseline drops 126 -> 68 entries with nothing added, and six now-dead `eslint-disable max-lines` directives are removed. A new eslint-tools test asserts the committed baseline still matches what the generator produces, so a stale entry or a forgotten regeneration fails CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(lint): classify eslint-tools in the coverage policy A project with a `test` target must be assigned a coverage tier, so adding eslint-tools broke `coverage:policy:check` before the unit suite even ran. Tier B alongside packaging and release-tools: these are Node tests over lint tooling, and a coverage percentage across a generated list would not mean anything. CI runs Tier B/C through its own `--run-non-tier-a` step, so the baseline-consistency test executes there rather than being skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
055170d188 |
test(performance): harden Xtream startup retry (#1307)
* test(performance): harden Xtream startup retry * test(performance): preserve Xtream teardown failures * test(performance): retry Xtream profile cleanup |
||
|
|
3c342bc555 | test(performance): preserve delayed worker samples (#1305) | ||
|
|
faec40ff7b |
test(performance): stabilize Xtream benchmark startup (#1304)
* test(performance): stabilize Xtream benchmark startup * test(performance): bound cancellation clock skew |
||
|
|
99a85da6b0 |
feat(packaging): register IPTVnator as the .m3u/.m3u8 handler (#1301)
* feat(packaging): register IPTVnator as the .m3u/.m3u8 handler Every runtime path for an OS-supplied playlist existed, but no packaging metadata claimed the file types — so the OS never offered IPTVnator as a handler and `open-file` could not fire from Finder. `fileAssociations` declares one entry per extension. Electron Builder derives all three platform registrations from it: macOS `CFBundleDocumentTypes` (the prerequisite for `open-file`), the NSIS registry entries, and, on Linux, the desktop entry's `MimeType` plus `/usr/share/mime/packages/iptvnator.xml` for deb/rpm/pacman. Neither platform needs a dedicated icon — both fall back to the app icon. Declaring `MimeType` under `linux.desktop.entry` would not have worked: Electron Builder assigns the association-derived value *after* spreading that object, so an explicit key there is silently overwritten. The per-association `mimeType` fields produce the same entry through the supported path. Registering the types also exposes a gap in the delivery side. The generated Linux `Exec` ends in `%U`, so file managers hand over a percent-encoded `file://` URI rather than a path, which `createPlaylistOpenRequest` would have resolved into a bogus relative path. It now decodes a `file://` candidate before the extension check. Suppressing the `%U` instead would mean putting an exec code in `linux.executableArgs`, which also passes it to the app as a real argument. Verified on macOS against a signed packaged bundle: Launch Services lists the app as a `public.m3u-playlist` handler, and an LS-initiated open imports the playlist both on a cold launch and against the already-running process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(playlist): open every playlist of a multi-file selection `%U` is the plural exec code, so selecting several playlists in a Linux file manager is one launch carrying one argument per file. Both argv paths called `extractPlaylistOpenRequestFromArgv`, which returned at the first match, so everything after the first playlist was silently discarded — a gap this PR itself opened by making the desktop entry reachable in the first place. The extractor is now plural and returns every match in argument order, and the queue gained `enqueueAll` so a selection is pushed under a single flush: a delivery that fails partway leaves the untouched remainder queued in arrival order rather than interleaved. Covered by unit tests over a mixed argv (percent-encoded `file://` URI, a non-playlist argument, a second URI) and by a new Electron E2E that launches with two playlist arguments and asserts both are imported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bc4e3a2e2c |
test(performance): bound worker sampling finalization (#1302)
* test(performance): bound worker sampling finalization * test(performance): reject timed-out worker captures * test(performance): settle every worker sample |
||
|
|
f80eb4d1b9 |
fix(playlist): open playlists handed over by the OS (#1299)
Opening an .m3u/.m3u8 file from the command line or a file association did nothing. The renderer parsed `process.argv` and sent an `OPEN_FILE` IPC event that had no `ipcMain` handler and no preload channel, so `sendIpcEvent` logged it as an unknown type and dropped it. The path now belongs to the main process, which is where the OS actually delivers it: - argv is parsed on first launch (skipping the executable and Chromium switches) and normalized to an absolute path; - macOS gets an `open-file` listener registered before `whenReady`, since Launch Services never puts the path in argv; - the single-instance guard forwards a second launch's argv and working directory instead of discarding them, so opening a playlist against a running app works too. Requests are queued in the main process until the renderer subscribes to the `OPEN_FILE` push and drains the queue, which closes the startup race. The import itself reuses the existing file path, so persistence, playlist-scoped EPG and the navigation to the new playlist behave exactly like a dialog import; a failed open now surfaces a snackbar instead of silence. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
80af9257a0 |
refactor(portals): share external-button and position-writer logic (#1298)
The Xtream and Stalker VOD detail views each carried a private copy of two behaviours: deriving the Play/Stop button state from the active external (MPV/VLC) session, and throttled persistence of the inline player position. A Play button or a resume point that behaves differently per portal is the kind of divergence users notice, so both now read from one implementation. Extracts `createExternalPlaybackButtonState` and `createInlinePlaybackPositionWriter` into portal/shared/util, and lifts the Stalker VOD download errand into its own helper. Behaviour is unchanged; the shared helpers are deliberately identical to the copies they replace. This also brings both hosts back under the 400-line ESLint limit, neither of which was baselined: vod-details.component.ts 389 -> 333 stalker-catalog-detail.component 394 -> 325 vod-details-playback.service.ts 345 -> 275 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
72f8cebd2e |
fix(e2e): reap data directories abandoned by earlier runs (#1296)
`removeDataDir` tolerates a locked directory rather than failing the run, but then abandons it, and nothing collects it on our behalf: Windows never clears %TEMP% on process exit, and the Unix equivalents only run on a schedule. Every teardown that lost that race leaked a database and user-data tree on developer machines and long-lived runners, invisibly, while CI stayed green. Sweeps leftover `iptvnator-electron-e2e-*` directories once per run, before the first one is created. Ownership is settled by pid rather than age: each run records its pid and the sweep asks the OS via `process.kill(pid, 0)`. - A live owner is kept, so a concurrent suite is never collected — this repo is routinely checked out into several worktrees at once. Age cannot answer this: writes land under `databases/` and `user-data/`, which never refreshes the root's mtime, so a run paused in a debugger looks arbitrarily old. - A dead owner is collected immediately. - An undeterminable owner (missing, empty or malformed marker) falls back to a 24h cutoff. The marker is published via rename so a half-written file cannot bypass that guard. - A live-looking owner past a week is collected anyway, since the OS recycles pids and a stranger inheriting one would otherwise pin the directory forever. Covered by a 10-test spec running on Linux, macOS and Windows, since `process.kill(pid, 0)` semantics are platform-specific. Each behaviour was verified to fail against the preceding implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c637a0520e |
chore(deps): bump softprops/action-gh-release from 2 to 3 (#1284)
* chore(deps): bump softprops/action-gh-release from 2 to 3 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(ci): allow softprops/action-gh-release v3 in the Snap workflow policy The Snap supply-chain policy test pins the exact major of every action the build workflow may use, so bumping softprops/action-gh-release in the workflow without updating BUILD_ACTION_ALLOWLIST fails publish-snap-workflow.test.mjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
55f68e73c8 |
chore(deps): bump actions/setup-node from 4 to 7 (#1285)
* chore(deps): bump actions/setup-node from 4 to 7 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(ci): allow actions/setup-node v7 in the Snap workflow policy The Snap supply-chain policy test pins the exact major of every action the build workflow may use, so bumping actions/setup-node in the workflow without updating BUILD_ACTION_ALLOWLIST fails publish-snap-workflow.test.mjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
553f45dedc |
chore(deps): bump actions/cache from 4 to 6 (#1281)
* chore(deps): bump actions/cache from 4 to 6 Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(ci): allow actions/cache v6 in the Snap workflow policy The Snap supply-chain policy test pins the exact major of every action the build workflow may use, so bumping actions/cache in the workflow without updating BUILD_ACTION_ALLOWLIST fails publish-snap-workflow.test.mjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <serega05@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
5e725a06f3 |
chore(deps): bump actions/deploy-pages from 4 to 5 (#1283)
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
0e16e179bf |
chore(deps): bump github/codeql-action from 3 to 4 (#1282)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [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/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
bfad82c26c |
fix(settings): stop settings silently reverting on restart (#1272)
Settings live in the renderer's IndexedDB, and two failure modes made them look saved while nothing reached disk. A second app instance sharing the same userData directory cannot take the Chromium storage lock, so its renderer reads defaults and every write is dropped. The app now holds a single-instance lock and focuses the running window instead of starting a rival copy. The lock is requested after the userData override so E2E runs with their own data dir keep independent locks, and after Squirrel event handling. IPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1 opts out for local CDP debugging. updateSettings() patches in-memory state before persisting and the submit path had no rejection handler, so a failed write produced an unhandled rejection and no user-visible feedback. SettingsStore now records which half of the round trip failed, and the settings page surfaces it through a dismissible error snackbar; the dialog stays open on failure so the save can be retried. Two follow-ups from review, both wider than the report: - a second launch now re-creates the main window when the lock owner has none left, so closing the last window on macOS no longer leaves a second launch quitting silently with nothing on screen - App.onMainWindowCreated() re-runs window-owned bindings for every rebuilt window, so the downloads broadcaster stops holding a destroyed window. This also fixes the same bug on the pre-existing dock `activate` path. Closes #1156 Closes #102 |
||
|
|
0334296f15 |
fix(electron-backend): make test suite and lint host-agnostic on Windows checkouts (#1176)
* fix(electron-backend): make test suite and lint host-agnostic across Windows/Linux checkouts Windows checkouts (core.autocrlf=true) had 13 pre-existing jest failures and 5 Windows-only lint errors in electron-backend while Linux CI was green: - embedded-mpv-native-source.spec: normalize CRLF after readFileSync so multi-line source assertions match on autocrlf checkouts - worker-runtime-paths.spec: build expected candidate paths with path.join instead of hardcoded POSIX strings - external-player-launch-context: join darwin-only paths (.app bundle executables, Homebrew Caskroom) with path.posix.join so simulated darwin platforms resolve correctly on win32 hosts (no-op on macOS) - app.spec: build packaged-navigation fixtures with path.resolve + pathToFileURL; file:///tmp/... is not a valid win32 file URL - lint target: quote the eslint glob. The unquoted ** was expanded by the POSIX shell on Linux (shallow match), so CI linted only a subset of files while Windows passed the literal pattern to ESLint and linted the full tree - the hosts checked different file sets - fix the 5 errors full-tree linting surfaces: prefer-const in epg-worker.service and database.worker-connection, no-unsafe-finally in external-player-session-registry and embedded-mpv-native.service (rewritten as catch-swallow with identical semantics, pinned by new regression tests), intentional no-control-regex in the recording filename sanitizer; drop stale unused disable directives - document the quoted-glob convention in CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: mirror the quoted-lint-glob convention into AGENTS.md AGENTS.md already mirrors the neighbouring max-lines/baseline paragraph from CLAUDE.md, and it requires coding conventions to stay in sync between the two files. The quoted-glob rule landed only in CLAUDE.md, so agents bootstrapping from AGENTS.md could reintroduce a host-dependent lint target. Also corrects the wording in both copies: the shallow expansion happens on macOS as well as Linux — /bin/sh has no globstar on either — and the target still exits 0 with a broken glob, which is why this went unnoticed. Adds the file-count check that catches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
19b592badb |
fix(epg): make manual EPG mapping lookups actually fail soft (#1291)
The mapping handlers documented a fail-soft contract but only honored half
of it. Four of them returned the database operation's promise from inside
the `try` block without awaiting it, so the rejection escaped the `catch`:
the try block exits before the promise settles. A `getDatabase()` failure
was caught, but a SQLite error in the operation rejected the
`ipcMain.handle` promise, and the renderer call threw instead of receiving
`null` / `{success:false}` / `[]`.
Adding `await` in handleGetEpgMapping, handleSetEpgMapping,
handleDeleteEpgMapping and handleSearchEpgChannels closes the gap.
resolveChannelIds and handleGetEpgMappingsBatch already awaited correctly
and are unchanged.
This is pre-existing — the same shape predates the epg.events.ts split in
|
||
|
|
5932e71cb9 | fix(electron-backend): process zero-delay database cancellation (#1295) | ||
|
|
a2d678bdda |
fix(packaging): stop the Linux frame-copy probe timing out on cold sandboxes (#1294)
The packaging verifier bounded `iptvnator_mpv_helper --runtime-probe` with RUNTIME_PROBE_TIMEOUT_MS (3s) — a constant it shares with the application's own startup capability gate. Three seconds is a tight budget for a helper that dlopens libmpv plus EGL/GL/GBM, and the Flatpak profile is closest to that edge because the helper runs inside the sandbox against its bundled closure: on #1277 the job failed three consecutive reruns and passed on the fourth with no code change, while the concurrent master job passed. Give the verifier its own budget rather than raising the shared one. The app's probe is a blocking spawnSync on the Electron main process, so a hung helper must not stall window creation, and a timeout there degrades gracefully to the native-view fallback. Nothing waits on the packaging probe but the CI job, which already has its own 120-minute bound, while a premature kill reports a healthy package as broken. - PACKAGE_VERIFICATION_PROBE_TIMEOUT_MS (15s) and PACKAGE_VERIFICATION_PROBE_MAX_ATTEMPTS (2) join the frozen probe contract; RUNTIME_PROBE_TIMEOUT_MS stays at 3s for the application gate. - runBoundedRuntimeProbe() retries only on ETIMEDOUT, repeating the identical bounded launch (same command, args, env, maxBuffer, killSignal) and announcing the retry on stderr so a degrading trend stays visible. Fail-closed behaviour is unchanged. A hard timeout is the one probe outcome that says nothing about the payload; spawn errors (a missing helper, a wrapper launched instead of the real ELF), termination by signal, nonzero exits and malformed or wrong-protocol lines all still fail on the first attempt, and a helper that keeps hanging still fails once both attempts are spent. The four new/extended verifier tests cover retry-then-success (asserting the second launch is identical to the first), exhausted timeouts still rejecting, four non-timeout verdicts each probing exactly once, and the attempt bound itself. Setting MAX_ATTEMPTS to 1 fails four of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d2fd27b535 |
test(performance): deflake the real-timer event-loop delay spec (#1293)
* test(performance): deflake the real-timer event-loop delay spec The real-timer capture spec failed under full-suite parallelism because `monitorEventLoopDelay()` records nothing on its first internal timer tick - that tick only seeds the previous timestamp, so the first delay sample lands on the second tick. Condition-based arming therefore needs two event-loop turns inside its fixed 50ms wall-clock budget, and a machine running 10 Jest workers stretches a single turn past 20ms. The capture then degraded to a documented `event-loop-delay-arm-timeout`, which is the intended graceful path, while the spec asserted the happy path of that race and turned an environmental outcome into a red build. Retry the real-runtime capture within a 5s budget instead. The real `node:perf_hooks` runtime and the real 20ms block are kept, since the fake harness returns a hard-coded histogram max and never measures anything. Every attempt still asserts a contract: instrumentation never breaks the wrapped work, and a null delay must carry a documented arm/flush timeout rather than being silently null. Budget exhaustion warns instead of failing, so load can no longer produce a false failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(performance): assert the real delay measurement unconditionally Codex flagged that budget exhaustion still passed the test, so a regression that made arming or flushing time out on every attempt would have been reported as a warning rather than a failure - removing the only assertion backed by Node's real histogram and a genuine event-loop block. Drop the tolerant retry loop. The arming deadline is read through the injectable `readMonotonicMs()`, so scaling only that clock leaves the wait bounded by its other limit, the 50-poll ceiling, which is ~25x the two event-loop turns arming actually needs. Everything else stays production code: the real `monitorEventLoopDelay()` histogram, real `setTimeout()` polling, and real epoch/CPU/ELU boundaries. The scaled clock reaches nothing but the wait budgets, since its only other consumer records phase events and this spec records none. The test now always asserts a real measurement and hard-fails otherwise. Verified by mutation: forcing arming to never arm fails it, and dropping the deliberate block fails it at maxMs 3.8ms against the 10ms floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
518964c57f |
refactor(electron-backend): split the last four files over the max-lines cap (#1288)
Follow-up to #1278, which split four of the files the electron-backend lint target had been silently skipping. Four were left over; this splits them, so no file in the project sits above the 400-line cap outside the baseline. None are added to tools/eslint/max-lines-baseline.mjs — the baseline only shrinks. Shared setup moves to *.test-helpers.ts, the suffix tsconfig.app.json already excludes, so the production build never sees jest globals. - remote-control.events.spec.ts 588 -> 188, plus remote-control-http.spec.ts. The mock registry moves to remote-control.test-helpers.ts; each spec keeps its own jest.mock() factories, which resolve the mock-prefixed exports. - downloads.events.spec.ts 530 -> 182, plus downloads-actions.spec.ts. The jest.doMock setup is not hoisted, so the whole harness moves to downloads.test-helpers.ts behind setupDownloadsEventsHarness(). - http-server.spec.ts 448 -> 377, plus resolve-static-file-path.spec.ts. The resolveStaticFilePath cases were already self-contained. - worker-performance-capture.spec.ts 408 -> 149, plus worker-performance-capture.resilience.spec.ts, matching the .concurrency and .histogram siblings. Test bodies are unchanged; the helpers keep the same identifiers in scope so the splits are a move, not a rewrite. Verified with the glob quoted locally (that quoting is #1176's, not part of this change): 245 files, 0 max-lines errors, and the only remaining lint errors are the five #1176 fixes. Both tsconfig.app.json and tsconfig.spec.json typecheck clean. Note: worker-performance-capture.concurrency.spec.ts is flaky on master independently of this change — it asserts a real 20ms event-loop block and failed 4/4 clean-master runs here. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
24f0dee6f0 |
test(performance): add formal M3U import benchmark (#1287)
* test(performance): add formal M3U import benchmark * test(performance): harden formal capture validity * test(performance): address benchmark review feedback |
||
|
|
26331676f9 |
fix(epg): await mapping queries so lookups fail soft (#1279)
The four EPG mapping handlers wrap their DB call in try/catch but return the
promise instead of awaiting it. An async function *adopts* a returned promise
rather than awaiting it, so the catch block only ever fired when getDatabase()
itself threw — a rejection from the underlying query escaped to the IPC caller
instead of returning the intended null / {success:false} / [] fallback.
Add the missing await to handleGetEpgMapping, handleSetEpgMapping,
handleDeleteEpgMapping and handleSearchEpgChannels, matching
handleGetEpgMappingsBatch and resolveChannelIds in the same module, which
already awaited and so already failed soft for both cases. This restores the
contract stated in the module's own doc comment: a mapping lookup must never
take down an EPG request.
The behavior predates the max-lines split in #1278, which carried it over
verbatim from epg.events.ts.
The new spec drives the real IPC handlers captured from ipcMain.handle, so it
asserts the contract that matters: the caller gets a fallback, not a rejected
promise. Reverting the four awaits fails exactly those four handlers and
leaves the already-correct batch handler green.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e2300bea11 |
test(settings): split the settings spec along the facade seams (#1277)
settings.component.spec.ts was 1516 lines and the last settings file in the max-lines baseline. The behaviour that moved into facades now has its own specs, driven directly instead of through the rendered page. - settings-app-update.facade.spec.ts: status polling/retry, bridge actions, release notes dialog, version messaging, dispose - settings-epg.facade.spec.ts: refresh, clear flow, post-save re-fetch - settings-playlist-reset.facade.spec.ts: summary, dialog, Electron progress, browser fallback, failure snackbar - settings-backup.facade.spec.ts: desktop export, browser download fallback - settings.component.spec.ts keeps the page shell, the facade lifecycle seam and runtime capabilities; settings.component.form.spec.ts takes hydration, section outputs, dashboard controls and submit - settings-section-scroll.directive.spec.ts gives the directive its first spec - shared TestBed fixtures live in settings/test-stubs/, kept out of both the app build (.stub.ts) and the coverage ratchet (test-stubs/) 105 settings tests, up from 94; every file is under the 400-line limit, so settings.component.spec.ts leaves the baseline. |