Commit Graph
2733 Commits
Author SHA1 Message Date
4gray fabffcc902 fix(stalker): reject replaced late edit targets 2026-08-09 18:23:34 +02:00
4gray e4bee6da40 fix(stalker): preserve concurrent edit state 2026-08-09 18:15:37 +02:00
4gray a998b0ebdd fix(stalker): keep edits off Xtream save path 2026-08-09 18:07:05 +02:00
4gray 37575c8d88 fix(stalker): merge late connection saves 2026-08-09 18:01:13 +02:00
4gray ffae919cc8 fix(stalker): persist submitted identity after navigation 2026-08-09 17:40:55 +02:00
4gray 5184bc042c fix(stalker): fingerprint portal URL credentials 2026-08-09 17:11:16 +02:00
4gray 7941fec936 fix(stalker): fence repair history reads 2026-08-09 17:01:32 +02:00
4gray e05f49c074 fix(stalker): defer repair retirement during edits 2026-08-09 16:48:57 +02:00
4gray d2c149378a fix(stalker): preserve edit-owned repair tokens 2026-08-09 16:44:02 +02:00
4gray c34bc8739d fix(stalker): verify repair override retirement 2026-08-09 16:31:14 +02:00
4gray 41559b8890 fix(stalker): retire restored repair overrides 2026-08-09 16:13:37 +02:00
4gray b888d6a39b fix(stalker): fence abandoned discovery retries 2026-08-09 16:03:08 +02:00
4gray bfbd252ea2 fix(stalker): retain abandoned auth fences 2026-08-09 15:36:37 +02:00
4gray 344182014f test(stalker): await settled append failure 2026-08-09 15:17:32 +02:00
4gray b4c90dc185 Merge remote-tracking branch 'origin/master' into agent/stalker-smart-discovery 2026-08-09 15:12:27 +02:00
4gray 5532825b30 fix(stalker): reject superseded portal responses 2026-08-09 14:58:07 +02:00
4gray a4e74f425f test(stalker): align simple portal session guard 2026-08-09 14:35:45 +02:00
4gray 79d3fb9dbd fix(stalker): fence stale portal modes 2026-08-09 14:20:44 +02:00
4gray bb474f0f8c fix(stalker): reject stale repairs before discovery 2026-08-09 13:59:16 +02:00
4gray a41a1d352a fix(stalker): release abandoned edit fences 2026-08-09 13:53:04 +02:00
4gray 854eb2c2e9 Merge origin/master into agent/stalker-smart-discovery 2026-08-09 13:39:37 +02:00
4grayandClaude Fable 5 cf74f7e4a0 feat(stalker): append portal pages on scroll and drop pagination everywhere (2/2) (#1395)
* feat(stalker): append portal pages on scroll and drop pagination everywhere

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

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

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

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

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

Round-1 review findings on #1395:

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

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

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

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

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

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

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

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

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

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

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

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

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

Round-4 findings on #1395:

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

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

portal-stalker-feature 263, lint clean.

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Round-5 Codex P2s on #1392:

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:33:56 +02:00
4gray 22f9aa4c24 test(stalker): await edit hydration 2026-08-09 11:00:49 +02:00
4gray e1f0031706 fix(stalker): hydrate playlist identity before edit 2026-08-09 10:53:58 +02:00
4grayandClaude Fable 5 d5f84fb130 feat(xtream): catch-up badge for live channels with archive (#1341)
* feat(xtream): show a catch-up badge on live channels that have archive

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

Closes #1128

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:47:44 +02:00
4gray 7c8b94a9ed fix(stalker): serialize overlapping edits 2026-08-09 10:27:33 +02:00
4gray a7ccdc905b fix(stalker): fence all edit authentication 2026-08-09 10:15:57 +02:00
4gray 87bb0aea31 Merge remote-tracking branch 'origin/master' into agent/stalker-smart-discovery
# Conflicts:
#	apps/web/src/app/app.config.ts
2026-08-09 10:07:22 +02:00
4gray b387fe2a55 fix(stalker): serialize edit discovery 2026-08-09 09:59:30 +02:00
4gray 87dc45957b chore(deps): align Angular packages on 21.2.19 (#1383)
* chore(deps): align Angular packages on 21.2.19

* docs(deps): align Vite patch references
2026-08-09 09:49:38 +02:00
4grayandClaude Fable 5 1a6af75761 feat(settings): per-section pages with unsaved-changes bar (#1384)
* feat(settings): split settings into per-section pages with an unsaved-changes bar

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

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

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

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

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

New SETTINGS.UNSAVED_DIALOG_* keys in all 19 locales.

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 09:34:03 +02:00
4gray 3505eafdd1 fix(stalker): make edited connection persistence atomic 2026-08-09 09:32:26 +02:00
4gray 8d6fe1e02d docs(stalker): document smart endpoint discovery 2026-08-08 22:06:27 +02:00
4gray d2d91e322b feat(stalker): discover portal connection on edit 2026-08-08 22:06:27 +02:00
4grayandClaude Fable 5 c95f826739 fix(playback): keep Video.js controls on the live MPEG-TS path (#1385)
Video.js was constructed without the controls option — the component relied
on a [controls] template binding on the original <video> element instead.
player.reset(), which every raw MPEG-TS/live source change goes through,
replaces the tech <video> element; the binding's target is disposed, so live
playback ended up with no native controls and a vjs-controls-disabled control
bar: no visible controls at all.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:12:59 +02:00
dependabot[bot]and4gray 7103f7e734 chore(deps): bump pnpm/action-setup from 4 to 6.0.9 (#1372)
* chore(deps): bump pnpm/action-setup from 4 to 6.0.9

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

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

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

* test(packaging): allow pnpm action setup v6

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 4gray <serega05@gmail.com>
2026-08-08 09:39:52 +02:00
dependabot[bot] fd29362d44 chore(deps-dev): bump electron from 41.7.2 to 41.10.3 (#1377)
Bumps [electron](https://github.com/electron/electron) from 41.7.2 to 41.10.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v41.7.2...v41.10.3)

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

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

* fix(build): preserve commented Vite URL imports
2026-08-08 08:03:09 +02:00
4gray f40320e42e test(stalker): isolate auth e2e state by worker (#1378)
* test(stalker): isolate auth e2e state by worker

* test(stalker): bound auth e2e worker slots
2026-08-08 01:05:08 +02:00
4gray fd96b85c19 feat(playback): recommend recovery actions (#1374)
* docs(playback): design recovery recommendations

* docs(playback): plan recovery recommendations

* refactor(playback): extract diagnostic utilities

* feat(playback): define recovery recommendation contracts

* feat(playback): rank recovery recommendations

* feat(playback): track session recovery attempts

* feat(playback): identify content recovery sessions

* feat(ui): add ranked playback diagnostic panel

* feat(playback): switch temporarily to recommended players

* test(playback): cover temporary player recommendation

* test(playback): verify recommendation capability guards

* docs(playback): document recovery recommendations

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

* fix(playback): remove derived tracking ownership

* fix(playback): preserve distinct recovery fallbacks

* fix(playback): reset resume for new sources

* fix(playback): preserve desktop recovery guidance

* docs(playback): clarify recovery policy exceptions

* fix(playback): reject stale progress updates

* fix(playback): keep protected recovery guidance neutral

* test(playback): cover stale progress output

* fix(playback): neutralize protected diagnostic copy

* fix(playback): harden runtime guidance ownership

* fix(playback): stabilize recovery application ownership

* fix(ci): classify playback util coverage

* fix(e2e): preserve playback fixture bytes
2026-08-08 01:04:39 +02:00
4grayandClaude Opus 5 5e4f2ca3dd docs(stalker): reconcile the Stalker docs after the API-compatibility series (#1375)
Nine PRs landed between 2026-08-01 and 2026-08-04 in parallel worktrees, each
editing its own section of docs/architecture/stalker-portal.md and CLAUDE.md.
Sections that were correct when written disagreed with each other, or with
master, afterwards. Every claim here was verified against the code.

Corrected in stalker-portal.md: routes listed without the /workspace prefix;
"simple portals carry only the mac= cookie" (every request goes through the
shared identity builder — but the direct branch forwards no serial, so no
SN/__cfduid either, while playback headers are NOT mode-gated); a facade
introduced as "three modules" above a list of five; the pre-#1370 "blank
fields are not generated" opening; an ambiguous stalker-identity.utils.ts
citation (two files share the name); two of the three surfaces that apply the
scoped header override; a bare {status: 1} now being a refusal; and the
session-state fields #1354 added to the backup exclusion list (mirrored in
playlist-backup-restore.md).

CLAUDE.md had no entry at all for portal mode / endpoint discovery / lazy
repair — the largest change of the series; added one. Its session-facade list
was missing two modules and status 1 still read as plain "blocked".

Mock server: documented the /stalker, /stream/gated and marketing-poster
routes and the HOST variable; replaced the global POST /reset guidance with
the real per-MAC isolation contract (OWNED_MACS, the sibling 00:1A:79:5F:*
range, mode: 'serial'); added get_main_info; refreshed the project tree; fixed
a broken anchor; and corrected MOCK_PORT, which moves the client side only —
nothing maps it to the server's PORT.

The repo skill's "keep Stalker request rules in Stalker data access" no longer
holds: the wire-format, identity, portal-mode and auth-failure contracts live
in shared/interfaces because the Electron main process cannot import renderer
libs.

Also fixes four stale code comments carrying the same claims, including
"Single choke point for Stalker API calls" — four callers deliberately go
direct, and only fetchViaProfile() wires repair itself.

Docs and comments only; no executable change. No release note (no user-visible
behavior); no-release-note label applied for the libs/** paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:49:49 +02:00
dependabot[bot] 53c318bac7 chore(deps): bump axios from 1.18.1 to 1.19.0 (#1367)
Bumps [axios](https://github.com/axios/axios) from 1.18.1 to 1.19.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.1...v1.19.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.19.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 20:43:34 +02:00
dependabot[bot] 33e345c83f chore(deps): bump github/codeql-action in the actions-minor-patch group (#1371)
Bumps the actions-minor-patch group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 20:42:54 +02:00
dependabot[bot] d8e3eb9219 chore(deps-dev): bump angular-eslint from 21.3.1 to 21.4.0 (#1350)
Bumps [angular-eslint](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/angular-eslint) from 21.3.1 to 21.4.0.
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/angular-eslint/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v21.4.0/packages/angular-eslint)

---
updated-dependencies:
- dependency-name: angular-eslint
  dependency-version: 21.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 20:42:29 +02:00
4grayandClaude Opus 5 9ff1c6ae01 feat(stalker): identity hardening (#1370)
MAC addresses are canonicalized to the uppercase colon form a real STB
sends and validated at the input boundary, with a hint when they fall
outside Infomir's OUI — which the stock server's default filter refuses
with a bare {status: 1} no user could diagnose. Normalization applies
only to a value the user actually edits: rewriting stored bytes would
move the session fingerprint for every existing playlist with no user
action, and the MAC is the account key.

Device IDs can optionally be derived from the MAC the way StbEmu and
stalker-to-m3u do — SHA256(MAC) and SHA256(MAC + "stalker"), which a
real box never reports as equal. The portal pins the first non-empty
device_id/device_id2 it sees to the MAC permanently, refuses a different
one, and treats a later empty value as an unrecoverable lockout, so
derived values are written into the visible fields and persisted as
literal strings, never recomputed at request time. The option is offered
at import only; the edit dialog warns instead once an ID has actually
reached the portal.

get_profile now reports one coherent MAG250 (ver, stb_type — previously
empty —, hw_version, image_version, client_type), and a device conflict
gets its own StalkerPortalError kind so the UI can explain it instead of
relaying the portal's "Your STB is damaged".

Closes the identity-fields cluster: #927, #860.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:09:07 +02:00
4gray 7111942509 chore(deps): update Nx to 22.7.2 (#1365)
* chore(deps): update Nx to 22.7.2

* fix(deps): keep Nx major updates manual
2026-08-04 16:07:45 +02:00