mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
perf/devtools-dev-only
328
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
acb8cb0318 |
fix(workspace): lead header Back to a parent route when the page opened the session (#1830)
* fix(workspace): lead header Back to a parent route when the page opened the session Settings, Discover, actor and in-portal search registered a header Back that only ran Location.back(). As the first entry of the session (deep link, reload, restored view) that did nothing in Electron and left the app in a browser. WorkspaceBackNavigationService.back(resolveParent) keeps Location.back() while the previous entry is an in-app one, and while that is unknown because the Navigation API is missing. Otherwise it opens the page's parent with replaceUrl, so history Back cannot return to the page: - Settings: the first workspace view (resolveDashboardPath()). - Discover: the catalog section it lists (vod for movies, series for TV). - Actor and search: the portal root, which redirects to its default section within the same navigation. The web E2E opens these pages in a fresh tab: a page.goto in the same tab leaves the previous document behind, often at the parent's URL, so history Back passed without the fix. Electron covers settings after a window reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): lead first-entry Back to the parent without the Navigation API Review follow-ups (Greptile): - Without the Navigation API (older Safari and Firefox) back() always called Location.back(), so a page that opened the session still left the app. The service now tracks the router's in-app history depth there (trackRouterHistoryDepth): first navigation 0, push +1, replacement keeps it, a traversal restores the depth recorded for its entry. Depth 0 opens the parent; an unknown depth (an entry from before a reload) keeps Location.back(). - Stalker's Discover (movie/tv section), actor and search pages now have tests that they hand the service the parent under the portal :id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): adopt the router navigation the Back depth tracker missed Review follow-up (Codex, Greptile): the lazy workspace shell creates the Back service after the first NavigationStart, so the tracker saw only its NavigationEnd, left the depth unknown and counted the next push as the first entry. It now adopts the router's current or last successful navigation when it starts: a first navigation is depth 0, a later one leaves the depth unknown (browser history Back), and a late start of the adopted navigation is not counted again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
93c5f1a051 |
fix(portals): preserve playlist ownership during detail handoffs (#1825)
* fix(portals): preserve playlist ownership during detail handoffs * fix(portals): reload Stalker categories only for a held destination Review follow-ups (Greptile, Codex): resetCategories() reloaded the category resource, and the route session calls it on a portal switch before the destination is resolved and on teardown, so it asked the portal being left, and a failed destination lookup could let that answer repopulate the sidebar. resetCategories() now only clears; the session calls the new reloadCategories() after installing the destination, and only when a handoff had already put that playlist in the store (the owner, and so the resource params, did not change). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
335392afa9 |
perf(portal): make the portal Eager components OnPush (#1821)
* perf(portal): make the portal Eager components OnPush Plan item C6 step 3 for libs/portal: the nine Eager components in portal/shared/ui, portal/stalker/feature and portal/xtream/feature switch to OnPush. Their templates read signals, signal inputs, async pipes and template-event state; the plain fields they write outside events (playback request ids, save throttles) are not rendered. The already-OnPush live channel lists filled their favorites Maps in a subscription and the Xtream list dropped programme previews after the EPG mapping dialog, all without marking the view. They now call markForCheck like the neighbouring handlers do, so a late favorites answer shows its hearts without waiting for an unrelated check. A regression test for the Xtream list fails without the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(portal): let the favorites handler's markForCheck render the heart Review follow-up (Greptile): the test forced detectChanges() after the favorites arrived, so it passed without the handler's markForCheck(). It now lets the fixture render on its own; removing the call fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2738bc28a1 |
docs(portals): document forced MPV/VLC launch and pending-start contracts (#1809)
* docs(portals): document forced MPV/VLC launch and pending-start contracts The rules #1792 settled for forced external launches and playback-start bookkeeping lived only in code comments and specs. Record them as contracts in the owning documents: - embedded-inline-playback.md: the shared detail-host rules (one external player per title, unconfirmed teardown cancels, ownership rechecked after every await, owner-scoped pending state). - xtream-portal-compatibility.md: the series launch chain, page-token duplicate guard and queued choice. - vod-multi-source.md: the movie menu launch and reset follow the copy the primary button acts on; pending resets are a list of targets. - stalker-portal.md: the per-series launch queue, the batch-held choice and the movie launch/reset pending start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(portals): correct launch-contract claims against the code - A launching session is published before the launch IPC resolves; what it lacks until then is an exact closer. - The series watched/reset batch and the Xtream movie launch gate are page-wide, not owner-scoped. - Only the Stalker movie hosts retire a pending start, and that does not clear the repeat guard of a launch still in flight. - Name only the specs that exercise the Xtream series rules. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(portals): tighten closer timing and page-wide gate wording A launching session may already have its closer before the launch IPC resolves, the Xtream movie launch gate does not cover the inline player's diagnostic fallback, and the hero-state spec covers only the external-player and reset rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(portals): fix stale session-timing comment and search guard wording - serial-details-external-launch.ts: Electron publishes a `launching` session as soon as the launch IPC arrives, not only after the launch settles. Comment only; no behaviour change. - stalker-portal.md: the search host's selection check does not include the content type; a switch to a series supersedes the launch through the playback owner key instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
2dfdd65f53 |
refactor(details): give the detail-page files real max-lines headroom (#1813)
* refactor(xtream): split the series details page into focused services serial-details.component.ts sat at the 400-line max-lines limit and its playback service and spec were close behind. Move cohesive concerns out without changing behavior: - route params, the provider-only flag and the (re)load of the addressed series go to SerialDetailsRouteService; the component still registers the effect, so effect order is unchanged - season descriptions, posters and the TMDB season enrichment go to SerialDetailsSeasonsService - actor, Similar and Discover navigation go to injectXtreamDetailNavigation, shared with the movie page - the watched toggles and the episode playback payload leave SerialDetailsPlaybackService for SerialDetailsWatchToggles and buildSerialEpisodePlayback - the spec's TestBed moves to a harness and the watched-toggle cases to serial-details.season-watch.spec.ts Counted lines: component 400 -> 318, playback service 388 -> 342, component spec 1196 -> 674. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(xtream): move VOD route selection and position state out of the page vod-details-route.component.ts grew from 618 to 741 counted lines and vod-details-playback.service.ts reached 395. Extract without changing behavior: - the route-derived read model (selected movie, category, catalog item, fallback view, multi-source identity, session and content keys) goes to VodDetailsSelectionService; the component keeps the same member names - trailer state and the Similar-rail click move into the hero presenter, the cancel-download prompt and the progress-ring geometry into the downloads service, navigation into injectXtreamDetailNavigation - stored positions (last seen, route row, guarded load) become VodDetailsPositionState and the external-launch bookkeeping becomes VodExternalLaunchClaim - drop the unused MatTooltip import (NG8113) and eight unused imports Counted lines: route component 741 -> 492, playback service 395 -> 344. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract series-view positions and watched toggles stalker-series-view.component.ts grew from 1601 to 1813 counted lines and its spec reached 1198 of 1200. Move code out verbatim: - saved positions, their reconcile and the persist/clear writes go to the component-provided StalkerSeriesPositionsService; the ordering of reads and writes goes to StalkerSeriesPositionQueue - episode, season and series watched toggles go to StalkerSeriesWatchToggleService, the batch core to runStalkerWatchToggleBatch - the lazy VOD season loads go to StalkerVodSeasonEpisodeLoader Every effect stays registered in the component constructor in its original order; template bindings and public member names are unchanged. The spec setup moves to a shared harness and the spillover-prefetch and TMDB season cases to their own specs; the 184 cases of the directory are unchanged. Counted lines: component 1813 -> 1136, component spec 1198 -> 670. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract catalog-detail position and search paging - stalker-catalog-detail.component.ts (397): the stored VOD position and its runtime updates become StalkerCatalogVodPosition, the Play/Resume start becomes startStalkerCatalogVodPlayback - stalker-search.component.ts (776, baselined): the paging resource, the accumulated results and the parental-lock bookkeeping become StalkerSearchPagingController, with pure helpers in stalker-search-results.util.ts. The spec reaches the moved members through component.paging; its cases and assertions are unchanged. Counted lines: catalog detail 397 -> 317, search 776 -> 482. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(ui): give season-container and vod-details real line headroom - season-container.component.ts (396): season auto-selection and the seasonSelected emission move to createSeasonAutoSelectState, called at the same place in the constructor so effect order is unchanged; the episode subline becomes buildEpisodeSubline - vod-details.component.ts (393): the cross-portal Similar loader, the provider-only download state and the actor/Similar route helpers move to sibling modules Inputs, outputs, selectors and public members are unchanged. Counted lines: season container 396 -> 341, vod details 393 -> 337. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-e2e): move Stalker portal helpers into fixtures stalker.e2e.ts was at 1196 of 1200 counted lines. Move the mock endpoints, scenario MACs and page helpers to stalker-portal.fixture.ts and the embedded-series steps three tests repeated to stalker-embedded-series.fixture.ts. Every test stays in stalker.e2e.ts in the same order, with the same serial mode and OWNED_MACS reset. Counted lines: 1196 -> 981. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: follow the detail-page extractions Name the Stalker watched-toggle and position services that now own the batch and reconcile code, point AUTH_REJECTED_MAC and the scenario MACs at stalker-portal.fixture.ts, and drop two stale statements about components sitting at the max-lines cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(xtream): note why the seasons service is injected last Its TMDB enrichment effect keeps the position it had as the first effect of the component constructor only while it is created after the other services' effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
84aef83a6c | feat(workspace): move page Back buttons into the header and add a history fallback (#1814) | ||
|
|
e8b181fcea |
fix(ui): Cyrillic/Greek weights, html lang, weight normalisation (#1780)
Load Roboto 600/700 and DM Sans 700 so Cyrillic and Greek headings render real semibold and bold faces instead of a synthetic bold, keep <html lang> in step with the UI language, and move every font weight onto the 400/500/600/700 scale (JetBrains Mono at 500 or lighter; the dashboard LIVE badge now uses the interface font at 700). Add the `styles:font-weights:validate` ratchet guard and its CI step. It reads stylesheets much as Sass and the browser do (cascade, layers, mixins, content blocks, `@extend`, `@at-root`, `:is()`/`:where()`, keyframes) and lists what it deliberately does not trace in its header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ab8460338a | feat(ui): cinematic movie and series details pages and dashboard hero (#1792) | ||
|
|
cb252940b2 | fix(workspace): move detail Back into the header and drop the rail brand (#1789) | ||
|
|
23a1860119 |
fix(ui): destructive confirmations, verb labels and provider icons (#1783)
* fix(ui): destructive confirmations, verb labels and provider icons
Confirmations: ConfirmDialogData.confirmLabel is required, so no dialog can
fall back to "Yes"/"No"; the dismiss defaults to "Cancel" and
`tone: 'destructive'` styles the confirm with .app-destructive-button. Every
caller names its action ("Remove playlist", "Clear", "Refresh playlist",
"Cancel download" with a "Close" dismiss). The confirm button has the
confirm-dialog-confirm test id and drops its no-op color="primary".
The no-op `warn` color input becomes .app-destructive-button on the EPG
mapping, playlist item, error view, EPG/reset settings, delete-all and source
cleanup buttons, and on the unsaved-changes dialog's Discard.
Provider icons come from SOURCE_TYPE_ICONS in shared/interfaces (Xtream
cloud, Stalker cast, M3U playlist_play / link / description / subject) in the
add dialog, auto-import, empty state, playlist switcher, playlist rows,
dashboard source rail, command palette, Sources filters and both reset
summaries. Stalker no longer borrows the Dashboard icon, and Xtream no longer
shares a glyph with M3U URL playlists.
The playlist error view removed a playlist through the stale
PlaylistActions.removePlaylist: it dropped the playlist from state before the
delete ran, swallowed failures, skipped the source activity guard and showed
no toast. It now uses PlaylistDeleteActionService like every other removal,
commits only a completed delete, toasts and goes home. The unused action and
its effect are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): one provider icon per playlist row, imperative Korean remove label
A restored Stalker or Xtream playlist can also carry a URL, and the row's
independent checks then showed the M3U URL icon next to the provider icon.
The row now switches on resolvePlaylistSourceIconKey(), the precedence every
other surface uses, so each source shows exactly one icon.
HOME.PLAYLISTS.REMOVE now names the confirm button and the row's delete
tooltip; in Korean it read "the playlist has been removed". It now says
"remove playlist", like every other locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep the auto-refresh badge on playlist rows with one provider icon
Showing one provider icon per row moved the auto-refresh badge into the M3U
branches only, so a restored Stalker playlist with a URL and auto-refresh
lost it although the URL is still re-fetched. The row now renders one icon
container: the provider icon from the shared precedence, then the badge for
any row with a URL or a local M3U, exactly the rows that showed it before.
The Xtream portal-status dot, used without source health, keeps that corner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): let the playlist row's cancel action render in the error color
The row's action buttons set `color: inherit`, and the selected row does so
again with more specific selectors. Both beat Material's token-driven icon
color, so the .app-destructive-button cancel action kept the row color
(selection blue on the active row). Pin the cancel button to
--mat-sys-error in both row states.
The large-deletion Electron E2E now checks the cancel color in both themes;
without this rule it reads rgb(47, 123, 255) instead of the error red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): give the dialog service spec the now-required confirm labels
ConfirmDialogData.confirmLabel became required, and the spec still built
confirmations without one. Jest only transpiles, so the suite stayed green,
but the "Typecheck Jest spec programs" CI step rejected it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
572034f3be | fix(ui): declare Material system tokens and migrate dead --mdc overrides (#1775) | ||
|
|
1dba959852 |
fix(portals): treat an undetermined audio language as unknown (#1689)
* fix(portals): treat an undetermined audio language as unknown ICU 78, which Electron 43 and Node 26 ship, canonicalizes the ffprobe marker `und` to the subtag `und` instead of an empty one. The source metadata then recorded it as a stated language, so a switch between a copy tagged `und` and one tagged English raised the "dub may differ" warning. CI's older ICU hid this: the existing spec only fails on the newer runtime. Decline `und` explicitly, and cover `und-US` plus the dub comparison. * test(portals): cover undetermined language across runtimes --------- Co-authored-by: 4gray <serega05@gmail.com> |
||
|
|
78a6648c8d |
perf(electron): let hidden and minimized windows report themselves hidden (#1724)
* perf(electron): let hidden and minimized windows report themselves hidden The main window was created with backgroundThrottling: false (since #1123, without a stated reason). Electron then keeps document.visibilityState at "visible" for a hidden, minimized or fully covered window and never lets Chromium throttle it, so every renderer timer, rAF and CSS transition ran at full rate in the background, and the playback keep-awake gate, which releases the display for a minimized window, could never see one. Use Chromium's default. Audible media and picture-in-picture are exempt from background throttling in Chromium, and a local check confirmed HLS playback continues unchanged through more than six minutes minimized, audible and muted. Playwright's focus emulation pins every page it attaches to as visible, so the new window-visibility E2E launches the app without Playwright and drives it over raw CDP (electron-unautomated-launch.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): harden the unautomated Electron launch Review follow-ups for the window-visibility E2E: - Resolve `electron` in the main process through a require created from the `node:module` builtin instead of `process.mainModule`, which only exists when the app entry is CommonJS. - Bound teardown like closeElectronApplicationAndConfirmExit: SIGTERM, then SIGKILL, 5 s each, then fail instead of waiting forever. - Surface CDP protocol errors from Runtime.evaluate instead of returning undefined. - Wait until the window is actually shown before hiding it. The app shows its window on ready-to-show, and a hide() that lands earlier is undone by that show(); this was the first-attempt failure on the macOS shard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): check the playback display lock is released while hidden Review follow-ups for #1724: - Add an E2E that plays the webm fixture in the unautomated launch, records the main process's prevent-display-sleep blockers, and asserts the keep-awake lock is taken while visible, released when the window is hidden, and taken again when it is shown. The visibility tests alone would still pass if the renderer gate or the bridge stopped updating powerSaveBlocker. - Validate CDP replies before dispatch (CodeQL js/unvalidated-dynamic-method-call): only a numeric id with a pending settle function is called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): skip killing an Electron that already exited stopElectron now checks the recorded exit state before each signal and tolerates a kill that races the exit: on Windows taskkill throws for a PID that no longer exists. Startup cleanup can no longer replace the startup error that explains the failure; a cleanup failure there is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep the watchdog cadence while the window is hidden With background throttling on, Chromium wakes a hidden, silent page's timers at most once per minute after five minutes, so a portal that asks for get_events every 30 s would see pings at half its cadence while the window is minimized. Tick the watchdog from a dedicated worker (createBackgroundInterval, an inline blob worker allowed by the renderer CSP), whose timers are not subject to page throttling. It falls back to a page setInterval where no worker is available or the worker fails to load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep a stopped watchdog interval stopped A worker error that arrived after stop() started the page fallback interval, which nothing cleared, so pings continued for an inactive playlist. stop() now marks the interval stopped, detaches the worker handlers, and the fallback refuses to start afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dc3f829807 | fix(parental-lock): focus, tokens and phone width for lock UI (#1761) | ||
|
|
f38c6f86d1 | feat(playback): draw catch-up programmes as seek-bar segments (#1750) | ||
|
|
bcc6186c88 |
fix(settings): hide cached title matches the parental lock withholds (#1735)
* fix(settings): hide cached title matches the parental lock withholds Cross-playlist title matches are cached by their consumers (Actor and Discover routes, the dashboard trending and recommendation rails and the four "similar in your portals" rails), so matches found while unlocked kept advertising locked titles and their playlist names after a relock. Each consumer now filters on read through a reactive predicate on the match's provider category, so a relock hides them at once and an unlock shows them again, without a re-query. Closes #1723 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fall back to unlocked copies of a withheld title match The dashboard trending and recommendation rails and the "similar in your portals" rails picked one match per title at load, so hiding a withheld match lost a copy of the same title in an unlocked portal. They now keep every row the lookup returned and pick the match on read from the rows the parental lock does not withhold. Adds the release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): derive the recommendations seed heading from the visible cards The rail kept only the seeds that contributed a card to the original selection, so a seed whose recommendations filled in after a relock (or an offline prune) was missing from the "Because you watched" heading. Every seed of the load is kept in order and the heading lists those that contribute a card now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d8229b98fa |
feat(playback): record recently viewed only after the stream plays (#1732)
* feat(playback): record recently viewed only after the stream plays A channel, movie or episode used to enter Recently Viewed (and the dashboard's Continue Watching hero) the moment it was selected or its link was resolved, so streams that failed straight away cluttered the history. Writers now defer the write to a root PlaybackHistoryGate, keyed by the stream URL and/or the playback session key. The inline players confirm those keys once the owned engine's position has advanced by two seconds (seeks, stalls, pauses and a previous stream's progress do not count), the radio player does the same, and a launched MPV/VLC session confirms on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on selection. Covers M3U (live, radio, movie detail), Stalker (live, radio, VOD, series), Xtream VOD and series, and the global live collection. The M3U host's embeddedPlayback is now compared by value: the history write updates the playlist meta mid-playback, and a new but identical playback object remounted the engine and restarted the stream. E2E flows that relied on recording-on-click now play local fixtures (HLS/TS/WebM routed in place of unreachable or public streams) and wait for confirmed playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): tighten recently viewed confirmation per review - Correlate by session key first: when both the deferred write and the confirmation carry a playbackSessionKey, only that is compared, so the same stream URL played in another playlist no longer records a failed attempt. URLs remain the fallback (portal writes, MPV/VLC sessions). The M3U radio player now receives the host's session key. - Count only playing progress: engines report `playing` (not paused, not seeking) with each time update, so short seeks of paused media no longer confirm a view. - Xtream: a write confirmed after a playlist switch still saves to its own playlist but no longer replaces the current playlist's recent list. - Global live tab: a row confirmed after another row was selected still moves to the top of an open Recently Viewed list (only a disposed tab skips the notification). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): confirm session-keyed history only by its own session A write deferred with a playback session key (M3U) is now confirmed only by that key. The app-wide MPV/VLC session confirmation carries just the URL, so opening the same stream externally from another playlist could still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is instead confirmed by the WebPlayerViewComponent that requested it, under its own session key, once the launch has opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): keep the history gate off the initial bundle The `@iptvnator/services` barrel ships in the initial bundle, so adding PlaybackHistoryGate there (and subscribing to it from the app-wide ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes. - Move the gate to a new lazy-only `playback-data-access` project (`@iptvnator/playback/data-access`; scope:shared, domain:playback, type:data-access) and register it in the coverage policy. - The gate subscribes to MPV/VLC session updates itself; it is created by the first deferred write, which precedes the launch it waits for. ExternalPlaybackService is back to master. - The Xtream "playlist switched before confirmation" check moves to the lazy helper; the initial-path store only takes a `skipListRefresh` flag. Net effect on this branch: +27 bytes over master (master itself is 108 bytes over the ratchet baseline already). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): drop late Xtream confirmations off the initial path The Xtream store ships in the initial bundle, so even the small `skipListRefresh` flag cost 27 bytes there. A confirmation can only arrive after a switch to another playlist from a slow MPV/VLC launch (the inline player goes with the page), so the lazy helper now drops it instead: recording it would misfile the item or replace the other playlist's recent list. with-recent-items is back to master. This branch is now 3 bytes below master on renderer.initialBytes; the ratchet still reports master's pre-existing overage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(playback): pass the spec type-check gate from master - playback-data-access: align tsconfig.spec.json with the epg-data-access config #1705 updated (bundler resolution, global.d.ts for window.electron). - M3U recent-history spec: type the selectSignal override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep late Xtream confirmations in their own playlist history A confirmation that arrives after a switch to another playlist (a slow MPV/VLC launch) is no longer dropped: the lazy helper saves it to the captured playlist through the data source, without reloading the store's recent list, which belongs to the other playlist by then. The store and its barrel ship in the initial bundle, so the save path stays in the feature helper; renderer.initialBytes stays under the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): correlate global live-tab history by its session key The unified Favorites/Recent live tab deferred its history write by stream URL only, so the same URL played from another playlist could confirm a failed selection, and a switch to catch-up before confirmation could never match. It now defers with the tab's playlist-scoped playbackSessionKey (the key its players confirm with), and the tab's radio player receives it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep MPV/VLC rows of the live tab confirmable by URL The live tab's session key can only be confirmed by its own inline players; MPV/VLC confirm the launched URL alone. A row that goes to an external player (also later, after a double-click) now defers by URL, and only rows played inline carry the session key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): per-channel M3U history attempts, capability-based Xtream fallback - M3U: the recently-viewed dedupe key now includes the channel id, so a second row of the same URL defers its own write (its session key) and is recorded when it plays after the first row failed. - Xtream late write: key uncached content by Xtream id per supportsXtreamSqliteDataSource (the data-source factory's contract), not by a generic Electron bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
650da4a1d3 |
ci(test): type-check Jest spec programs and gate it in CI (#1705)
* build(test): make spec tsconfigs resolve what Jest resolves Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot see Angular's exports-only secondary entry points, and dropped global.d.ts, so tsc reported thousands of resolution errors and no window.electron typing. Switch them to module: preserve with bundler resolution (ts-jest still forces CommonJS emit outside ESM mode), add global.d.ts to every spec program, type jest.unstable_mockModule for the ESM workspace, include the ui-epg and ui-playback specs that jest.web-esm.workspace.ts runs under the web spec config, and drop the snack-bar stub that shadowed the real Material types. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci(test): gate spec type-checking with typecheck:spec Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into the unit-and-typecheck job after typecheck:ci, and document the gate and the spec tsconfig conventions in the validation map. Also bring the non-Tier-A spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to the same conventions so the gate covers the whole workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: fix the spec type errors surfaced by typecheck:spec With the spec programs resolving modules and ambient typings correctly, tsc reported 432 genuine errors across the Tier A projects: read-only capability flags assigned on Partial<> doubles, signal-store values used as types, fixtures missing required fields, index-signature property access, partial bridge doubles cast through incompatible shapes, and deferred resolvers narrowed to never. Type the doubles instead of casting to any: writable mapped types for capability flags, InstanceType<typeof StalkerStore>, typed jest.fn signatures, protectedState: false on test signal stores, and completed fixtures. Production changes are limited to bracket access for index-signature properties under the libs' noPropertyAccessFromIndexSignature setting and two narrowing guards in the global favorites loader. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(playback): use the ESM setup's jest global in the controls fixtures The fixture imported jest from @jest/globals, which is not a direct dependency. Jest provides that module at runtime, so tests passed, but on a clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI. The ESM test setup already installs import.meta.jest as the global, typed by @types/jest, as the other ESM specs use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: type the parental lock doubles merged since the gate was written The parental lock feature (#1601) and the Stalker actor route landed on master with spec doubles declared as zero-argument jest.fn()s that the tests then drive with the real arguments, plus a copy of the ResizableDirective override imported from a library that does not export it. Give the doubles the lock service's real signatures, drop the dead override as in the sibling layout specs, use bracket access for the actor route's personId param, and keep the Stalker layout spec within the 1200-line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9d02f90dfe |
perf(web): keep backup/restore and portal helpers off the initial path (#1734)
* perf(web): keep backup/restore and portal helpers off the initial path #1601 (parental lock) put about 35 KB onto the renderer's initial path by design (the lock service, lock store and enforcement gate the workspace resolver and the catalog data sources) and was merged with the ratchet red: renderer.initialBytes 1,655,428 against the 1,619,993 baseline. Offset it without touching the lock gate. Code splitting puts a module in the chunk shared by every entry that reaches it, so helpers only lazy routes use landed in initial chunks because eager files reach them through barrels: - PlaylistBackupService (only the lazy settings page) moves to @iptvnator/services/playlist-backup and out of the services barrel. - The eager Xtream data layer and root shell import the portal logger and DI tokens through @iptvnator/portal/shared/util/logger and /tokens instead of the barrel, whose navigation, keyboard-shortcut and download helpers (about 45 KB) belong to the lazy portal routes. renderer.initialBytes 1,655,428 -> 1,598,232 bytes (-57,196). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(performance): lower the initial-bytes baseline to 1,598,232 bytes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e45cd85a78 |
feat(settings): PIN-protected parental lock for categories (#285) (#1601)
* feat(settings): add PIN-protected parental lock for categories (#285) Locks are per category (Xtream category ids, Stalker genre ids, M3U group titles) and kept in one renderer lock store persisted to app_state / localStorage; `categories.locked` is the SQLite index re-stamped from it. While the lock is active the DB worker filters every content read, the PWA data source, the Stalker store and the M3U channel list filter in memory, and the enforcement service reloads the stores and steps off withheld selections. Settings → Parental lock sets the PIN (PBKDF2, never in Settings), the relock timeout and Lock now; lock toggles live in the Xtream/M3U management dialogs and a new Stalker lock dialog, all behind the PIN. Backups carry the locks per playlist entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): harden the parental lock after review - The M3U group dialog opens only after the PIN, like the Xtream and Stalker dialogs: it lists locked group names and can rewrite the locks. - Change PIN and Disable always verify the stored hash, even while the session is unlocked, so an app left unlocked cannot lose its lock. - Stalker paging judges progress on the raw portal page: withheld ids the list has not seen count as progress, a page made only of locked rows requests the next one itself, and the VOD total is reduced by withheld ids so the grid stops asking once every visible row is in. - Parental lock contract linked from the agent context map after the guidance reorganization; bridge helpers split out to stay under the file-size cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): guard locked categories on routes, PWA search and paging - Xtream and Stalker `:categoryId` routes carry a parental-lock guard: a locked category reached by URL prompts for the PIN and redirects to the section root on refusal (Electron row ids are mapped to provider ids). - PWA search filters withheld categories like the catalog reads. - Electron warm-cache detection confirms an empty, lock-filtered read with the unfiltered existence check instead of refetching from the provider. - A Stalker lock flip past page 1 drops withheld rows at once and restarts the list from page 1 instead of appending onto stale pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compile the PIN hashing helper in the Node backend build The web backend compiles the shared interfaces library without DOM typings, so the DOM-only `SubtleCrypto` / `BufferSource` names broke its Docker build. The helper now describes the WebCrypto surface it needs structurally and reaches it through `globalThis`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the remaining parental-lock gaps from review - Detail routes check the item's own category: a locked movie or series paired with an unlocked category id in the URL is still refused. - `requestUnlock()` awaits the settings load before it can answer "not active", so a slow startup cannot open a management dialog unguarded. - `SETTINGS_UPDATE` only persists the `parentalLockEnabled` mirror and releases the worker on switch-off; it no longer re-locks the worker on every ordinary settings save under a renderer that shows "unlocked". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): cover PWA cold navigation, Stalker search and the PWA lock editor - The Xtream detail guard hydrates the PWA session cache before judging an item on a cold navigation and fails closed when the catalog cannot place the item. - The dedicated Stalker search route filters withheld genres, re-fires on lock changes, judges paging on the raw page and restarts from page 1 on a lock flip. - The Xtream category dialog loads its lock candidates through the capability-selected data source; the PWA source now lists its raw categories with lock flags, so locks can be configured there too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): arm the relock timer on enable and harden Stalker search relock - The idle timer follows the unlocked transition instead of `active`, so the session that just enabled the lock still locks itself later. - Stalker search closes an open detail whose genre became withheld on relock and advances by itself past pages made only of locked rows (only while they add ids the list has not seen). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close lock editors on relock and clear withheld details opened from All The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on unreadable settings and finish relock clean-up - Unreadable settings (IndexedDB load failure) left the feature switch at its default and announced "unlocked" to the main process. A stored PIN now stands in for the switch, and without one nothing is announced, so the worker keeps its mirrored locked default. - Lock applies run one at a time and abandon superseded results; the Electron data source keys its in-flight share by lock version so a relock can never reuse an unlock refresh's unfiltered rows. - The stored in-portal Xtream search is re-run on a lock change. - Stalker live/radio selections are judged by tv_genre_id, and both live layouts drop the playback of a channel whose category became withheld. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on an unreadable lock store and make lock writes reliable - A lock store that cannot be read is no longer treated as empty: while the lock is active every category is withheld (renderer predicates and set-based filters alike) until the PIN is entered or the store reads again, and writes are refused meanwhile so an empty in-memory store can never wipe the persisted locks. The lock set now lives in its own ParentalLockLockStore service. - The M3U group dialog's lock write is awaited and a failed save is reported in a snackbar instead of being silently dropped. - The Electron categories.locked re-stamp clears and re-locks inside one transaction, so a failed restamp keeps the previous index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): drop pre-relock Stalker search pages and fail closed on a corrupt lock store - A Stalker search page issued before a relock was filtered with the pre-relock withheld set and could still be applied after it; the staleness check now includes the parental lock version. - A lock store payload that does not parse or is not an object is a failed read (everything withheld until it reads again), no longer an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps - The window before the initial lock store read settles now withholds everything, like an unreadable store: settings can report the feature as on before the locks are known. - The store commits before the SQLite index re-stamp; a failed re-stamp now rolls the store back, a failed rollback re-stamps on the next access, and every launch re-derives the index from the store. - Xtream category/content reloads fail closed: a rejected reload empties the affected lists (content types drop back to idle) instead of keeping rows read under the previous lock state. - Enabling/disabling the feature persists through one guarded path that undoes the in-memory switch and skips the Electron mirror on a failed settings write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(xtream): move the parental-lock reload specs beside the content spec The content feature spec sits at the 1200-line spec cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the startup lock-index reconciliation and withhold genre-less rows when failing closed - The lock store is readable only once the SQLite index has been re-derived from it, and a re-stamp that keeps failing keeps the session fail-closed, so catalog reads can never serve rows stamped unlocked by a stale index. - While everything is withheld, Stalker rows without a genre are withheld as well (the store filter and the renderer predicate). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries - The Electron mirror of the feature switch is awaited; a mirror that cannot be written undoes the settings write, so a reload never starts from a mirror that disagrees with the persisted switch. - A failed multi-type re-stamp rolls the store back AND re-stamps every touched type from it, since earlier types may already carry the new locks; a failed rollback keeps the playlist stale (fail-closed). - A persisted lock store whose nested entries are not what writeLocks produces is a failed read, not an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save - A relock now fails closed immediately: the selected detail is stepped off against the lock store, the catalog lists and stored search results are emptied, and the filtered reloads publish only while the captured lock version is still current. - Backups carry M3U lock titles verbatim (exact dedup), since the locks match group titles exactly. - A relock-timeout write that fails reverts the in-memory value and shows the settings save-failure snackbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store - A write that removes a playlist's last lock clears the SQLite index first and drops the store key afterwards, so an interruption between the two can only leave a state the startup reconcile repairs toward locked. - A PIN hash read failure is distinct from an absent PIN: the session stays locked and every PIN-protected step re-reads it first. - Backup export awaits parental lock initialization and refuses to run while the lock store is not readable, since an absent lock field means "no opinion" on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read - ElectronXtreamDataSource serves no categories, content or search hits while the lock store withholds everything; its SQLite index may still carry a stale stamp. - setupPin decides whether to persist the switch from the settings value before the PIN is stored, since enabled follows hasPin while the switch is unknown. - A lock store recovered by a later read marks its playlists stale so the index is re-derived, a persisted entry must carry all three lists, and a stale Stalker search page is dropped before touching the withheld-id bookkeeping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep unlocked category routes reachable and defer a relock reload that overtakes the initial hydration - The Xtream category guard no longer runs the item check on category-only routes (Number(null) is 0), which prompted for the PIN on every unlocked VOD and series category while the lock was active. - A lock change during the initial Xtream hydration withholds the rows the hydration publishes and runs the filtered reload once it has settled, on every path that marks the content initialized. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): resolve hidden live categories before relocking playback and reload categories in the deferred hydration path - The Xtream live layout resolves a playing channel's category through the unfiltered rows when the visible list lacks it (search can play a hidden category's channel); until that lookup lands the category is unknown and a relock stops the channel. - A relock that overtakes the initial hydration now withholds the category publications too and reloads categories with the content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): step off the M3U channel and Stalker selection before awaiting the Xtream relock reload The Xtream store stays populated after leaving that portal, so its reload runs on every apply; a locked M3U channel no longer keeps playing behind a slow database or provider read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries - The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity - A hidden-category lookup that lands after a later playback (same provider id, another playlist) no longer overwrites the newer channel's category; resolutions are generation- and playlist-checked. - Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id or the row's cmd/name, so id-less rows no longer collapse onto one key and stall paging past locked pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the Electron cached category/content reads while locks are unknown The warm-route hydration reads the cache directly; it now returns nothing while the lock store withholds everything, like the live reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps - clearSearchResults() advances the search request version, so a search issued under the previous lock state cannot republish what a relock just cleared. - A lock write publishes its store revision only once every touched type is stamped, so a reload triggered by it cannot read a later type through its old stamps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire a resolving Stalker live playback when the session relocks The embedded player defers selecting the channel until its stream resolves, so the enforcement service's cleared selection could not retire the request; it now carries the lock version it was issued under and is dropped when a relock happened meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(settings): one lock entry point per rail plus a right-click Lock/Unlock - Stalker's dedicated lock button becomes the same "Manage categories" (tune) button the Xtream rail has; it opens the lock-only dialog, so every portal type shares one entry point and the rail header keeps three actions. - Right-clicking a category (Xtream, Stalker) or an M3U group offers a single-row Lock / Unlock through the shared CategoryLockMenuComponent, behind the same PIN gate and lock store as the dialog. - The settings hint explains where locks are set; group lock strings added to all locales (ru/de translated). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): serialize lock-store writes and drop a deleted playlist's locks - Lock-store mutations run through one write queue: each rewrites the whole persisted store, so overlapping edits could otherwise snapshot the same store and the later write would drop the earlier edit. - Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP hook; "Remove all playlists" clears the lock store once the deletion has succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): apply single-row lock toggles inside the lock store's write queue The right-click Lock/Unlock (portal categories and M3U groups) built the new list before entering the queue, so two quick toggles shared one snapshot and the second dropped the first. Lock writes now accept an edit of the current list, evaluated inside the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed settings read before any parental-lock settings write updateSettings writes the whole settings object, which after a failed startup read is the defaults; enabling the lock or changing the relock timeout then replaced the user's persisted preferences. The read is retried first and the write refused while settings stay unreadable. The settings writes move to parental-lock-settings-writer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): show the locked-groups row on the M3U rail and roll the relock timeout back to the recovered value - The M3U groups rail now renders the same "N locked · Enter PIN to show" row as the portal category rail, so locked groups no longer vanish without an in-context unlock. - A failed relock-timeout write rolls back to the value read after the settings retry, not to the pre-retry default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed clear-all of the lock store and keep restored new playlists free of stale locks A lock-store clear that failed after "Remove all playlists" only logged, so a later restore reusing a playlist id could inherit the deleted playlist's locks. The in-memory store now empties at once and the persisted clear is retried on the next access; a restore that creates a playlist starts it from empty locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check - The idle relock no longer interrupts a playing radio station: playing <audio> counts as activity, like video. - A restore retries a failed lock-store read before checking a reused id for stale locks, and aborts while the store stays unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked - A relock during a page-1 Stalker search now filters the rows already on screen at once, so old unlocked results are not clickable while the replacement page is pending. - When every M3U group is locked the groups rail still renders, with its "N locked · Enter PIN to show" row, instead of the plain empty state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(settings): keep the PIN dialog and the Stalker enforcement step off the initial path Master (#1712) moved the UI component barrel and the Stalker data layer out of main.js and tightened the initial budget to 2 MB. The parental-lock prompt imported the PIN dialog through the ui/components barrel and the enforcement service injected the Stalker store at startup, which pulled both back in (2.55 MB, over budget). The PIN dialog now loads through a local lazy file on the first prompt, and the Stalker step loads only while a Stalker route is open: initial total 1.65 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping - Removing a playlist's last lock clears the SQLite index first; if the clear or the store write then fails, the index is re-stamped from the previous locks at once. Title matching and multi-source discovery query the worker directly and trust the index, so the stale flag alone did not protect them. - A rollback publishes its store revision only after every type is re-stamped, so a reload cannot read a later type through the attempted stamps. - docs: restore the index rules the earlier surfaces rewrite dropped from the contract, now in the Lock store lifetime section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the M3U groups view when every group is locked With every group locked the filtered channel list is empty, so the container showed its generic empty state and the groups rail's "N locked · Enter PIN to show" row never appeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the lazy Stalker enforcement step cannot load A rejected chunk (e.g. a stale PWA page after a deployment) escaped applyStalker(), so a locked Stalker selection kept playing after a relock and the Xtream step was skipped. The step now leaves the Stalker route on a load failure, which clears the selection and stops playback, and the Xtream step still runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): defer a stale Xtream hydration as soon as the catalog is withheld On Electron a relock that overtook the initial content hydration waited for the category reload before the content reload set the deferral flag; the older unlocked hydration could publish its streams in that window. withholdCatalog() now sets the flag itself, before anything is awaited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore backup locks after the Xtream merge and snapshot the Stalker lock dialog's categories - A backup restore now writes the parental locks last, so a failed Xtream merge leaves the playlist's previous locks in place instead of the backup's possibly smaller set. - The Stalker lock dialog snapshots the category list before its lazy import and opens only if the route is unchanged, so another portal's categories can never be saved under this playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on relock ahead of the apply queue and judge Xtream selections by the lock store - On relock the synchronous fail-closed steps (M3U channel, Stalker selection, the locked Xtream detail, catalog lists, stored search) run immediately instead of queueing behind an earlier apply that may still wait on a slow or hung read. - The post-reload Xtream checks decide by the lock store through the unfiltered category rows rather than by absence from the reloaded list, which also omits merely hidden categories; unreadable rows fail closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): require the PIN for lock-bearing backup restores and fail closed during index re-stamps - A backup carrying lock lists replaces the matching playlists' locks, possibly with an emptier set; the import now asks for the PIN (after the file was chosen) and aborts when it is refused. - While a write re-stamps the SQLite index the playlist counts as stale, so a relock inside that window reloads fail-closed instead of through the old stamps. The internal store write now needs only a readable store, so a rollback can still land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop parental-lock Xtream reloads once the playlist is switched A reload issued for playlist A no longer publishes into the shared Xtream store after the user opened playlist B: the store's reloads guard on the playlist they read for, and the enforcement apply retires its search refresh and selection checks on a playlist switch as on a newer lock version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-ask the PIN before a relocked backup merge and keep the PIN cooldown across prompts A backup merge now asks for the PIN again right before it replaces a playlist's locks when the app relocked during the import, instead of relying on the answer given at the start. The wrong-PIN count and the 30-second pause move from the dialog into the lock service, so dismissing and reopening the prompt no longer resets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): refuse lock removals that commit after a relock and keep the index stale until the store write lands Lock edits that take a lock away now commit only while the session is unlocked, checked inside the write queue at commit time, so an editor save still in flight (or queued) when the app relocks cannot remove locks. Adding locks stays allowed. The Xtream category dialog drops its lock draft after a relock, and a backup restore re-asks the PIN only when it would remove a lock. Clearing a playlist's last lock keeps its index stale until the store write has landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): clear an Xtream detail from a hidden category synchronously on relock The synchronous relock step now clears a selected Xtream item whose category the visible category list cannot place (a manually hidden category opened through search), instead of leaving it usable until the awaited reloads and lookup finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the Electron bridge lacks the parental lock worker filter A new runtime capability requires the lock-state and index-stamping IPC. When Electron reads Xtream through the SQLite worker without it (a partial or older preload), the locked session withholds every category instead of trusting a worker that never learned the lock state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-check lock removals at their durable commit points A lock removal that passed the unlocked check before its write is asked again right after the store write and, for Xtream, after the index stamps. A relock in between writes the previous store back or rolls the stamps back before anything is published. The stale-index bookkeeping, index stamping and store merge move into helpers to keep the lock store within the file size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed revert of a refused lock removal and fail closed meanwhile When writing the previous store back after a relock-refused removal fails, the lock store now keeps a pending rewrite, is not readable (the locked session withholds everything) and rewrites the persisted store from memory on the next access, so a restart cannot load the removal. A failed "Remove all playlists" clear shares the same retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): authorize lock removals when issued and close genre-less Stalker details in fail-closed mode A lock removal is now authorized right before its first write is issued; a relock that lands after that is ordered after the write, which completes. This drops the post-write rollback, whose own failure could leave the persisted store diverged from memory across a restart. The Stalker search closes a detail without a genre on relock while every category is withheld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the previous locks when an Xtream rollback write fails When an Xtream lock edit's re-stamp fails and the rollback store write fails too, memory now goes back to the previous locks and a pending rewrite persists them on the next store access before the index is re-stamped, so the failed edit cannot take effect through that re-stamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stalker): cover page-one rows leaving the screen on relock while the reload hangs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): offer the portal unlock row in fail-closed mode When the lock store cannot be read every portal category is withheld but no locked ids are known, so the rail showed no "Enter PIN to show" row. It now shows the row without a count in that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed for direct worker title lookups and capture the Stalker lock dialog context before the PIN Catalog title matching and multi-source discovery query the SQLite worker directly; they now return nothing while the parental lock withholds everything (unreadable store or a bridge without the worker filter). The Stalker lock dialog captures its playlist, provider and section before the PIN prompt and re-checks them after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retire multi-source alternatives on a lock change and keep reconcile off in-flight stamps The VOD multi-source host keys its discovery session to the parental lock version: a lock change drops the discovered sources, retires discoveries and switches in flight, and rediscovers through the worker's new lock state. Stale-index entries of a write still stamping are no longer retried by a concurrent reconcile, which could re-stamp from a store the write had not committed yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN - A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the relock timeout behind the PIN and bind M3U group lock toggles to their playlist A locked session can no longer change the relock timeout: the Settings selector is disabled until the PIN is entered and the service refuses the change while locked. An M3U right-click lock toggle now captures its playlist before the PIN prompt and is saved only if that playlist is still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): reset a locked M3U channel however it becomes active The enforcement service now checks the active M3U channel whenever it changes while locked, so numeric zapping, next/previous and remote commands, which select from the full channel list, cannot start a channel of a locked group. Numeric zapping also skips such a channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): bind the M3U group management result to its playlist The groups view captures the playlist before the PIN prompt and drops the management dialog's hidden and locked group lists once another playlist is open, so they cannot be saved under that playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(parental-lock): record the accepted restart case of a failed rollback write Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): build bulk lock drafts only from a readable lock store The Xtream and M3U management dialogs offer lock toggles, and the Stalker lock dialog opens, only once the lock store has been read. A draft built from the empty fail-closed snapshot would otherwise replace the real locks with nothing on Save if storage recovered in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the parental lock switch on the saved state and roll back to the recovered value The Settings switch snaps back to the saved state when clicked and follows it once the PIN action succeeds, so a cancelled or refused PIN no longer leaves it showing the opposite state. A failed switch write is undone to the value read after the settings retry instead of the hard-coded inverse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): match noncanonical PWA Xtream category ids against their locks The PWA data source compared raw provider category ids such as "009" with locks stored as numbers, so such a category stayed visible while locked. Both sides are now compared in canonical numeric form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): close the M3U group editor on any relock The group management dialog lists every group name, locked ones included, even when it opened without lock toggles (unreadable lock store). It now closes on any relock, and the groups view re-checks the lock state before opening it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea51cae3db |
feat(settings): search settings from the header and the command palette (#1714)
* feat(settings): search settings from the header and the command palette The header search on the Settings page was shown but disabled. It now searches a shared index of all 56 settings rows by translated title, description and English synonyms, replaces the section page with ranked results, and opens a result by scrolling to, focusing and briefly highlighting its row. Enter opens the best match, and the section navigation shows per-section match counts. The command palette gains a "Settings" group that lists the best six matches for a non-empty query, so any setting is one Ctrl/Cmd+K away. Rows hidden by the current form state fall back to the control that reveals them; rows the runtime cannot render are never returned. The index ships through a new @iptvnator/workspace/shell/util/settings-search sub-entrypoint so it stays out of the eager bundle, and a registry spec keeps it in step with the section templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): let search reveals win over pending input and gate embedded MPV rows - A reveal (result click, command palette, Enter) now cancels a search keystroke still waiting for its debounce, so its q navigation can no longer supersede the reveal and leave the results open. - Embedded MPV extra options and auto-reconnect require a lazily probed embedded MPV capability; frame copy also needs frameCopyAvailable, so search never offers a row the settings page cannot render. - Keyboard users keep a focus-visible ring on the revealed row after the highlight fades. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): wait for palette probes without Promise.allSettled The web tsconfig lib predates Promise.allSettled; use Promise.all over rejection-safe probes instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8ebb7e3424 |
perf(ci): run Tier A coverage concurrently with isolatedModules ts-jest (#1701)
Tier A coverage runs projects a few at a time (largest first, bounded Jest workers, buffered output, fail-fast kept) and ts-jest transpiles with isolatedModules instead of type-checking per process; five type re-exports become export type, two decorated inputs use import type. Unit Tests and Typechecks job: 26 min -> 9 min (Tier A step 23 min -> 6.5 min). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
eb61d37dc3 |
docs(tmdb): replace catalog titles in matching examples with stand-ins (#1658)
* docs(tmdb): replace catalog titles in matching examples with stand-ins The Cyrillic and Arabic examples that document title folding were taken straight from a user's own portal catalog while the folding bugs were being diagnosed, and they spread from there into comments, fixtures, assertions and the architecture doc. A public repository is not the place for someone's viewing inventory, and the TMDB ids pinned alongside them identify the exact shows. Swap in stand-ins that reproduce the property under test rather than the title: a word whose "й" folds away, one whose "ё" does, a two-word title carrying a season marker, an Arabic phrase whose initial hamza decomposes into a separate word. Every replacement was run through the real `normalizeTitle` and `cleanTitleForSearch` before being written, so the folded keys, the wire queries and the cache lookup keys are the same shape as before — "Лейка" and "Леика" still meet on one key while staying two different searches, and "AR| أمثلة تجريبية" still keys as a hamza split into a space. Real TMDB ids become synthetic ones. One channel fixture that read as a film title becomes a plain channel name. Deliberately left alone: the leading-tag rule's "Akira | 1988" / "Момо | Momo" examples in `title-normalization.util.ts`. Those are not an inventory — they are the evidence for a regex decision measured over 1.27M catalog titles, and inventing replacements would document a measurement that never happened. No release note: comments, fixtures and docs only, with no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(tmdb): label the folding stand-ins as illustrative, not as history Review caught that the substitution left synthetic titles inside sentences that assert observed fact: the architecture doc claimed a particular folded query "returns zero results while `Лейка` returns the show" and named the stand-ins as the titles that were searched, missed and negatively cached, and several comments read the same way. The titles never existed, so the reading is wrong in the one direction that matters — a future reader debugging a regression would take them for production evidence. Keep the claim that is actually established, which is a class-level one: under the old single-form design every Cyrillic title carrying "й"/"ё" and every Arabic title carrying a hamza form was searched folded, missed, and cached as missing for the negative TTL. Present the strings themselves as illustrative stand-ins chosen to fold the same way. The verified invariant — what NFD does to those letters, and what the two tiers therefore produce — is unchanged and still assertable, because it is a property of the text rather than of any title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(tmdb): finish the sweep — a missed cache key and a last observed-fact claim Two spots the first pass left behind: - `tmdb-search-cache-cleanup.spec.ts` kept a catalog-derived lookup key verbatim. Only the TMDB id beside it had been swapped, because the sweep matched the title in its display casing and the key stores it lowercased — so one item of the inventory stayed in the repository, twice. - `SearchTitleVariant`'s doc comment still asserted that one specific folded string finds nothing on TMDB. State the mechanism instead: folding rewrites the letters the search matches on, so the folded form finds nothing there. `content-search.util.sqlite.spec.ts` gets a channel fixture that no longer reads as a film title. `search-text-fold.util.spec.ts` is deliberately left alone. Its "Ёлки" is a common noun sitting in a Unicode corpus beside "Amélie", "İnşaat" and "Ά", not an inventory entry — and its rows are precomposed/decomposed PAIRS (U+0401 against U+0415 U+0308). A textual substitution rewrites only the composed half and silently turns the pair into two different words; doing it failed that spec, which is what a fixture encoding an invisible property is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(search): finish the fold fixtures by rewriting both halves of the pair The last two occurrences lived in the Unicode fold corpus, which an earlier attempt left alone after a textual substitution failed the spec. The reason it failed is the point: one row is a precomposed/decomposed PAIR — U+0401 against U+0415 U+0308 — asserting that both spellings fold to one string. Replacing the visible text rewrites only the composed half and silently turns the pair into two different words, which is not a thing a reader or a regex can see. Rewrite both halves by code point instead, keeping the decomposed half decomposed: U+0415 U+0308 + the new stem. Verified by decoding every quoted string in the file afterwards, and the spec passes (534/534). A repository-wide sweep now finds no occurrence of the replaced titles in either normalization form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
42b41ebabc |
fix(epg): advance the Xtream channel list's current programme as time passes (#1647)
The "current programme" under each Xtream Live TV row was written only on
scroll-into-view, on a new EPG result, or on an EPG offset change. Nothing
re-evaluated it as wall-clock time passed, so once a programme ended the row
stayed on it until the category was left and re-entered. The progress bar
under the row never moved either.
The rows on screen now re-check themselves once a minute. A programme still on
air only has its progress bar advanced, at no request cost; once it ends the
row is re-picked, and only a programme on air or upcoming may replace it, so a
guide that has run out can never walk the row backwards. An exhausted cache is
dropped and refetched at most once per cache lifetime per channel, while an
empty answer from the provider is left alone. What is on screen stays there
until a replacement arrives, so a refreshing row never blanks out.
A programme occupies [start, stop) in every comparison, the visible slice is
read from the viewport rather than remembered, and the two services behind
this are root-provided because a live layout mounts the channel list more than
once over a single EPG queue: EpgRefillLimiter is the floor on refetching an
exhausted guide, keyed by playlist since stream ids are provider-local, and
EpgRefreshCoordinator owns the one timer and merges every mounted list's
request, because the queue is latest-wins and separate timers would cancel
each other on exactly the boundaries that matter.
Contract: docs/architecture/m3u-playlist-module.md ("Xtream channel-row
programme refresh").
Fixes #767. Supersedes #1610.
Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f80a21beef |
feat(collections): open a Stalker live favorite inside its portal (#1639)
Live channels in Favorites and Recently viewed now offer "Open in <playlist>" for Stalker portals, the counterpart of the VOD "View in portal" action. The chip in the programme panel and the channel's context menu jump to the channel inside its portal's Live TV, with its genre selected and the channel playing. Radio stays hidden: it is a separate legacy-paged section with no open-on-arrival contract. The handoff refuses to guess. The store records which genre the rendered ITV list belongs to, so the deferred play waits for a list that can actually serve the channel instead of inferring it from array identity. An id claimed by one channel as its `id` and another as its `stream_id` is ambiguous, so neither is played and the user still lands in the right genre. The handoff is abandoned when the user changes genre, search, portal or section first. Two pre-existing defects surfaced during review and are fixed here: - A blank provider id shadowed a valid one. `a ?? b` keeps an empty string, so a channel with a blank `stream_id` was stored with no identity at all and could not be selected, played or found again. Six writers had that shape and three private copies of the skip-the-blank rule; all now go through one `firstNonBlankStalkerId` helper. - Route-session readiness could publish before the store held the portal's row. The constructor starts one sync and the first navigation starts another, and the second skipped the bootstrap because the playlist id was claimed before the awaits that install it. Arrivals are now serialized, the id is claimed only after the store write resolves, and only the newest sync publishes readiness. Both fixes carry regression tests that fail on the old behavior. |
||
|
|
4e575a810e |
feat(dashboard): say where you left off instead of which provider it came from (#1646)
Every dashboard title carried a "Xtream · Series" / "Stalker · Movie" subtitle. Provider kind and content kind are the app's own taxonomy, not a property of the title, and they are identical on every card in a rail — so the one line that could tell two cards apart said nothing. The hero now shows the source name alone, through `playlistDisplayLabel` (a stored playlist name is routinely the pasted URL with credentials, or a MAC). Continue Watching cards show what actually varies: the "S1·E5" chip plus "12 min left". Favorites keep the title alone, Recently Added keeps the source name, and a meta row with nothing in it is no longer rendered. Stalker shows filed under Movies had no badge, no progress and no resume. An embedded-VOD row announces its episodes through a `series[]` array and carries no `is_series` flag, so `extractStalkerItemType` reports `movie` on purpose — the item must keep routing to the VOD catalog — while its progress is a set of episode positions keyed by the parent id, which the dashboard was looking up as a single `vod` row and never finding. Split the two questions: `PortalActivityItem.watch_kind` records the progress model when it differs from the routing type, and every reader that has to choose goes through `resolvePortalActivityWatchKind` instead of `type`. That makes the resume handoff reachable for Stalker, so wire it through `STALKER_SERIES_RESUME_TARGET`: consumed once after the series positions are read, hydrating a lazy Ministra season first with a bounded two-attempt retry, and playing nothing at all when the position read failed rather than restarting the episode from zero. Also fixes the global-recent route template, which bound `[seriesResume]` only on its Xtream branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b30c783e85 |
fix(search): locale-invariant Turkish case folding in every search path (#1640)
Turkish upper and lower case queries now return the same results everywhere a title can be searched. Lower-casing the dotted capital "İ" (U+0130) leaves a combining dot behind, so "İnş" and "inş" reached different search arms and different results. - Case folding is locale-invariant: `toLowerCase()`, never `toLocaleLowerCase()`, which under a Turkish or Azeri OS locale maps ASCII "I" to the dotless "ı". - The Electron content search composes to NFC and drops the leftover combining marks before tokenizing, and its LIKE/GLOB pattern builders additionally spell the `'tr'`-locale İ forms, since SQLite LIKE folds only ASCII. - A shared `foldSearchText` covers every in-memory filter: channel lists, the Xtream and Stalker catalogs, category filters, collections, the EPG guide, the command palette, sources, the playlist switcher and the download lists. - Composing before the strip keeps canonically equivalent spellings equal while the fold stays accent-sensitive; the Turkish I/ı pair is deliberately left alone, as the FTS index does not fold it either. Covered by a SQLite-backed spec over the real trigram index plus regression cases in the affected renderer specs. Closes #609. Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
975b1f7f74 |
refactor(collections): split the unified collection page and live tab (#1642)
* refactor(collections): split the unified collection page and live tab Both files sat far above the workspace's 400-line hard maximum and were only green because `tools/eslint/max-lines-baseline.mjs` exempted them. No behavior change: every template binding, public signal and handler the components exposed still resolves the same way, and the two baseline entries are gone. `unified-collection-page.component.ts` (966 → 398 lines) keeps the view surface and delegates: - `unified-collection-data.service.ts` — component-provided; owns the rows, the favorite uid set, the loading/reload indicators and every mutation, plus `loadedRequest` (the PR #1636 invariant, now read through one `mutationRequest` computed instead of two ad-hoc fallbacks) - `unified-collection-load.ts` — the reload key and the load effect - `unified-collection-scope.ts` — the This-playlist/All-playlists toggle - `unified-collection-content-type.ts` — the Live/Movies/Series tab - `unified-collection-history.ts` — the `window.history.state` view entry - `unified-collection-detail-state.ts` / `-detail-navigation.ts` — the inline detail and where an item this route cannot render goes instead - `unified-collection-clear-action.ts`, `-labels.ts`, `-favorites-sort.ts` `unified-live-tab.component.ts` (1051 → 426 lines) continues the pattern its siblings already established: - `unified-live-selection.ts` (+ `-selection-generation.ts`) — activating a row while the mounted player stays alive - `unified-live-selection-view.ts` — what the selection implies about the source and the player surface - `unified-live-epg-view.ts`, `unified-live-epg-map.ts` — the EPG panel and the rail's now-playing map - `unified-live-catchup.ts` — the timeshift override - `unified-live-recording-metadata.ts`, `unified-live-channel-rows.ts` Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(collections): move the collection data service into data-access Codex review on PR #1642: `UnifiedCollectionDataService` loads and persists favorites/recent rows, which CLAUDE.md places in `@iptvnator/portal/shared/data-access`, not in the `type:ui` project that renders them — and the dialog-scoped `SourceCleanupService` already sets that precedent for a component-provided stateful collection service. It was also the only non-root `@Injectable()` in `portal-shared-ui`. `collection-reload-indicator.ts` moves with it: `type:data-access` may not depend on `type:ui`, and the indicator is the service's own loading state machine rather than a view. Both are exported from the data-access collection barrel, so the boundary is now lint-enforced instead of conventional. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
15d964ef88 |
fix(collections): non-destructive reload feedback for the scope switch (#1636)
Switching the unified Favorites/Recent page between "This playlist" and "All playlists" kept the old items on screen with no feedback until the new query resolved. The skeleton cannot be reused there: it unmounts a playing channel and drops focus from the toggle. Add a separate reload state that keeps content mounted: after a 180 ms grace period an indeterminate progress bar overlays the header separator, the content region carries aria-busy, and the grid or the live tab's channel rail dims — the player never dims. Fast IndexedDB/SQLite answers show nothing. Only the latest request settles the state. Also bind Clear and drag reorder to the request that loaded the rows still on screen (`loadedRequest`) instead of `effectiveScope()`, which moves ahead the moment the toggle is clicked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
033a08cad9 |
feat(collections): open a favorite or recent live channel inside its playlist (#1634)
* fix(xtream): keep a live-channel handoff alive until its playlist catalog is loaded Arriving at /workspace/xtreams/:id/live from another route with openXtreamLiveItemId in history state silently did nothing: the Xtream shell mounts the live layout after its session bootstrap, i.e. after the arrival's NavigationEnd, so the layout's NavigationEnd subscription never saw it. When the layout was reused instead (playlist switch), the shared store still held the previous playlist's catalog at NavigationEnd and the "not in liveStreams" verdict dropped the pending id. Read the state once at mount as well, carry openXtreamLivePlaylistId in the navigation state, and treat a miss as final only once currentPlaylist is the requested playlist and isContentInitialized is true. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(collections): open a favorite or recent live channel inside its playlist Live channels watched from Favorites / Recently viewed had no visible playlist and no way to jump there, unlike movies and series with "View in portal". Add the live counterpart: - getLiveCollectionPlaylistNavigation() resolves the channel inside its playlist (Xtream via the live layout's auto-open state, M3U via openM3uChannelUrl on the player's all view); Stalker resolves to null until its ITV layout gets an open-on-arrival contract, so nothing is shown there instead of landing on the section root. - app-open-in-playlist-chip, projected into the EPG timeline / list-view toolbar through a new [epgToolbarAction] slot beside the channel name, visible in the collapsed state too. - "Open in <playlist>" entry in the channel row context menu, which also covers radio rows and rows that are not playing. Both label with playlistDisplayLabel and reuse PORTALS.VIEW_IN_PORTAL_TOOLTIP. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(xtream): only match an auto-open channel against the requested playlist's catalog Review finding (Greptile/Codex P1): the playlist check ran only on a miss, so a colliding provider-local xtream_id in the previous playlist's catalog was accepted, played the wrong channel and consumed the handoff. Check the playlist before consulting the catalog at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7790e68147 |
feat(portal): show each season's own poster beside the season tabs (#1628)
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.
Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.
The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
c016c73f86 |
feat(shell): zoom shortcuts on Windows and Linux (#1109) (#1623)
* feat(shell): zoom shortcuts on Windows and Linux (#1109) Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform. Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new synchronous, preload-local bridge method `adjustZoomLevel`, which steps the frame-bound temporary level through `webFrame.setZoomLevel` — never a main-process `webContents.setZoomLevel`, whose per-URL entry the app's `file://` path routing resets. Step and limits live in `libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the key before the application menu, and `preventDefault()` keeps the menu role from stepping a second time (Electron only performs the menu key equivalent in its unhandled-keyboard-event hook). Persistence is unchanged: the main process still reads the live level back on close, quit and reload. The zoom E2E now drives the real shortcuts (in, out, numpad, reset). Help dialog entries added and translated for all locales; contract updated in docs/architecture/workspace-shell.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(shell): never step a stored out-of-range zoom level against the request A level persisted before the shortcuts existed (the macOS menu roles never clamped, and the store restores any finite level) was clamped BEFORE the step, so the first zoom-in from level 7 rendered smaller. Step from the raw level instead: a press further out leaves an out-of-range level where it is, a press back in lands on the limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(shell): state the zoom bridge's return contract precisely `adjustZoomLevel` steps by `stepZoomLevel`'s rules; a stored out-of-range level is never moved against the request, so the returned level is not itself guaranteed to be within `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(release): add a release note for the zoom shortcuts The Release note gate requires an added `.changes/*.md` for runtime changes; the shortcuts are a user-visible feature of their own, so they get their own note and the persistence note stays about persistence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c76a901e8d |
fix(ui): keep one Back arrow on detail pages (#1627)
Movie and series detail pages showed two arrow_back controls while the inline player was open: the shell's sticky arrow (added in #1576 so Back survives scrolling) meant "Close player" in watch state, while the now-playing bar carried a second arrow that meant route-level Back — the same icon with two meanings, next to a "Close player" button that duplicated the first. The shell's sticky arrow is now route-level Back in browse and watch alike, and the bar carries no arrow of its own. Closing the player is the bar's "Close player" button and Escape, which still unwinds one level (close, then back). Hosts without a browse Back target (M3U, downloads) render no arrow in either state. Unit specs for the shell and the inline player cover the new contract; the Electron and web E2E helpers that pressed Back from watch are updated, and the M3U flow closes the player through the bar's button. Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note follow the change. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
fa8ce26991 |
feat(playback): fullscreen episode panel for series playback (#1620)
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen. - Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged. - Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request). - Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded. - Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7f724494b9 | feat(portal): mark movies as watched from the detail page (#1605) | ||
|
|
e9eca1c386 |
chore(deps): upgrade Angular to 22.1 and Nx to 23.2 (#1603)
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2 * fix(deps): complete Angular migrations after rebasing on master * fix(ci): use the Node pin for Windows runtime refresh * docs(deps): synchronize the workspace-shell Node requirements |
||
|
|
ef3f98d026 |
feat(portals): posters-only cover wall for movie and series grids (#1604)
* feat(portals): posters-only cover wall for movie and series grids Add `Settings.showCoverTitles` (Settings > General, default on). Turning it off drops the title row under VOD/series covers in catalog, favorites and recent grids and reveals the title as a bottom-gradient overlay on hover and keyboard focus, pinned open for items whose cover is missing or failed. `CoverTitlesService` is the single resolver: the opt-out AND a hover-capable pointer, so touch-only devices keep their titles. Live channel grids, search results, "recently added" rails and dashboard rails always keep labels. Catalog and collection cards become keyboard buttons (role, tabindex, aria-label, Enter/Space, focus ring) and poster alt text is the title. The default-on boolean coercion moves into `settings-opt-out.util.ts` because the settings store reached the max-lines limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep nested Remove key presses from activating the card Enter/Space on the content card's nested Remove button bubbled into the card's own key handlers: Enter opened the item before removing it and Space opened it while cancelling the removal. Only keys pressed on the card element itself now activate it. Regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while an in-section search filters the grid The posters-only wall exempts search results because they are identified by the name the user typed; the category grid's own in-section filter is the same case, so `app-grid-list` now keeps the title row while its `searchTerm` is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while the collection tab search is active The unified favorites/recent tab filters by its own search term, so its matches are identified by name like every other search result. The tab now opts its cards out of the posters-only wall while the term is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): move the card Remove control out of the button surface An interactive control nested inside a role="button" is an invalid accessibility structure. The content card's activation surface is now its own inner element and the Remove button a sibling positioned over the poster corner, labelled by its tooltip text. Spec asserts the control is never a descendant of the button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): draw the collection card focus ring where it is not clipped The card's overflow: hidden clipped an outline drawn on the inner activation surface on every edge, so keyboard users saw no focus indication. The ring now sits on the outer card via :has(> .content-card__activation:focus-visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): detect any hover-capable pointer for the posters-only wall `hover` describes only the primary pointer, so a touch-first tablet with a mouse or hover-capable stylus attached lost the wall. The resolver now reads `(any-hover: hover)`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
17b8aa309d | fix(m3u): restore DASH playback from favorites and recently viewed (#1597) | ||
|
|
bd848aaad6 | feat(playlist): clean up selected inactive desktop sources (#1593) (#1596) | ||
|
|
62655a8b5d | feat(playlist): show desktop health indicators for network sources (#1592) | ||
|
|
fff022afe4 | fix(ui): keep detail back navigation available while scrolling (#1576) | ||
|
|
bad8a0991e |
feat(downloads): download completed Xtream catch-up programmes as TS (#1572)
* feat(epg): copy catch-up programme URLs without changing playback * feat(downloads): save completed Xtream archive programmes as TS * fix(epg): let newer archive copy requests supersede pending work * fix(downloads): protect archive partials and independent submissions * fix(downloads): verify archive identity through finalization * fix(downloads): bound archive storage and capture cleanup entries * fix(downloads): preserve archive ownership across failure paths * fix(downloads): recover explicitly verified archive completions * fix(downloads): journal archive promotion before publishing files * fix(downloads): reset archive proof before an explicit restart * fix(downloads): preserve archive recovery ownership and interruption * fix(downloads): verify durable archive identity at resume open * fix(downloads): fence archive commands during completion commit * fix(downloads): persist archive ownership throughout its lifecycle * fix(downloads): protect archive removal and missing-file recovery * fix(downloads): journal private cleanup captures for recovery * fix(downloads): journal active archive cleanup before removal * fix(downloads): clean settled archives before deleting stale rows * fix(downloads): preserve archive ownership on removal and resubmission * fix(downloads): recover proven archive completions before retry * fix(downloads): recover local archives before remote transfer checks * test(downloads): resolve archive fixture from workspace root * fix(downloads): distinguish reused archive inodes by creation time * fix(downloads): bind fresh archive reservations to owned files * fix(downloads): clean reservations when ownership writes fail * fix(downloads): commit archive reservation and ownership atomically * fix(downloads): retain captures until replacement restoration succeeds * fix(downloads): require durable ownership before cleanup relocation * fix(downloads): preserve 64-bit archive file identities on Windows * refactor(release): keep capture fixture constants in their shared module * fix(downloads): preserve the last link of captured foreign files * fix(downloads): expose retained archive recovery files * fix(downloads): keep recovery instructions open while copying |
||
|
|
7f06690e72 |
feat(epg): copy catch-up programme URLs (#1569)
* feat(epg): copy catch-up programme URLs without changing playback * fix(epg): let newer archive copy requests supersede pending work |
||
|
|
97b0264dee |
fix(xtream): render catch-up start times in the panel timezone (#1563)
* fix(xtream): render catch-up start times in the panel timezone
The `{Y-m-d:H-M}` segment of an Xtream timeshift URL is read by the panel
with `strtotime()` in ITS timezone (`server_info.timezone`), never the
viewer's. The timezone was learned in memory only, by the store's
`checkPortalStatus()`, so the Favorites / Recent catch-up resolver — which
reads the STORED playlist row — always fell back to the viewer's local
clock and asked the panel for the wrong programme (#1562).
- Normalize the panel's clock once (`resolveXtreamServerTimezone`): an
ICU-resolvable name is kept, otherwise a `UTC±HH:MM` offset is derived
from the `time_now` / `timestamp_now` clock pair, so spellings such as
`UTC+3` no longer silently mean "local time".
- Persist it on the playlist row through `transformPlaylistMeta` (no-op
when unchanged) and project it back from the payload in
`DB_GET_PLAYLIST`, so both catch-up entry points and a restart see it.
- Format with `hourCycle: 'h23'` (server midnight is `00`, never `24`) and
read timestamp-less EPG `start`/`end` strings in the panel's clock.
- Mock: `tzoffset:tzoffset` scenario with an unusable timezone name and a
+03:00 clock pair; Electron e2e covers Live TV, Favorites, a restart into
Global favorites, and the clock-pair derivation at a UTC-3 viewer.
Closes #1562
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): guard the account-info answer by playlist identity and reject rolled-over dates
Review follow-ups (Greptile):
- A source switch while `get_account_info` is in flight no longer hands
playlist A's status or clock to playlist B: the store is patched only
while the asking playlist is still selected, the timezone is persisted
under the asking playlist's id regardless, and a late failure cannot mark
the newly selected playlist unavailable.
- `parseNaiveUtcMs` reads the constructed date back, so out-of-range panel
strings (`2026-13-01 25:00:00`) are rejected instead of silently rolling
over into a real instant.
- Document that a clock-derived fixed offset is a DST-less snapshot, refreshed
by every account-info check and only ever used for non-standard servers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): drop a panel clock that no longer belongs to the source
Review follow-ups (Codex + Greptile):
- A metadata update or DB_UPDATE_PLAYLIST that points the source at another
server drops the persisted `serverTimezone` (payload-only) until the next
account-info check, so Favorites / Recent cannot keep rendering the OLD
panel's clock; an update that supplies a clock keeps it.
- A late account-info answer is persisted only onto a row that still points
at the panel it came from — an edit that moved the source during the
request keeps the clock the edit flow dropped.
- The PWA data source and the route-session converter carry the persisted
timezone into the store playlist, so a later response without a usable
clock has a previous value to preserve.
- Mirror the catch-up timezone contract into AGENTS.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): drop the stale panel clock inside the UPDATE statement
Review follow-up (Codex): the database worker interleaves requests, so a
read-modify-write of the playlist payload could hand a concurrent upsert's
newer payload back to the past. The `serverTimezone` removal on a server
URL change is now one `CASE … json_remove(payload, '$.serverTimezone')`
expression inside the same UPDATE, guarded by `json_valid`; the spec runs
the real statement against Electron's SQLite on the actual `playlists`
table (moved, renamed, clock-less, malformed-payload and NULL-URL rows).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(xtream): split the server-clock primitives out of the timezone util
Review follow-up (Greptile): `xtream-server-timezone.util.ts` had grown past
the 300-line file guideline. The zone-agnostic wall-clock primitives (stored
forms, Intl parts, naive parsing) now live in `xtream-server-clock.util.ts`;
the timezone util keeps the Xtream policy and re-exports the public helpers,
so every import and the spec are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): offer the learned panel clock to storage on every check
Review follow-up (Codex): a transient storage failure left the clock in the
store but not on the row, and the next check compared the answer with the
in-memory value and never retried. The resolved timezone is now always
handed to `transformPlaylistMeta`, whose row-level equality check keeps the
common case a read without a write; a failed write is retried by the next
account-info check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): apply an account-info answer only to the panel it came from
Review follow-up (Codex): an in-place edit keeps the playlist id while
moving the source, so an answer already on the wire for the OLD panel
passed the id-only guard and patched the new panel's status and clock into
the store. One `answersFor(candidate, credentials)` predicate now gates the
store patch, the error path and the persisted-row transform alike.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): never report another panel's status for the selected playlist
Review follow-up (Greptile): callers gate content initialization on the
value `checkPortalStatus()` returns for whatever is selected NOW. When the
answer no longer describes the selected playlist (source switch or in-place
edit during the request), the store's own verdict about the current
selection is returned instead of the old panel's status — on success and on
failure alike.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): persist the panel clock with one conditional UPDATE
Review follow-up (Codex): `transformPlaylistMeta` reads the row and then
upserts it whole, while the Xtream edit dialog saves through
`DB_UPDATE_PLAYLIST` outside `PlaylistsService`'s queue and the database
worker interleaves requests — an edit landing between that read and the
upsert was silently undone.
Persistence now goes through `IXtreamDataSource.rememberServerTimezone`:
- Electron: new `DB_SET_PLAYLIST_SERVER_TIMEZONE` worker op — one UPDATE
that `json_set`s the payload only while the row still points at the
request's connection and does not already carry the value; a malformed
payload is never rewritten (CASE, not AND, so json_extract cannot run
before json_valid). Wired through the worker types, main handler,
preload, bridge interface, both IPC contract tables and
`DatabaseService.setXtreamPlaylistServerTimezone`.
- PWA: `transformPlaylistMeta`, whose read and write share one IndexedDB
readwrite cursor transaction, plus the localStorage copy.
The store no longer injects `PlaylistsService`; it offers the resolved clock
to the data source and keeps only its in-memory guards. Real-SQLite coverage
for the op (fresh / same / moved / NULL / malformed / missing rows),
delegation specs for both data sources, docs updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): keep the stored panel clock across clockless full upserts
Review follow-up (Codex): a `PlaylistsService` mutation that read the row
before `DB_SET_PLAYLIST_SERVER_TIMEZONE` landed and upserted afterwards
replaced the payload with its clockless snapshot. `DB_UPSERT_APP_PLAYLIST(S)`
now carry the STORED clock into a snapshot that has none while the row still
points at the same connection (`playlistConflictUpdate`, nested CASE so the
json_* readers never run on a malformed payload); a snapshot with its own
clock, or one that moves the source, wins as is. Real-SQLite coverage for
kept / moved / own-clock / batch rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(release): split capture-navigation under the max-lines cap
`tools/release/capture-navigation.ts` had grown to 567 counted lines, past
the 400-line rule, which failed `release-tools:lint` and — because the file
was not in the baseline — the max-lines baseline test on master and on
every PR branched from it. The 19 named setup actions are now grouped by
subject over one leaf module of shared page helpers:
- `capture-navigation-helpers.ts`: playlist-id registry, dialog handling,
navigation moves, `settleUi`
- `capture-navigation-setup-actions.ts`: add-playlist dialogs, settings
sections, remote control
- `capture-navigation-portal-actions.ts`: portal catalogs, live lists,
alternative sources (the two identical live-category flows share one
helper)
- `capture-navigation-download-actions.ts`: the download manager shots
- `capture-navigation.ts`: the `runAction` dispatcher, theme switching and
the re-exported API the seeding driver and the capture script import
Actions call their siblings directly instead of recursing through
`runAction`, so no module depends on the dispatcher. The action vocabulary
is unchanged (same 19 names, same waits and timeouts); every file is under
300 lines and the new modules are listed in the `release-tools` lint target.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(electron): move the panel-clock SQL into its own operations module
Review follow-up (Greptile): the timezone persistence, invalidation,
upsert-preservation and row projection had landed in
`playlist.operations.ts`, a baselined 1,000-line file. They now live in
`playlist-server-timezone.operations.ts` (155 lines) — the three SQL
shapes plus the payload projection — and the playlist operations compose
them; the baselined file shrinks by 107 lines. Behaviour and the
real-SQLite coverage are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
15c2ac1f39 |
feat(packaging): add AppManager discovery metadata to AppImages (#1559)
* feat(packaging): add AppManager discovery metadata to AppImages * test(xtream): restore live queue URL service mock * test(xtream): extract live layout component stubs |
||
|
|
7d1503fd31 |
feat(epg): rebuild the programme guide for M3U playlists (#1560)
* docs(epg): add programme guide redesign spec for the M3U host Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): add programme guide implementation plan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add window-scoped guide programme queries Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): harden guide query scoping, caps and row mapping - Scoped guide programme/coverage queries now include legacy (unsourced) rows via source_url IN (...) OR IS NULL OR '', mirroring EpgQueryService's legacy fallback. - getProgramsForChannels/getProgramCoverage build their result from the normalized, capped window.channelIds instead of the raw request, so a key cut by the cap is absent rather than [] — an invalid window now returns {}. Truncation logs counts only. - Split the 100-channel guide cap from a new 2000-key coverage cap, and cap sourceUrls at 50; normalizeGuideWindow takes the cap as a parameter and moved (with guideWindowOverlapSqlText) into epg-guide-window.util.ts. - Extracted shared row mapping (toEpgProgramFromRow/isValidEpgProgram) into epg-program-row.util.ts, used by both EpgQueryService and EpgGuideQueryService so invalid start/stop rows are dropped identically in both. - Added a real-SQLite-backed test for the overlap predicate's exact text, plus per-key array copies in the response. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): render the guide predicate in tests and document its scope Correct the guide query's JSDoc: it runs one query accepting the union of requested-source and unsourced legacy rows, unlike EpgQueryService's two-query scoped-then-legacy fallback. Replace the hand-maintained plain-SQL twin of the Drizzle overlap predicate with a rendered copy of the real predicate (SQLiteSyncDialect().sqlToQuery) in the spec, add a source-scoping case, and drop the now-redundant operator-sequence test. warnIfTruncated reuses uniqueTrimmedStrings and names which read (programme/coverage) was truncated. Rename epg-query.service.ts's local EpgProgramRow to EpgProgramSelectRow so it isn't confused with the shared EpgProgramRow type, and document getProgramCoverage like its sibling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): expose guide programme and coverage reads over the bridge Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): separate coverage chunk size in the guide plan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add guide source contract, day layout maths and preferences Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): key guide IPC answers by trimmed, present keys only Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): guide search hits carry a row id Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): make guide geometry DST-safe and tighten the contract Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): cache guide programmes per day with batched loading Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add guide keyboard navigation controller Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): make guide programme cache robust to first-run effects and coverage failures Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add the programme guide grid components Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add a Guide button to the timeline toolbar Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(m3u): adapt the playlist channel list to the guide contract Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): guard the guide's initial group scope and track language changes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(m3u): open the programme guide in place with a docked player Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): scope guide keys to the grid, clip the now-line and re-measure on resize Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(epg): remove the multi-EPG overlay and the channel-range IPC Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): document the programme guide and its release note Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * i18n(epg): translate the programme guide Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): let the guide own the keyboard and gate its entry points While the programme guide is open the docked player carries `data-player-shortcuts-suspended`, which `ControlsShortcuts` now honours alongside `[inert]` — the arrows moved the player's volume instead of the guide's row focus. The external-player strip loses its Collapse toggle (nothing to reveal, no preference to write), the header action and its palette command report `disabled` when the guide cannot open, the docked strip derives its programme from the active channel's own schedule instead of the retained NgRx value, switching playlists closes the guide, and the collapsed strip can reach 48 px on phones. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): keep the sidebar mounted while the guide is open Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the guide destroyed `app-channel-list-container`, whose `ngOnDestroy` dispatches `resetActiveChannel()`. That cleared the active channel, which unmounted the block hosting `app-epg-guide` and tripped the `!canOpenGuide()` effect into closing the guide again: the guide never appeared and the page dropped to "Please select a channel". The sidebar now stays mounted and is hidden with `.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read by assistive technology while the guide owns the layout. Hiding also preserves the channel list's scroll position across guide toggles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(e2e): cover the programme guide flow Imports a two-channel playlist with XMLTV, opens the guide from the timeline toolbar and asserts the row list, the "Only with EPG" filter, a channel switch that keeps the guide open, the hidden-but-mounted sidebar, and that the player element survives both the mode and channel switches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(epg): tidy guide docs, palette gating and the unbound output Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): match guide favorites by channel URL and skip re-activating the playing row Favorites are persisted by channel URL (FavoritesActions.updateFavorites), so the Favorites scope compared the wrong key; the id stays as a legacy fallback. A double-click arrives as click, click, dblclick and each activate restarts playback, so the guide now leaves the already-playing row alone and the commit path only closes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(epg): let the guide window predicate use the programme time index Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): stabilise guide row identity, seed the sidebar group and provide translations in every player fixture Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(epg): split the guide shell, add a roving focus model and offset-aware search times The shell component now owns rows, focus and the viewport only: the day, zoom, density, filters, clock and day geometry move to EpgGuideViewState, and every programme-dialog entry point to EpgGuideDialogController. Keyboard navigation is reachable by assistive technology: exactly one grid cell carries tabindex="0" (the focused cell, else the playing row's channel cell, else the first row's), the guide moves DOM focus with it after each handled key, a click hands the roving index to the clicked cell, and the viewport, rows and cells expose grid/row/gridcell roles. Search results were formatting raw provider instants, so they ignored the EPG display offset; they go through getProgramTimeMs like every other time the guide renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): make guide row ids collision-proof and gate the G shortcut Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): keep guide keys on the grid, reconcile focus with filtered rows and wrap the toolbar Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): describe guide row ids as scope-local Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown Search hits carry scope-local row ids, so a scope change drops them. Two playlist entries can share an id but not a stream, so the active row is matched by id + url before falling back to the id. A failed coverage query now rejects instead of answering an empty set, which the guide already treats as "coverage unknown" (every row stays visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): tell duplicate guide rows apart by group, keep G out of dialogs, use prototype-safe answers The store spreads the selected channel, so the active row is matched by id, url, group and name before widening; G no longer closes the guide from a dialog or menu; guide answers use null-prototype records so a key named __proto__ stays an own property. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): let coverage reject on lookup failures and compare whole entries for the active guide row EpgQueryService.getChannelMetadata swallowed database errors into {}, so the guide's coverage read could publish an empty set after a transient failure; the guide now uses the strict resolveChannelMetadata (getChannelMetadata is the fail-soft wrapper around it). The active guide row is matched on the whole channel entry (all fields except the reducer-rewritten epgParams) before widening to url and id, so copies that differ only in playback headers or logo are told apart. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): let the guide return catch-up to live and normalise programme-search rows The guide source contract gains an optional livePlayback signal: while the host plays a catch-up URL, the active row may be activated again, which is how the M3U host returns to live. EPG_DB_SEARCH_PROGRAMS now maps the raw snake_case rows to the EpgProgram shape the bridge promises (plus the joined channel name), so search hits resolve their channel and keep descriptions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): name search hits, keep guide coverage strict on mapping failures - Search results and the unresolved programme dialog show the channel's display name (playlist row name, else the XMLTV display name the search joined in) instead of the raw XMLTV id. - The guide coverage read resolves manual mappings through a strict variant that rejects on database failure, so a mapped channel can never be reported as uncovered and hidden by "Only with EPG". - Architecture doc describes the tiered active-row resolution. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): offer the Guide action in the list view too The EPG list view mirrors the timeline's input/output contract, but the Guide action was bound only in the timeline branch, so Settings → EPG → Guide view = List lost the in-panel entry point. The list toolbar now carries the same icon-only Guide button behind `guideAvailable`/`openGuide`, and the M3U host binds it in both branches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0a2373f192 |
feat(portals): fold live TV panels in nested levels with a category dropdown (#1556)
## Summary Live TV panels now fold from the outside in, in three nested levels, instead of one toggle that hid the categories rail and the channel list together: 1. **Categories + channels + player** (browse, unchanged). 2. **Channels + player** — a new `chevron_left` in the categories rail header hides only that rail. The channels header then turns its title into a **category dropdown** that opens the same shell panel as a popover (search, sort, counts, selection are one implementation), plus a `chevron_right` that brings the rail back. 3. **Player only** — the channels header chevron, as before. The floating restore handle and `Cmd/Ctrl+B` return to the level the user collapsed from, not always to level 1. Every level is restored as stored, per surface (`live-sidebar-state:<surface>`, from #1555): a hidden rail is discoverable through the workspace header toggle and the hidden-list empty state that #1555 added, so this PR no longer needs its original "player-only never restores" rule. The level `Cmd/Ctrl+B` comes back to is seeded from the restored level and kept for the session. ## Design notes - Nested levels rather than two independent booleans: "channels hidden, categories visible" makes no sense since a category click has to bring the channels back anyway. The model follows the outside-in collapse of three-pane apps (Mail, Slack, Plex). - The categories rail folds at level 2 **only while a category is selected**: the live root ("All Items" grid) has no channels header to host the way back, so folding there would strand the user. Level 3 folds it regardless, because the floating restore handle lives in the content area. - `LIVE_CATEGORIES_POPOVER` (`@iptvnator/portal/shared/util`) is the DI bridge: the workspace shell provides `WorkspaceLiveCategoriesPopoverService` (CDK overlay hosting `WorkspaceContextPanelComponent` in `presentation="popover"`), the Xtream and Stalker live layouts inject it optionally and keep their plain heading without a provider. - M3U and the unified live tab have no categories rail and treat level 2 like level 1; their code is untouched. ## Merged with #1555 (per-surface rail state) #1555 landed while this PR was open and reworked the same service: state per surface (`m3u` / `portal` / `collection`), a workspace header toggle, the hidden-list empty state, and the legacy shared key forgotten on startup. This PR keeps that model and layers the three levels onto the `portal` surface (`areCategoriesHiddenFor`, `hideCategories` / `showCategories` / `collapse` / `expand` per surface; `toggle(surface)` returns to the level the surface collapsed from). "Show playing channel" uses `expand('portal')` so it keeps a deliberately hidden categories rail folded, and the category sort preference moved to `PortalCategorySortStateService` so the popover copy of the context panel and the retained rail agree. ## Also fixed along the way - The channels header showed "Channels" instead of the category name: provider category ids are strings, the selection is numeric. Compared via `String()` now. - A collapsed context panel left a 22px padding strip beside the channels rail. - The panel toggle labels said "Hide channels list" while also hiding categories; labels and tooltips are honest now (8 new i18n keys, all 18 locales). Docs: `docs/architecture/iptvnator-ui-guidelines.md` ("Collapsible Live Sidebar" rewritten), `docs/architecture/workspace-shell.md`. Release note: `.changes/portals-live-panel-collapse-levels.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0dcfba7045 |
fix(live-tv): keep a hidden channel list discoverable and scoped per surface (#1555)
* fix(live-tv): keep a hidden channel list discoverable and scoped per surface The second report in #1458 ("all channels disappear after clearing the playback history, reset does not bring them back") was not data loss: the history write never touches playlist items. The reporter's screenshot shows a collapsed channel rail, a state persisted under one localStorage key shared by the M3U player, the Xtream/Stalker live layouts and the favorites/recent live tab. It survived restart, "Remove all playlists" and re-import, and the only way back was a 32px chevron or Ctrl/Cmd+B. - LiveLayoutSidebarStateService keeps the state per surface (m3u / portal / collection) under live-sidebar-state:<surface>; the M3U player now goes through the service instead of its own signal. The legacy shared key is forgotten on startup and never read, so the update itself restores the list for everyone who got stuck. - The workspace header renders a view_sidebar toggle on every route that renders its own rail (M3U all/groups, Xtream live, Stalker itv/radio), so the control exists in both states instead of disappearing with the rail. Collection pages keep their own toggle beside the content switch. - While the rail is collapsed and nothing plays, every live host shows app-channel-list-hidden-state (title, shortcut hint, full-size "Show channels list" button) instead of asking to pick from a list that is not on screen. app-portal-empty-state gained optional hint/action inputs. - New LAYOUT.CHANNELS_LIST_HIDDEN(_HINT) strings in en plus 18 locales. Tests: service, empty-state, hidden-state and header component specs, a separate video-player-sidebar spec (the main M3U spec sits at the test line budget), and an Electron E2E covering history clearing, restore via button/header/shortcut across restart and re-import, per-surface scoping against an Xtream portal, and legacy-key cleanup. Refs #1458 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(live-tv): mirror the EPG offset setting in the sidebar spec mock Master's player reads `resolvedEpgOffsetMinutes` from the settings store; the new sidebar spec was cloned from the movie-gate harness before that field landed, so its playing-channel case threw inside the EPG effect. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-e2e): scope the Stalker radio rail toggles to the rail The workspace header now carries a second "Hide/Show channels list" toggle, so the role+name locators matched more than one button and tripped Playwright's strict mode. Target the rail's own chevron and the floating restore button, and assert the header toggle mirrors the state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(live-tv): honour Cmd/Ctrl+B on collection pages and hide the header rail toggle on phones Codex review follow-ups on #1555: - The hidden-list state advertises Cmd/Ctrl+B, but the favorites/recent collection page had no handler; only the routed M3U/Xtream/Stalker live layouts did. The page now toggles the collection surface while its live tab is on screen, with the same typing/inert guards as the other hosts. - At the phone breakpoint the header already holds the drawer toggle, switcher, search and Add; the live rail is a bottom drawer with its own toggle there, so the header rail toggle is hidden below 640px. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(live-tv): migrate the live navigation helpers to the per-surface sidebar API master (#1554) added `XtreamLiveChannelNavigationService` and `stalker-live-navigation.ts`, which expand the rail through `sidebar.setState('expanded')` on the pre-split signature. Point them at the `portal` surface and update their specs; drop the now-unused hidden-state stub from the Xtream layout spec, which master pushed to the max-lines budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
436825bdec |
fix(xtream): try advertised TS after initial web HLS HTTP failure (#1558)
* fix(xtream): try advertised TS after initial web HLS HTTP failure * refactor(playback): extract fullscreen channel panel state * test(xtream): keep synthetic media within the mock project |
||
|
|
61b06b9f31 |
fix(portals): preserve live channel navigation while browsing (#1554)
* fix(portals): preserve live channel navigation while browsing * test(portals): await media source assertion in remote E2E * fix(xtream): capture destination queue for live auto-open |