Merge origin/master into claude/parental-control-feature-31dde2

Resolves the settings-store defaults split, the electron-conf key list and
the guidance reorganization (CLAUDE.md now imports AGENTS.md; the parental
lock contract is linked from the agent context map instead).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-25 22:12:58 +02:00
commit ae5d3f49de
732 files changed
+42572 -15380

No files matched your search

@@ -33,7 +33,8 @@ is _outside_ the parent's range. Taking "latest" would break them:
| ---------------- | --------- | --------------------------------------- | ------------- |
| `@xmldom/xmldom` | 0.8.13 | `mpd-parser` `^0.8.3`, `plist` `^0.8.8` | 0.9.x ❌ |
| `fast-uri` | 3.1.4 | `ajv` `^3.0.1` | 4.x ❌ |
| `js-yaml` | 4.3.0 | `electron-updater` `^4.1.0` | 5.x ❌ |
| `js-yaml` | 4.3.2 | `electron-updater` `^4.1.0` | 5.x ❌ |
| `smol-toml` | 1.7.1 | `nx` exact `1.6.1` (see below) | 1.8.x ❌ |
| `form-data` | 4.0.6 | `axios` `^4.0.5` | 4.0.6 ✅ |
| `ajv` | 8.18.0 | `electron-conf` `^8.13.0` | 8.20.0 ✅ |
@@ -43,6 +44,12 @@ Before changing any of these, check the parent's declared range first:
npm view <parent>@<version> dependencies --json
```
One deliberate exception: `nx` pins `smol-toml` to an exact version, so no
override can stay inside that "range". The `smol-toml` override targets the
first patched release (a minor above the pin) and is re-checked against
`npm view nx@latest dependencies.smol-toml` whenever Nx is updated — once Nx
itself ships the patched pin, the override is dropped.
## Verifying an override actually applied
Grepping `pnpm-lock.yaml` for the old version still finds it, but that hit is
+100 -12
View File
@@ -162,18 +162,20 @@ Contracts:
player subtree, so shell state changes cannot recreate the `<video>`.
- **External MPV/VLC sessions do not flip the layout to watch** — browse
layout stays, and the primary CTA keeps its "Stop <player>" behavior.
- The shell's sticky control and Escape close inline playback through
`closePlayerRequested`; hosts wire it to `closeInlinePlayer()`. From browse,
they emit the host-owned `backClicked` instead (unless `backAvailable=false`).
Escape respects fullscreen, menus/dialogs, editable fields and hidden/inert
surfaces, and consumes a handled key so one press performs only one action.
See [Portal Detail Navigation](./portal-detail-navigation.md).
- The now-playing bar separates two exits: the back arrow emits
`backClicked`, which hosts wire to their route-level `goBack()` (straight
back to the list — everything browse offers is also visible in watch, so
a two-step unwind would be ceremony); the "Close player" button and
Escape emit `closed`/`closePlayerRequested` and return to browse without
navigating.
- The shell's sticky arrow emits the host-owned `backClicked` in browse and
watch alike (unless `backAvailable=false`, when it is not rendered at all):
hosts wire it to their route-level `goBack()`, straight back to the list —
everything browse offers is also visible in watch, so a two-step unwind
would be ceremony. Escape alone unwinds one level: in watch it emits
`closePlayerRequested`, which hosts wire to `closeInlinePlayer()`; in browse
it emits `backClicked`. Escape respects fullscreen, menus/dialogs, editable
fields and hidden/inert surfaces, and consumes a handled key so one press
performs only one action. See
[Portal Detail Navigation](./portal-detail-navigation.md).
- The now-playing bar has one exit of its own: the "Close player" button
emits `closed` and returns to browse without navigating. It carries no back
arrow — a second arrow beside the sticky one, with a different meaning,
was the duplicate this contract removes.
- Entering watch scrolls the shell to the top; leaving keeps the scroll
position.
@@ -248,6 +250,16 @@ seasons, with per-episode watch-progress bars from playback positions.
- Layering: the rail is an opaque panel rendered on top of the stage, so the
ambient fill stays behind it and shows in the flexible gap between the
docked player and the rail on very wide stages.
- Fullscreen: the rail cannot show over a fullscreen video, so the same
host offers the whole series — season tabs plus the selected season's
episodes — as the slide-in side panel of the player's fullscreen surface.
`PortalInlinePlayerComponent` provides `FULLSCREEN_CHANNEL_PANEL` for the
nested view from the hosts' `seriesEpisodes` / `episodePlaybackPositions` /
`seasonLoadStates` inputs, an episode picked there travels the same
`upNextEpisodeSelected` output as a rail click, and a season tab picked
there travels `episodePanelSeasonSelected` into the host's
`onSeasonSelected`. Contract: "Fullscreen episode panel" in
`docs/architecture/player-controls-contract.md`.
Season navigation inside `SeasonContainerComponent` uses season tabs
(`SeasonTabsComponent`; a dropdown beyond 6 seasons) instead of the old
@@ -266,6 +278,82 @@ episode" chip appears when the playing episode is outside the opened
season. Season descriptions come from `get_series_info` seasons (Xtream)
or `TmdbEnrichmentService.getSeason` (Stalker).
The tabs and description sit in a **season card** with an optional cover
column: the selected season's own poster (`seasonPosters` input, keyed like
`seasonDescriptions`; TMDB season poster first, provider `seasons[].cover_big`
next — see "Season/Episode Enrichment" in `tmdb-metadata-enrichment.md`).
The column is sized by `--season-cover-width` (96 / 120 / 144px for
`Settings.coverSize` small / medium / large, `apps/web/src/_cover-size.scss`;
medium matches the About block's poster) and is not rendered at all — the
card collapses to one column and the tabs render exactly as before — when
the selected season has no poster, when the item has a single season (that
poster is the show poster a few hundred pixels below the hero), or when the
image request failed. The hero poster never follows the season: the show
keeps its identity element, the season gets its own picture next to its own
text. The fullscreen episode panel shows the same poster as a compact season
strip (poster, season name, episode count — `PORTALS.EPISODE_COUNT_ONE` /
`PORTALS.EPISODE_COUNT_OTHER`) above its season tabs, under the
same gates, from `PortalInlinePlayerComponent.seasonPosters` through
`buildFullscreenEpisodePanelSeasons`.
Beyond six seasons the tabs become a `mat-menu` dropdown, and that dropdown
carries **season thumbnails** from the same `seasonPosters` map
(`SeasonTabsComponent.seasonPosters`, passed by the season container and by
the fullscreen episode panel): a 28×42 poster projected into the leading slot
of each menu row that has one, and the selected season's poster inside the
closed trigger (`season-tabs__dropdown--with-thumb` tightens the pill around
it). A season without a poster gets no placeholder tile — its row simply
starts with the text — and a poster whose image request fails is dropped from
both places rather than left as a broken-image frame (the component keeps its
own failed-URL set, independent of the container's cover column). The pill
row (six seasons or fewer) deliberately stays text-only: the design review
rejected per-pill thumbnails as a second poster rail, and the season cover
beside the tabs already shows the selected season's picture.
### Manual watched toggle for movies
Movies carry the same manual "watched" affordance as episodes, in the
detail action row of both portals: Xtream renders an icon square after
Favorite (`vod-details-watched.service.ts`), Stalker a labelled button
inside the shared `app-vod-details` component, wired by the routed
catalog detail and by the collection inline detail (Favorites / Recent).
Both hosts delegate to `createVodWatchedToggle()` in
`@iptvnator/portal/shared/util`:
- **Marking** writes a full-progress `vod` position row — the same shape
playback leaves behind, so catalog badges, the dashboard and the
Play/Resume rule need no new state. The duration is the stored row's
(real), else the provider's `duration_secs` (Xtream), else 1 s: Stalker
VOD details state no runtime, and "position === duration" is what
"watched" means, whatever the number.
- **Unmarking** deletes the row, which also forgets the resume point —
the trade the episode toggle already makes.
- Both writes use the rejecting `savePlaybackPositionOrThrow` /
`clearPlaybackPositionOrThrow` boundary: the row on screen changes only
after a confirmed write, and a refused write reports instead of showing
the movie as (un)watched.
- The toggle is **disabled while the movie plays** (inline player mounted,
external session live or launching for this content): the player
persists its position every ~15 s and would overwrite a just-written
row, silently flipping the movie back.
- It acts on the **route copy's row only**. Positions are keyed by
(playlist, stream), so a pinned multi-source alternative keeps its own
state, exactly as playback would leave it.
- A completion that lands **after navigation** (`stillCurrent`) still
refreshes the playlist's catalog positions (the write did land), but
neither patches the new page's row nor shows its snackbar.
- A watched copy shows **Play**, never "Resume 1:32:00" from its final
seconds — `app-vod-details` folds `isWatched` into `hasPlaybackPosition`,
which Xtream's route already did through its 90% rule.
Catalog cards derive their corner badge from one shared `PortalWatchState`
(`unwatched` / `in-progress` / `watched`, `portal-watch-state.ts`): both
facades map a movie's position through `watchStateFromProgressPercent` /
`resolvePortalWatchState` (90% threshold, shared with the Resume rule),
and a series through `resolvePortalSeriesWatchState`, which reports at
most `in-progress` — the list payload never carries the episode total, so
"every episode watched" is not decidable there.
The season header carries a season-level watched toggle next to
"Download season" (`season-watch-toggle.util.ts` builds the request:
marking touches only unwatched episodes so real durations survive;
+7
View File
@@ -987,6 +987,13 @@ The Electron main process holds an `electron.powerSaveBlocker` of type `prevent-
Current development behavior:
- The addon build supports `darwin`, `win32`, and `linux`; Windows and Linux builds require running on that target OS.
- The addon is compiled by upstream `node-gyp`, declared as a root
`devDependency` and resolved with `require.resolve` in
`apps/electron-backend/build-embedded-mpv.js`. That resolution must not rely
on the `NODE_PATH` that pnpm's `.bin` shims export: it only reaches pnpm's
hidden hoist (`node_modules/.pnpm/node_modules`), so an undeclared
`node-gyp` works under `pnpm nx …` and in CI but fails from a plain
`node apps/electron-backend/build-embedded-mpv.js` on a clean install.
- The build script first looks for staged inputs at `vendor/embedded-mpv/<platform>-<arch>/`. On Linux, local development can fall back to distribution `libmpv-dev` headers and libraries. `LIBMPV_INCLUDE_DIR` overrides the header root. `LINUX_NATIVE_LIBRARY_DIR` is a link-time override and must name a directory already visible to the system dynamic loader; it is never inherited as helper `LD_LIBRARY_PATH`.
- When the staged-input path is used, it must contain `include/mpv/client.h`,
`runtime-manifest.json`, and the platform runtime/build files. The Linux
+44 -5
View File
@@ -109,16 +109,55 @@ errs towards contacting the host:
| Trip | 2 consecutive host-level failures within an inclusive 120 s window |
| Open for | 30 s (`OPEN_DURATION_MS`), matching the repo's other cooldowns |
| Half-open | exactly ONE trial request; the rest keep fast-failing until it settles |
| Reset | any HTTP response — 200, 404, even 502 — the host answered |
| Reset | any HTTP response — 200, 404, even 502; an accepted TCP connection clears the streak but not an open breaker |
| Key | `URL.origin` — scheme, host **and** port (see below) |
| Kill switch | `IPTVNATOR_DISABLE_CONNECTIVITY_GUARD=1` (read per call) |
**Host-level failure** means an error with no HTTP response whose code is one of
`ETIMEDOUT`, `ECONNABORTED`, `ENOTFOUND`, `EAI_AGAIN`, `ECONNREFUSED`,
`EHOSTUNREACH`, `ENETUNREACH`. `ECONNRESET` is deliberately excluded: a reset
mid-transfer happens on hosts that are very much alive. Cancelled requests
(`ERR_CANCELED`) and SSRF-policy refusals are `inconclusive` — they say nothing
about reachability and only release the half-open slot.
`EHOSTUNREACH`, `ENETUNREACH` — observed **before the TCP connection was
established**. `ECONNRESET` is deliberately excluded: a reset mid-transfer
happens on hosts that are very much alive. Cancelled requests (`ERR_CANCELED`)
and SSRF-policy refusals are `inconclusive` — they say nothing about
reachability and only release the half-open slot.
**A timeout after the handshake is a slow host, not a dead one.** axios raises
the same `ECONNABORTED` whether the SYN went unanswered or the panel accepted
the connection and then thought for longer than the request budget (a heavy
`get_vod_info` on a busy home server). Only the former is what the guard
exists for; tripping on the latter turned "slow" into thirty seconds of
"portal is not responding" for every request, which is how users described
it. Each transport therefore reports whether the connection was established —
Electron through `ValidatedAxiosRequestConfig.onConnect` (the request gets its
own agent, observed via `observeAgentSocketConnections`, instead of the shared
keep-alive `globalAgent`; a pooled socket that is already connected counts as
connected), the web backend through `WebBackendHttpGetOptions.onConnect`,
honoured by `ProviderAxiosTransport`, which owns the `ClientRequest` — and
the accepted connection is credited THE MOMENT IT HAPPENS
(`reportGuardedHostConnected` / `reportProviderRequestConnected` from the
connect hook call `HostConnectivityGuard.reportConnected`, which clears the
failure streak but deliberately closes no open or half-open breaker: whether a
host that accepts a connection also answers is exactly what the trial exists
to find out, so the trial keeps its slot until it settles), and
`classifyHostRequestFailure(error, { connected })` then reads the eventual
host-level code as `inconclusive`. Two things hang on that
ordering. Clearing at connect time is what keeps an unanswered SYN, an
accepted-but-slow request and another unanswered SYN from adding up to a
trip, since the middle request proved the host alive in between. Crediting it
at connect time rather than when the timeout settles is what keeps a request
that connected and then hung for 30 s from reopening a breaker that later
requests opened in the meantime — by then its evidence is older than theirs.
Such a request still costs its full timeout; the guard only stops charging it
to the host. Electron does not install the observer while an environment
proxy applies to the request — decided by the very resolution axios performs
(`proxy-from-env`, the same pinned package: `<protocol>_proxy` / `all_proxy`
in either case, `no_proxy` exemptions honoured, so a LAN portal listed there
keeps its observer): through a proxy the socket connects to the proxy, whose
handshake proves nothing about the portal, so those requests keep reporting
their timeouts as host-level exactly as before. Regression coverage:
`apps/electron-backend/src/app/util/host-connectivity-guard.slow-host.spec.ts`
(real loopback sockets) and the Xtream mock's `silent` scenario, whose
`get_vod_info` / `get_series_info` accept the connection and never answer.
**A failure is only charged to the endpoint that produced it.** Reaching any
later hop _proves_ the guarded endpoint answered — the first hop is always the
+106 -1
View File
@@ -162,7 +162,10 @@ ArrowUp/Down, PageUp/Down, Home/End and Space retain native scrolling there;
scroll keys do not bubble into document-level player shortcuts. A row's main
button remains separate from favorite/info actions, supports native Enter and
Space activation, and retains keyboard focus on activation. Tab/Shift+Tab use
the normal DOM order. Scrolling from a virtual row moves focus to its viewport
the normal DOM order; Safari's default keyboard preference skips buttons on
plain Tab, so there the row button is reached with Option+Tab (WebKit E2E
runs press it through `pressTab` in `apps/web-e2e/src/e2e-helpers.ts`).
Scrolling from a virtual row moves focus to its viewport
before CDK can recycle the row; asynchronous data updates never move focus.
Xtream aligns a newly selected channel only when it is outside the viewport;
updates to the same selected ID never re-align it. A smooth scroll to an
@@ -234,6 +237,65 @@ minimum dimensions and flexible columns over fixed row widths.
- Do not render placeholder and real logo at the same time
- Keep logos contained with `object-fit: contain`
## Cover Grids
Movie and series covers render in three surfaces: the catalog grid
(`app-grid-list`, `libs/portal/shared/ui/.../grid-list/`), the favorites /
recent card (`app-content-card`, same lib) and the dashboard rails. All of
them size from the `--cover-grid-min-width` / `--cover-rail-width` /
`--cover-gap` tokens that `Settings.coverSize` writes onto `<html>` as
`data-cover-size` (`apps/web/src/_cover-size.scss`). The same file carries
`--season-cover-width` (96 / 120 / 144px) for the season cover beside the
season tabs on series detail pages; medium equals the About block's 120px
poster so browse and watch share one secondary-poster size.
### Posters-only wall
`Settings.showCoverTitles` (Settings > General, default on, only an explicit
`false` opts out — coerced like `webPlayerSharedControls`) removes the title
row under VOD and series covers so the grid shows more rows per screen.
- **Resolution.** `CoverTitlesService.postersOnly` (`libs/portal/shared/ui`)
is the single source: the opt-out AND a hover-capable pointer
(`(any-hover: hover)` media query, tracked live). On touch-only devices the
preference is ignored and titles stay under the covers, because a tap
already opens the item and there is no gesture left to peek at a hidden
name.
- **Scope.** Catalog grids (Xtream/Stalker VOD and series), unified
favorites/recent grids and the portal favorites tab. Exempt, regardless of
the setting: live channel grids (`type` `live`/`itv`/`radio` or the
`logo` variant — logos are too often missing to identify a channel),
search results and "recently added" rails (they answer by name; hosts
pass `[allowPostersOnly]="false"` to `app-content-card`; `app-grid-list`
and `app-unified-grid-tab` drop the wall themselves while their
`searchTerm` input is non-blank, i.e. an in-section search is filtering
the list), and
the dashboard rails (their meta rows do not fit an overlay).
- **Reveal.** The title is a `.cover-title-overlay` inside the poster
wrapper: bottom gradient scrim, two clamped lines, 150 ms ease-out
opacity, shown on `:hover` and `:focus-visible` of the card, none under
`prefers-reduced-motion`. It is `aria-hidden`; the card itself carries the
accessible name.
- **Pinned caption.** When the item has no cover to identify it — no
poster URL, or the image failed and the default poster / placeholder is
showing — the overlay is pinned open (`--pinned`). Both components track
failed URLs so the fallback branch re-renders instead of swapping `src`
in place.
- **Layout hints.** The grid's `contain-intrinsic-size` drops from 270 px to
222 px (bare 2/3 poster) under `.grid-list--posters-only`, and the skeleton
hides its text lines so loading matches the cards it precedes.
- **Keyboard.** Both cards expose a `role="button"`, `tabindex="0"` surface
labelled by the title, activated by Enter and Space (Space prevents the
page scroll) and carrying a `:focus-visible` ring (`card-focus-ring`
mixin in `libs/ui/styles/_content-grid.scss`). On `app-content-card` that
surface is the inner `.content-card__activation` element, and the Remove
control (labelled by `removeTooltip`) is a SIBLING positioned over the
poster corner — an interactive control nested inside a `role="button"`
is an invalid accessibility structure. Its ring is drawn on the OUTER
`.content-card` via `:has(> .content-card__activation:focus-visible)`,
because the card's `overflow: hidden` would clip an outline on the inner
surface on every edge. Poster `alt` is the title, not a literal.
## EPG Views
The shared timeline and list still contain local dark surfaces, blue selection
@@ -469,6 +531,49 @@ The progress bar should clearly communicate:
Avoid making the track too faint, especially in dark theme.
## Loading States
Two loading states exist, chosen by whether content is already on screen.
### First load: skeleton
Nothing is rendered yet, so the skeleton replaces the whole content area and
mirrors the row/card geometry it precedes (see Channel List Item and Cover
Grids). The unified Favorites/Recent page gates this on `isLoading`, set only
while its item list is empty.
### Reload with content on screen: non-destructive indicator
A reload of a list that is already rendered (the collection page's
"This playlist ↔ All playlists" scope switch, a favorites reload) must never
swap back to the skeleton: that unmounts a playing channel, drops focus from
the toggle the user just clicked, and flashes on fast IndexedDB/SQLite answers.
Instead keep everything mounted and layer feedback on top:
- an indeterminate `mat-progress-bar` (2 px track, `--app-selection-color`
fill) absolutely positioned over the header's bottom separator, so its
appearance never shifts content;
- `aria-busy="true"` on the content region for the whole reload;
- the list or grid dims to opacity `0.6` with a 160 ms transition (`0ms` under
`prefers-reduced-motion: reduce`). The player is never dimmed — the live tab
dims only its channel rail.
**Grace period.** The bar and dimming render only once the reload has run for
`COLLECTION_RELOAD_INDICATOR_DELAY_MS` (180 ms); a reload that settles sooner
shows nothing. `aria-busy` is set immediately, since it does not paint. A
superseded reload keeps the earliest deadline and never clears the indicator;
only the latest request's completion does. Reference implementation:
`createCollectionReloadIndicator` in
`libs/portal/shared/data-access/src/lib/collection/collection-reload-indicator.ts`.
Controls that triggered the reload stay enabled and reflect the requested
value at once (`scope.set()` runs synchronously before the load starts).
Because the rows on screen then belong to the PREVIOUS request, actions on
them (Clear, drag reorder) must bind to the request that loaded those rows,
never to the toggle's current value — "This playlist" applied to still-mounted
global rows would delete other playlists' favorites or write foreign URLs
into this playlist (`loadedRequest` on `UnifiedCollectionDataService`, which
the collection page reads through its own `mutationRequest`).
## Navigation Lists
Use the shared `nav-list.scss` treatment for sidebar and context-panel list items.
+187 -4
View File
@@ -978,7 +978,34 @@ These URLs are playlist-scoped by default:
TTL expires.
- Scoped lookups fall back only to Settings-managed EPG URLs for channels
missing from the playlist-declared source. Playlist-local sources from other
playlists are not treated as global fallback sources. Single-channel current
playlists are not treated as global fallback sources. The one opt-out is
`EpgLookupOptions.anySourceFallback` (renderer-only, never forwarded to the
bridge): after the scope — playlist sources, then the global ones — has
answered, the keys still without a programme are retried once against every
imported source through the source-less batch path and its cache. The
ladder has the same shape with or without the bridge's batch endpoint: on
an older preload the scoped pass runs as per-channel scoped lookups
(`getScopedCurrentProgramForChannel`, same scope -> fallback-scope walk,
same scoped cache key) and only the any-source retry is source-less.
Collapsing that preload straight into the source-less lookup would drop
the caller's scope, which is what the scopes exist to prevent. The
dashboard live
rails pass the option: without it a favourite whose guide only exists in
another playlist's XMLTV showed no programme on the dashboard while its
"See all" row — resolved by `StreamResolverService`, which never scopes by
source — had one. They still ask **per source scope**, one lookup per
distinct set of playlist-declared XMLTV URLs, and namespace the answers by
that scope: a `tvg-id` is unique inside a guide, not across imports, so a
single flat map keyed by lookup key alone would hand one playlist's card
the programme another playlist's guide resolved for the same id. Playlists
sharing a guide share one lookup. Only a card that carries a real XMLTV key
is widened: an Xtream or Stalker card has none, so its lookup key is just
its display title, and searching every guide by title would let a
same-named M3U channel answer for a portal channel. Those cards keep the
strict scope (their own programmes come from the portal), and the
any-source flag is part of the scope identity so the two never share an
answer. Wiring: `DashboardLiveEpgPresenter` in
`libs/workspace/dashboard/feature/src/lib/rails/`. The channel list keeps the strict scope. Single-channel current
program lookups include the source URL set in their cache and in-flight keys,
so playlist-local and global lookups deduplicate without reusing the wrong
source scope. Batch current-program lookups use the same source-scoped
@@ -1063,6 +1090,62 @@ These URLs are playlist-scoped by default:
## EPG Integration
### Xtream channel-row programme refresh
The "current programme" line under each Xtream Live TV channel used to be
written only when a row scrolled into view, when an EPG result arrived, or
when the display offset changed. 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 (#767).
`PortalChannelsListComponent` re-checks the rows on screen once a minute. The
rules, each of which exists to avoid a specific failure:
- A programme still on air only has its **progress bar** advanced. No cache
read, no request: a row with nothing to learn must not cost traffic.
- Once it ends the row is re-picked from the queue's cache, and only a
programme that is **on air or upcoming** may replace it. A finished
programme is never re-applied — it would keep presenting itself as current,
and the earliest-item fallback that fills a blank row on first paint would
move an advanced row *backwards*.
- A cached guide whose programmes have **all** ended is dropped
(`EpgQueueService.invalidate`) and refetched, because the queue skips any
stream that still holds a cached answer. An **empty** answer means the
provider has no guide for that channel and is left alone; re-asking would
put one call per EPG-less visible row on the wire every minute.
- What is on screen stays there until a replacement arrives, so a refreshing
row never blanks out.
A programme occupies `[start, stop)` in every one of these comparisons, so
"has it ended" and "what is on air" cannot disagree on the boundary instant.
The selection rules are pure functions in
`libs/portal/xtream/feature/src/lib/portal-channels-list/epg-preview-program.ts`
and take an explicit `nowMs` in the PROVIDER's clock (`epgProviderClockMs`),
never `Date.now()`.
Two root-provided services exist because a live layout mounts the channel list
more than once — the sidebar and the fullscreen channel panel render side by
side — over one shared `EpgQueueService`:
- `EpgRefillLimiter` is the floor on dropping an exhausted cache, the one
place that overrides the queue's own throttling. A provider whose guide has
genuinely run out answers the refill with the same finished programmes, so
without a floor the row would ask again on the very next tick. Records carry
the owning playlist, since a stream id is provider-local and the service
outlives a playlist switch, and they expire by age rather than by viewport
membership — a claim dropped when its row scrolled away would be handed back
the moment the user scrolled to it again.
- `EpgRefreshCoordinator` owns the single timer and merges what every mounted
list needs into one queue request. `EpgQueueService.enqueue` is latest-wins:
it bumps one generation, replaces the queue and the visible set, and drops an
earlier caller's entries after its XMLTV await. Separate timers would cancel
each other whenever both lists had rows to fill, which is exactly what
happens on a programme boundary. Each list still decides for itself what is
stale (that reads only its own state) and contributes its whole visible
slice, since the queue drops anything outside the visible set it was last
handed; a channel both lists show is fetched once, and playlists stay apart
because their credentials differ.
### XMLTV response compression
The Electron EPG worker decodes HTTP `Content-Encoding` layers in reverse
@@ -1081,6 +1164,63 @@ retried as plain XML. Source errors and consumer cancellation terminate the
whole decoding chain. The decoder does not recursively unpack file layers or
change XML parsing, source reconciliation, or database persistence contracts.
### Local XMLTV files
An EPG source is not necessarily a URL. Settings → EPG and the playlist
dialog accept, next to `http(s)` links, a file on the user's computer in any
of these shapes (Electron only — the PWA has no EPG import at all):
- a `file:` URL (`file:///home/you/epg/guide.xml.gz`, `file:///C:/epg/guide.xml`)
- an absolute POSIX path (`/home/you/epg/guide.xml`)
- a Windows drive or UNC path (`C:\epg\guide.xml.gz`, `\\nas\share\guide.xml`)
Both surfaces also offer a folder button that opens the native file picker
(`EPG_OPEN_FILE_DIALOG` → `ElectronBridgeApi.openEpgFileDialog`, gated on
`RuntimeCapabilitiesService.supportsEpgFilePicker`) and writes the chosen
absolute path into the row. Relative paths are refused: the main process
has no meaningful working directory to resolve them against. The value is stored exactly as typed
(trimmed) and is the source key everywhere — freshness, reconciliation, the
progress panel and `epg_channel_sources` all treat it like a URL string.
The shape rules live in one place, `classifyEpgSourceReference()` in
`libs/shared/interfaces/src/lib/epg-source-reference.util.ts`, and both
forms validate through its structural `validateEpgSourceReferenceControl`.
In the worker, `openEpgSourceStream()` (`epg-source-stream.ts`) is the single
entry that yields decoded XMLTV bytes: remote sources still go through
`requestWithValidatedRedirects` with the private-network/TLS trust policy,
while local sources are read with `fs.createReadStream` behind the same
signature-sniffing optional gunzip stage the HTTP path uses, so `.xml`,
`.xml.gz` and an extension-less gzip file all parse. A missing file, a
directory, or a truncated gzip fail the import with a plain message; nothing
is retried as XML.
**Provenance rule.** Only values the user chose or typed by hand may be
local, and the decision is enforced in the main process, not in the form.
The local branch bypasses `validateRemoteUrl` (which only knows http/https),
so two layers guard it:
- Header-declared M3U sources (`x-tvg-url`/`url-tvg`/`tvg-url`) are filtered
to remote links — `extractM3uEpgUrls` no longer matches `file:`, and
`resolvePlaylistEpgSourceState` / `filterPlaylistEpgUrlsForFetch` drop a
legacy non-remote entry that older versions stored from a header unless it
also appears in `manualEpgUrls`. A downloaded playlist cannot point the
importer at a file on disk; a user whose own local M3U references a local
XMLTV adds that file in the playlist dialog instead.
- `FETCH_EPG`/`EPG_FORCE_FETCH` carry renderer-supplied strings, so
`EpgWorkerService.startFetch` asks the main-process
`EpgLocalSourceAuthorizer` (`epg-local-source-authorizer.ts`) before a
local path reaches the worker. A path the native picker returned is trusted
at once; a hand-typed path is confirmed once in a native message box the
renderer cannot fake, and a refusal fails that fetch — the progress panel
shows it with Retry ("Retry to be asked again") and the IPC result does
not report success. Allowed paths persist under
`TRUSTED_LOCAL_EPG_SOURCES` in the main-process config (`store.service.ts`)
so startup refreshes need no prompt. The worker's local branch opens only
when main set `allowLocalFile` on the fetch options — the flag is never
taken from the renderer — and the service defaults to a deny-all authorizer
until `epg.events.ts` installs the persisted one, so a missing wiring fails
closed. This mirrors `save-file-dialog` → `write-file` for playlist exports.
### EpgService (`@iptvnator/epg/data-access`)
```typescript
@@ -1170,9 +1310,11 @@ class EpgService {
- The unified favorites/recent live tab
(`libs/portal/shared/ui/.../unified-collection/unified-live-tab.component.ts`)
hosts the same timeline but does not use the NgRx playlist state; it keeps
its own `activeTimeshift` signal, resolves the replay URL with
`resolveM3uCatchupUrl`, and swaps the inline player's playback target (or
hands the URL to the configured external player). Selecting another channel,
its own `activeTimeshift` signal and hands it to
`createUnifiedLiveCatchup` (`unified-live-catchup.ts`), which resolves the
replay URL with `resolveM3uCatchupUrl` and swaps the inline player's
playback target (or hands the URL to the configured external player).
Selecting another channel,
closing the player, or "Return to live" clears the override.
- Catch-up activation is never silent: if the replay URL cannot be resolved
for a programme the user clicked, both hosts surface a
@@ -1764,3 +1906,44 @@ deletes with follow-up cleanup warnings. The UI does not resurrect a deleted
row after a cleanup failure. Downloaded files are not removed. The dialog's
confirmation covers deletion of the source and associated favorites, history
and playback positions; no deletion happens on merely opening the dialog.
## Opening playlists from the operating system
Electron only: a `.m3u`/`.m3u8` path passed
on the command line, opened through a file association, or delivered by macOS'
`open-file` event is normalized to an absolute path in the main process
(`apps/electron-backend/src/app/services/playlist-open-request.ts`) and queued there. The renderer
(`apps/web/src/app/services/playlist-open-request.service.ts`) subscribes to the
`OPEN_FILE` push **before** calling `announcePlaylistOpenListener`, which is
what makes the main process flush. `OPEN_FILE` is the only way out of the
queue, and a request stays there until the renderer confirms receipt via
`acknowledgePlaylistOpenRequest` — `webContents.send()` returns before the
listener runs, and a reload or dead render process keeps the `WebContents`
alive, so a successful push is not proof of delivery. Anything unacknowledged
is replayed to the next renderer that announces itself. The renderer
imports them on a single promise chain so a burst arrives in a deterministic
order. `addPlaylist$` in `libs/m3u-state` uses `concatMap` (not `switchMap`)
for the same reason: each action carries a different playlist, so a newer add
must never cancel an older one's write, EPG fetch and navigation. The import
itself reuses the normal file path
(`updatePlaylistFromFilePath` → `PlaylistActions.addPlaylist`), so persistence,
playlist-scoped EPG, and the navigation to the new playlist all behave exactly
like a dialog import.
The OS-level registration that makes those paths reachable is
`fileAssociations` in `electron-builder.json` — one entry per extension, each
with its own `mimeType`. Electron Builder derives all three platform
registrations from it: macOS `CFBundleDocumentTypes` (which is what makes
`open-file` fire from Finder), the NSIS registry entries, and, on Linux, the
desktop entry's `MimeType` plus `/usr/share/mime/packages/iptvnator.xml` for
deb/rpm/pacman. Two traps: it assigns the derived `MimeType` _after_ spreading
`linux.desktop.entry`, so declaring `MimeType` there is silently overwritten and
must not be used; and it appends `%U` to `Exec`, so Linux file managers hand
over percent-encoded `file://` URIs rather than paths —
`createPlaylistOpenRequest` decodes them before the extension check. `%U` is
also the _plural_ exec code, so a multi-file selection arrives as one launch
with one argument per file; `extractPlaylistOpenRequestsFromArgv` returns all
of them and `enqueueAll` queues the batch, because stopping at the first match
would silently drop the rest of the selection. Adding an exec code to
`linux.executableArgs` would suppress the `%U` but also pass that code to the
app as a real argument, so it is not an option.
+83 -15
View File
@@ -56,23 +56,70 @@ Omit `pnpm nx migrate --run-migrations` when the first command reports that no
migrations exist. Major Nx updates always use this manual workflow and the
resulting PR runs the full CI pipeline.
## Angular 22 Toolchain Compatibility
Angular framework/Material 22.1 and CLI/build 22.1 use Nx 23.2 and TypeScript
6.0. Framework and CLI patch numbers may differ; keep framework packages aligned
in both dependency sections and all official Nx packages on one exact version.
Use `.nvmrc` for development/CI; supported LTS Node ranges are
`^22.22.3 || ^24.15.0`. Docker uses the supported Node 24 line.
The Angular 22 migration explicitly preserves the old default change detection
with `ChangeDetectionStrategy.Eager`. Existing OnPush components and
`provideZoneChangeDetection` remain in their existing modes.
Existing eager components under Angular ESLint carry a line-scoped explanation
for the newly recommended OnPush rule; new components keep that rule enabled.
Adopting OnPush in those components requires a separate behavior review.
TypeScript 6 migration settings preserve compiler defaults and per-project output roots;
`ignoreDeprecations: "6.0"` temporarily supports legacy Node test configs and
`baseUrl`, rather than changing the whole workspace to browser resolution.
Two third-party packages need scoped compatibility bridges in
`pnpm-workspace.yaml`:
- `nx-electron@22.0.0` still declares Nx 22 peers. Its existing package-layout
patch also changes the removed `@nx/js/src/utils/buildable-libs-utils` import
to `@nx/js/internal`. A package extension supplies its undeclared runtime
dependency `webpack-node-externals`. The packaging suite loads every executor
and checks the single application manifest. Remove these bridges only after
the upstream package declares and implements the current Nx APIs.
- `ngx-indexed-db@22.0.0` declares Angular `<22`, but its used API remains
compatible. Its allowance is scoped to the exact Angular version and must be
revalidated on upgrades. `apps/web-e2e/src/basic.e2e.ts` opens a historical
`iptvnator` v1 database, retains the `_id` key path, and checks reload
persistence. The shared interfaces package declares the same library version.
Remove the allowance when upstream includes the installed Angular version.
Jest uses root Babel 7 while Angular build retains its own Babel 8. The website
uses Astro 7 with the root TypeScript 6; no Astro-specific compiler package
extension is needed. Keep the website build in toolchain validation.
The website's alias uses a relative `./src/*` target without the deprecated
`baseUrl` option.
Electron's webpack build resolves `ts-loader` 9.6.2 through the existing
`nx-electron` version range. Keep at least 9.5.7: earlier versions clear
`rootDir` in transpile-only mode and fail with TypeScript 6 (TS5011/TS6059).
Angular unit-test setup explicitly uses `setupZonelessTestEnv` and separately
imports `zone.js/testing` for existing `fakeAsync` helpers. This preserves the
Angular 21 scheduler used by jest-preset-angular 15: its `setupZoneTestEnv` loaded
Zone but did not provide zone-based change detection on Angular 20+. Preset 17
does provide it, which disables automatic fixture rendering and changes
`whenStable()` behavior. Production applications continue to use their existing
`provideZoneChangeDetection`; browser and Electron E2E validate that runtime.
## Vite Dev-Server Patch
Angular's development builder currently resolves Vite `7.3.6`. That release's
asset and worker transform prefilters can catastrophically backtrack on a large
generated chunk containing unrelated `new URL...` expressions while searching
for a valid `new URL(..., import.meta.url)` construct. The Electron development
server can then fail a lazy chunk request with `Maximum call stack size
exceeded`, even though static builds succeed because they do not pass emitted
chunks through Vite's request-time plugin container.
Angular's development builder resolves Vite `8.1.5`. It contains upstream precise
asset/worker URL matchers, but also uses them as raw-code prefilters. Those
prefilters reject valid expressions containing comments before the handlers can
strip the comments and apply the precise matchers.
`patches/vite@7.3.6.patch` backports Vite's upstream precise-matcher fix from
[vitejs/vite#21800](https://github.com/vitejs/vite/pull/21800). Bounded
prefilters keep the request-time scan linear while allowing comment-bearing
asset and worker expressions to reach the precise matcher after Vite strips
comments. Keep the patch while the supported Angular toolchain resolves Vite
`7.3.6`; remove it only after the resolved Vite contains the upstream fix. Run
the regression check after any related manifest or lockfile update:
`patches/vite@8.1.5.patch` adds bounded asset/worker prefilters and uses the asset
prefilter for bundled URL rewriting too. The precise handler matchers remain
unchanged. This preserves the earlier protection against expensive scanning of
large false-positive chunks and retains comment-bearing expressions. Remove the
patch only when the resolved upstream Vite passes the same behavior checks:
```bash
pnpm run deps:vite:test
@@ -83,6 +130,22 @@ patched prefilter/matcher wiring and version pin, stress-tests the false-positiv
chunk shape, and preserves ordinary and comment-bearing asset and worker
`new URL(..., import.meta.url)` matches.
## Electron Builder signing patch
`app-builder-lib` 26.15.7 is patched in
`patches/app-builder-lib@26.15.7.patch` with the upstream backport
electron-userland/electron-builder#10172. For macOS signing,
`security set-key-partition-list -k` must receive the temporary keychain's own
password rather than the `.p12` import password. macOS runner images since
`macos-26-arm64` 20260831 verify that password; the old argument caused
`SecKeychainUnlock: The user name or passphrase you entered is not correct`.
Keep the patch until electron-builder resolves a fixed app-builder-lib (26.16.1+).
Run `pnpm run deps:electron-builder:test` after related dependency updates; it
also rejects a mismatch between the patched and installed version.
Native addon builds additionally require the root `node-gyp` devDependency;
see [runtime staging](embedded-mpv-native.md#runtime-staging) before removing it.
## Placement Decision
- `apps/` owns runtime applications, development servers, E2E applications,
@@ -163,7 +226,12 @@ the lazy workspace shell feature into the initial bundle.
Use scoped aliases from `tsconfig.base.json` and expose public imports through a
library's `src/index.ts`. Do not introduce legacy bare aliases such as
`services`, `components`, `shared-interfaces`, or `database`, and avoid deep
imports unless a sub-entrypoint is explicitly configured.
imports unless a sub-entrypoint is explicitly configured. The configured
sub-entrypoints are `@iptvnator/shared/interfaces/ipc-commands` and
`@iptvnator/shared/interfaces/zoom-level`, both for the Electron preload: the
`@iptvnator/shared/interfaces` barrel pulls in `ngx-indexed-db`, which the
preload bundle must not carry, so the preload only type-imports the barrel
and value-imports those two dependency-free modules directly.
For a buildable library that has a local `package.json`, its `name` must match
the scoped alias. Nx uses that package name when rewriting buildable dependency
+158 -20
View File
@@ -7,6 +7,9 @@ Embedded MPV rendering and native-view bounds behavior remain documented in
## Current status
The shared-controls preference checkbox is visible only when HTML5, Video.js
or ArtPlayer is selected in Settings → Playback.
The shared-controls foundation supports four runtime consumers and includes:
- the `PlayerController` contract, default state, and capability presets;
@@ -399,11 +402,21 @@ is rendered by `WebPlayerViewComponent` as a sibling of the engine and staged
on the view's `fullscreenSurface` — the same host element every engine
receives as `fullscreenTarget` — so it sits inside the fullscreen element and
survives the engine remount a channel switch causes.
It injects `FULLSCREEN_CHANNEL_PANEL` optionally: a live host provides
`FullscreenChannelPanelHost` (`panelTemplate`, optional `panelTitle`) from its
component `providers`, and the panel stamps that template into its body with a
`FullscreenChannelPanelContext` of `{ searchTerm: Signal<string>, close }`.
Without a provider — VOD detail pages, series playback — nothing renders. The
It is a host-agnostic side panel — a channel list for live hosts, an episode
list for series playback (see "Fullscreen episode panel" below) — and keeps
its `FULLSCREEN_CHANNEL_PANEL` name for history. It injects the token
optionally: a host provides `FullscreenChannelPanelHost` from its component
`providers` — `panelTemplate`, optional `panelTitle`, optional
`panelSearchEnabled` (default true; false drops the header's search field and
shows the title as a text row in its place, keeping the header height) and
optional `panelKind` (`'channels'` | `'episodes'`, only the accessible name
of the list and the close button's label; every pointer and keyboard rule
is identical for both) — and the panel stamps that template into its body
with a `FullscreenChannelPanelContext` of
`{ searchTerm: Signal<string>, open: Signal<boolean>, close }`. `open` lets a
body that stays mounted between openings react to the panel coming up (the
episode list centres its playing row on it). Without a provider — a movie in
a VOD detail page — nothing renders. The
host resolves the user preference itself: `Settings.fullscreenChannelPanel`
(default on, Settings → Playback, offered for the web players while the
shared controls are on and for Embedded MPV — the legacy vendor chrome
@@ -505,22 +518,44 @@ engine on first entry; a confirmed frame-copy capability survives an unknown
probe during an MPV application remount, preserving the open panel and its
search/scroll state. A confirmed native/unsupported result revokes it, and
switching to a web engine clears the remembered MPV capability). Nothing is
drawn over the video while the panel is closed — there is
no handle or hint. An invisible 28px hot zone (40px on coarse pointers) on the
drawn over the video while the panel is closed and the pointer rests. Mouse
movement over the stage (the fullscreen element) reveals a slim, pointer-
transparent hint tab on the left edge — a CSS chevron, no icon glyph or text
— that fades `CHANNEL_PANEL_HINT_IDLE_MS` (2.5 s) after the last move, so it
comes and goes with the controls chrome; it lights up (`--armed`) while the
pointer rests in the hot zone, and it is not rendered while the panel is
open. Touch movement never reveals it. The hint answered the first field
report: the zone was an invisible strip with nothing telling the user where
the list lived. An invisible 40px hot zone (48px on coarse pointers) on the
left edge opens the panel after a 160ms mouse dwell; a sweep across the edge
is ignored. The zone remains mounted above the scrim and below the panel while
open, preserving the pointer target until the opening animation covers it.
A delayed fullscreen paint therefore cannot turn a stationary edge hover into
a synthetic mouse-leave that closes the panel. The zone stops above the controls bar (`bottom: max(25%, 140px)`)
so the leftmost transport button never loses a click or tap to it. The `C` key opens it too and focuses the search field (hover does
not steal focus). Touch has neither hover nor a `C` key, so a tap on the hot
zone opens the panel at once: the handler is bound to `pointerup`, not
`pointerdown`, so the hot zone is still the tap's click target and the click
that follows dies on it instead of reaching the video. It closes when the
mouse leaves the panel for 420ms, on the close button (tooltip names Escape),
on Escape, on the host's `close`, or through a transparent scrim
over the video that swallows the click so the player's click-to-pause never
sees it. The Escape that closes the panel is consumed (`preventDefault`):
is ignored, and a click or tap on the zone opens at once without the dwell —
only a primary press that began inside the zone and is released there
(`pointerdown` records the pointer, `pointerup` must match it; a drag
released over the edge, a right or middle button, or a pen barrel button
never opens). The synthetic `pointerenter` that follows an explicit close
neither opens nor arms the hint: the zone re-arms on the next real
`pointermove`. The zone remains mounted above the
scrim and below the panel while open, preserving the pointer target until
the opening animation covers it. A delayed fullscreen paint therefore cannot
turn a stationary edge hover into a synthetic mouse-leave that closes the
panel. The zone stops above the controls bar (`bottom: max(25%, 140px)`) so
the leftmost transport button never loses a click or tap to it. The `C` key
opens it too and focuses the search field — or, for a host without one, the
panel itself (`tabindex="-1"`), so the next Tab reaches its first control
(hover does not steal focus).
Touch has neither hover nor a `C` key, so the tap path above is its way in:
the handler is bound to `pointerup`, not `pointerdown`, so the hot zone is
still the tap's click target and the click that follows dies on it instead
of reaching the video. It closes when the mouse leaves the panel for
`CHANNEL_PANEL_CLOSE_GRACE_MS` (1 s) — but only once the pointer has engaged
with the panel: a hover-opened panel counts as engaged from the start, while
a `C`-opened one ignores the mouse roaming over the video until it has
visited the list, so the shortcut never leaves the user typing into a closing
search field (`FullscreenChannelPanelState.show(opener)`) — on the close
button (tooltip names Escape), on Escape, on the host's `close`, or through
a transparent scrim over the video that swallows the click so the player's
click-to-pause never sees it. Clicks inside the panel never close it: the
panel sits above the scrim, so no in-panel hit can reach it. The Escape that closes the panel is consumed (`preventDefault`):
Electron leaves HTML fullscreen on an unhandled Escape, and the close
shortcut must only slide the panel away; a closed panel leaves Escape alone,
so the key still exits fullscreen then. A CDK overlay the list opens (sort menu, row context menu) renders in
@@ -555,6 +590,76 @@ without it every tooltip, sort menu and context menu opened while fullscreen —
the panel's view-switcher tooltips and row context menus included — would sit
invisibly under the top layer.
#### Fullscreen episode panel (series)
Series playback gets the same panel as an **episode list**. The provider is
`PortalInlinePlayerComponent` (`libs/ui/playback/src/lib/portal-inline-player/`),
the component both series hosts (Xtream `SerialDetailsComponent`, Stalker
`StalkerSeriesViewComponent`) render around `app-web-player-view` and that
already feeds the Up Next rail — so it is the nearest provider for the nested
view, which also shields that view from a page-level channel-list provider
(the M3U player's) while its VOD detail hosts the player. The host object
itself is built by `createEpisodePanelHost()`
(`portal-inline-player-episode-panel.host.ts`) from the component's inputs
and exposed as `episodePanel`, so the player's own responsibilities stay
readable. It declares `panelKind: 'episodes'` and `panelSearchEnabled:
false`: season tabs are the navigation, and the header shows the series
title (`seriesTitle`, else the playback title) where the search field would
be.
- Data: the hosts pass their season→episodes map (`seriesEpisodes`, the same
`Record<seasonKey, XtreamSerieEpisode[]>` the season container gets, so the
TMDB overlay's stills and overviews ride in `info`), the per-episode
playback-position map (`episodePlaybackPositions`) and, for Stalker lazy
VOD series, per-season load states (`seasonLoadStates`: `loading` while
a request is on the wire, `unloaded` while the portal has not answered —
after a failed request too; `vodSeasonLoadStates` on the host, computed
by `getVodSeasonLoadStates()` in `@iptvnator/portal/stalker/data-access`).
`buildFullscreenEpisodePanelSeasons()`
(`libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.util.ts`)
turns them into `FullscreenEpisodePanelSeason[]` — numeric keys ascending,
named keys after, each row a `FullscreenEpisodePanelItem` that extends the
Up Next entry with `seasonKey`, `episodeNumber`, `overview`, a "45 min"
`durationLabel` (numeric `duration_secs`, else the portals' text forms) and
the shared ≥90 % `watched` rule; the playing row is the one whose id equals
`contentInfo.contentXtreamId`.
- Body: `app-fullscreen-episode-panel` stamps `SeasonTabsComponent` (the
detail page's tabs: pills up to six seasons, a dropdown beyond, watched
check marks, the "Back to playing episode" chip) over the selected season's
rows: a 16:9 still or, without one, a large numeral tile so the no-TMDB
case still looks designed; the `S01E03` label, runtime, watched check or
"Now playing" marker; the title (label as fallback); a 3-line clamped
overview when there is one; a progress bar on the thumbnail. Rows are
buttons inside `<li>`s of a `<ul>`, so their button role survives for
assistive technology. A loading season shows a spinner row, a loaded empty
one the season-empty copy, and an unanswered one a "could not be loaded"
row with a Retry button that re-emits the season selection — the tabs
never re-emit an already selected key, so a failed lazy load would
otherwise be stuck.
- Selection: the tab follows the playing episode's season (`linkedSignal`)
and resets to it whenever playback moves into another season; a tab the
user picks holds until then. Opening the panel (context `open`) centres
the playing row inside the list's own scroll box — `scrollTop` math, never
`scrollIntoView`, so the stage and page are not scrolled with it — and the
effect depends on primitives only (open flag, shown season key, playing
season key and episode id), so the season objects a progress tick rebuilds
never yank a list the user is scrolling.
- Actions: an episode click emits the item through the inline player's
`upNextEpisodeSelected` — the Up Next rail's output, so the host plays it
through its inline episode flow and the engine remount keeps fullscreen
exactly as a "next episode" does — and then calls the context's `close`;
a click on the playing row is inert. A season tab click emits
`episodePanelSeasonSelected` (as does the retry row), wired by both hosts
to the same `onSeasonSelected` their season container uses, so Xtream's
TMDB season enrichment and Stalker's lazy VOD season load run for the
panel's season too.
- Gates: `Settings.fullscreenChannelPanel` (one setting for channels and
episodes; its label reads "Channel and episode list in fullscreen"),
`contentInfo.contentType === 'episode'` and non-live playback (a movie
never gets the panel), and at least one season. Native-view Embedded MPV is
withheld by the view's `enabled` input as for channels; external MPV/VLC
never mount the inline player, so they are excluded by construction.
### Keyboard ownership
Unmodified Space/K, F, arrow keys, and M are playback shortcuts. Playback keys
@@ -1431,3 +1536,36 @@ replacement, track-list lifecycle and stable IDs, caption preference and
explicit-off behavior, MPEG-TS live/VOD handling and duration projection,
volume preservation/authority, stale ArtPlayer `customType` callbacks, and
collaborator teardown. Persistent/background player ownership has not landed.
## Radio and display sleep
### Radio audio player
M3U `radio="true"` entries use `AudioPlayerComponent` under
`libs/ui/playback/src/lib/audio-player/`. The player always renders inline and
uses HTML5 `<audio>` regardless of the configured video player. Radio bypasses
`shouldShowInlinePlayer`'s external-player gate and hides the EPG ribbon and panel
toggle. The station artwork, blurred logo background and glass controls form the
radio layout; title/group scrolling is CSS-only. It supports play/pause, mute,
and volume, including the volume keys in 5% steps. Volume shares the video
players' `volume` localStorage key. The template, SCSS and TypeScript component
live together; routing/integration stays in the M3U player template.
### Display sleep during playback
`PlaybackKeepAwakeService` in the web app watches `<video>` using document-level
capture listeners because media events do not bubble. Release listeners also
attach to the tracked element: Chromium's pause after DOM removal never reaches
the document. A playing video holds a display-sleep lock only while the document
is visible or that video is in picture-in-picture, which survives minimization.
Electron uses main-process `powerSaveBlocker` through
`window.electron.setPlaybackKeepAwake`. The renderer vote clears on reload,
main-frame non-same-document navigation, crash (`render-process-gone`) or
destruction; Angular navigation does not itself clear it. The PWA uses Screen
Wake Lock. Browser auto-release clears its sentinel; the next media, visibility
or PiP synchronization can request another lock. If state changes during a
pending request, rejection triggers one queued re-evaluation rather than losing
that update. Radio `<audio>` deliberately never blocks display sleep.
Embedded MPV owns a separate blocker in `EmbeddedMpvNativeService`, and external
MPV/VLC inhibit their own screensaver.
+137 -21
View File
@@ -30,10 +30,14 @@ The button has an opaque app-themed surface, visible keyboard focus, an Escape
shortcut hint via native `title` and Electron `no-drag` hit testing. The hint
does not create an overlay that could consume the first Escape press.
The sticky control and Escape unwind one level: watch emits
`closePlayerRequested`, browse emits `backClicked`. Hosts retain their existing
route/inline/collection return behavior. The now-playing bar's separate route
Back action still returns directly to the list. Browse Escape requires focus
The sticky control is route-level Back in both states: it emits `backClicked`
whether or not inline playback is active, so the arrow keeps one meaning and
the list is one click away while watching. Only Escape unwinds one level: watch
emits `closePlayerRequested`, browse emits `backClicked`. Hosts retain their
existing route/inline/collection return behavior. The now-playing bar carries
no second arrow; its "Close player" button is the pointer counterpart of the
watch Escape (two arrows with different meanings, and two controls for one
action, were the pre-#1576 confusion this replaces). Browse Escape requires focus
inside this shell; watch keeps the existing global close shortcut, including
M3U playback started from its sidebar. Handled events, key repeats/modifiers,
editable fields, inert/hidden shells, fullscreen, dialogs and menus are ignored.
@@ -41,13 +45,15 @@ Escape bubbles through the shell before Material's body-level tooltip dispatcher
so focused detail actions return with one press even while their tooltip is open.
The document listener remains the outside-shell watch fallback; `defaultPrevented`
prevents duplicate actions and preserves descendant handlers' priority.
After closing a player, lost focus moves to the sticky control (or the shell
when there is no browse Back), without scrolling or stealing existing focus.
After Escape closes a player, lost focus moves to the sticky control (or the
shell when there is no browse Back), without scrolling or stealing existing
focus.
Hosts without browse navigation set `backAvailable=false`: M3U uses its channel
sidebar, and collection bootstrap placeholders have no return handler. They
have no browse button or browse Escape action; M3U watch still offers Close
player. Loading/error shells with a return handler keep Back available.
render no sticky arrow in either state and have no browse Escape action; their
watch exits are the bar's Close player button and Escape. Loading/error shells
with a return handler keep Back available.
## Summary
@@ -55,9 +61,9 @@ player. Loading/error shells with a return handler keep Back available.
- Stalker uses an inline/store-state detail model.
- Detail pages themselves are two-state (browse ↔ watch) inside
`PortalDetailShellComponent`; entering/leaving watch is a layout state,
not a navigation. Route-level back semantics are unchanged; the
sticky watch control closes the inline player, while the now-playing bar
retains its separate direct return to the list. See
not a navigation. Route-level back semantics are unchanged; the one
sticky arrow returns to the list from either state, while Escape and the
now-playing bar's Close button close the inline player. See
[Embedded Inline Playback](./embedded-inline-playback.md).
- Favorites and recently viewed collections now use collection-owned inline detail
for non-live Xtream and Stalker items.
@@ -68,13 +74,27 @@ player. Loading/error shells with a return handler keep Back available.
Stalker movies/series into the matching global collection route with detail
pre-opened.
- The dashboard hero CTA and the Continue Watching cards' explicit "Resume
episode" ⋮ action for Xtream series carry a one-shot season/episode resume
target. The collection-owned Xtream detail consumes it after its episode
positions load and starts that exact episode; the cards' default click is
detail-only (movie-like), as is opening the series from the collection grid
itself. If the positions load fails, the target stays unconsumed and the
handoff degrades to detail-only rather than starting the episode at offset
zero. Continue Watching cards also expose "Mark as Watched" (maxes out the
episode" ⋮ action for Xtream and Stalker series carry a one-shot
season/episode resume target. The collection-owned detail consumes it after
its episode positions load and starts that exact episode; the cards' default
click is detail-only (movie-like), as is opening the series from the
collection grid itself. If the positions load fails, the target stays
unconsumed and the handoff degrades to detail-only rather than starting the
episode at offset zero. Xtream reads it from `XTREAM_SERIES_RESUME_TARGET`;
Stalker from `STALKER_SERIES_RESUME_TARGET`, provided by
`StalkerCollectionDetailComponent` and consumed by
`StalkerSeriesViewComponent` (`stalker-series-resume.ts`), which first
hydrates the lazy Ministra season the target lives in. The two Stalker
shapes differ in the handoff state: a lazy `is_series` row carries
`contentType: 'series'` (the flag is read by `extractStalkerItemType`) and
`resolveStalkerCollectionDetailMode` sends it through the VOD detail flow
without changing that type, while an embedded-VOD row — a `series[]`
episode array and no flag — carries `contentType: 'movie'`, because the
type resolver is deliberately blind to that array so the item keeps
routing to the VOD catalog. Only the detail, reading the stored row, can
tell the second shape from a real movie, so
`getOpenCollectionDetailItemState` keeps a resume target for any non-live
Stalker item, not only `contentType: 'series'`. Continue Watching cards also expose "Mark as Watched" (maxes out the
tracked position row) and "Remove from history" in the same ⋮ menu.
- Ready Download Manager cards open one of the three focused
`downloads/:downloadId` routes. These local details hide the workspace
@@ -130,6 +150,101 @@ player. Loading/error shells with a return handler keep Back available.
and those rows keep the history return as well. The marker is set only by this
builder and only alongside `returnTo`, so the dashboard handoff and every
other `stalkerReturnTo` caller keeps its re-navigating behaviour.
- Live channels in the same collections (the unified live tab of global
favorites/recent and a portal's own favorites/recent tabs) get the live
counterpart of that bridge, "open in playlist". Targets come from
`getLiveCollectionPlaylistNavigation()` (`libs/portal/shared/util`), which
lands on the channel, never on the playlist root: Xtream reuses
`buildXtreamNavigationTarget` and its `openXtreamLiveItemId` state, which
`LiveStreamAutoOpenStateService` turns into a selected, playing channel.
The layout reads that state once at mount and again on every
`NavigationEnd`: arriving from another route, the Xtream shell mounts the
layout only after its session bootstrap, i.e. after the arrival's
`NavigationEnd`, which a subscription made in the layout can never observe
(the handoff used to be silently lost for every cross-route arrival), while
the event read still covers re-navigation to the same `/live` route with the
component reused. The state also carries `openXtreamLivePlaylistId`: `NavigationEnd` fires
before the route session resets the shared `XtreamStore`, so at capture
time `liveStreams()` can still be the PREVIOUS playlist's catalog, and the
live layout used to read "not in this list" as "channel gone" and drop the
handoff (a jump from a collection or global search into another portal then
landed on the live root with nothing selected). The layout now consults the
catalog at all only once `currentPlaylist()` is the requested playlist —
stream ids are provider-local, so a colliding id in the previous catalog
would otherwise play the wrong channel — and treats a miss as final only
once `isContentInitialized()` is true; until then it keeps waiting and the
effect re-runs as the store switches and loads;
M3U navigates to `/workspace/playlists/:id/all` with `openM3uChannelUrl`
(`OPEN_M3U_CHANNEL_URL_STATE_KEY`), the same key global search writes and
the M3U player selects by URL; Stalker navigates to `/workspace/stalker/:id/itv`
with `openStalkerLiveItemId` + `openStalkerLivePlaylistId` (+ the row's
genre as `openStalkerLiveCategoryId` when known) from
`buildStalkerLiveNavigationTarget`, which `StalkerLiveAutoOpen`
(`stalker-live-stream-layout/stalker-live-auto-open.ts`, the Stalker
counterpart of the Xtream service + effect) consumes: it reads the state at
construction and on every `NavigationEnd`, waits until `currentPlaylist` is
the requested portal (channel ids are provider-local, so a colliding id in
the previous portal's list must never match), then locates the channel in
the full ITV channel list cache — `get_ordered_list` is server-paged, so
the row may sit on any page of its genre — selects that genre (`'*'` for a
channel without one), expands the rail and plays it. While the list loads it
waits (the cache turning ready re-runs the effect); a portal that cannot
serve a full list (`itvFullListUnsupported`, the cache's reactive
unsupported set), a load that fails transiently (the cache only arms a
retry cooldown and changes no signal, so the flow awaits the preload
promise and treats "settled, neither ready nor unsupported" as the same
outcome) or a channel missing from the list (censored genres are excluded
from `get_all_channels`) falls back to selecting the remembered genre, so
the user still lands in the right list, and the handoff is consumed either
way. That remembered genre is the stored row's `tv_genre_id` (an opaque
portal id, numeric on most panels but not all); the row's `categoryId`
counts only when it is not a section marker, because app-written
favorites/recent rows carry `'itv'` there. Playback is deferred whenever
selecting the genre changes the list scope (another genre, the All Items
grid — `null`, a different row source from the `'*'` All list — or an
active search): `playChannel` → `navigation.prepare` captures the
displayed rows as the remote/numeric channel order, and the store serves a
category a tick after `setSelectedCategory` — even from the full-list
cache — so playing right away would capture the previous scope's queue.
The store answers "whose channels are on screen?" with
`itvChannelsCategory` (set wherever `itvChannels` is served, cleared by
`setItvChannels`), which is what the deferred play waits for. Array
identity cannot answer it: filtering by `'*'` hands back the cache by
reference, and clearing a search replaces the rendered list without the
source moving. Clearing the search IS synchronous, so a genre already on
screen plays at once. A pending play is dropped when a newer handoff
arrives, when the user switches portal, genre or section or starts a
search, and when the layout is destroyed. Two things settle before any of
that: `StalkerWorkspaceRouteSession.isReady` (its sync resets the selected
category and item for the arrival, and the store keeps the previous
portal's playlist and cache across a revisit, so the playlist check alone
passes too early and the selection would be wiped a tick later), and the
user — the handoff is abandoned when the genre or search changes away from
what it captured once it became actionable, which is measured after the
session's own resets so they never read as a user action. Readiness is
published by the NEWEST sync only, and never before the store holds that
portal's row: the session applies arrivals one at a time and claims the
playlist id only after `setCurrentPlaylist()` resolves. The constructor
starts a sync before the first `NavigationEnd` starts another, so two run
at once — the second used to find the id already claimed, skip the
bootstrap and report ready while the first was still awaiting that write,
which let a revisited same-id portal whose endpoint or credentials had
changed play against the PREVIOUS row. A failed bootstrap leaves readiness
false rather than handing the arrival a stale row. Stalker radio stations resolve to `null`: they live in the separate
`radio` section, whose station list is legacy-paged with no
open-on-arrival contract, so the action stays hidden for them.
Two surfaces render the one verdict: `app-open-in-playlist-chip`
(`libs/portal/shared/ui`), projected into the EPG timeline / list-view
toolbar through the panels' `[epgToolbarAction]` content slot beside the
channel name (the "where is this from" answer sits on the now-playing
surface and survives the collapsed state; it is absent for radio, which
has no EPG panel, and without EPG support), and an "Open in <playlist>"
entry in `app-global-favorites-list`'s row context menu
(`openInPlaylistRequested`), which also covers radio rows and rows that
are not playing. Both label with `playlistDisplayLabel` (a stored playlist
name can be a URL carrying credentials) and reuse
`PORTALS.VIEW_IN_PORTAL_TOOLTIP`; `UnifiedLiveTabComponent` performs the
navigation. A row whose target does not resolve shows neither.
- Do not force both portals into the same browse/detail behavior unless the full
portal detail architecture is being changed.
@@ -180,9 +295,10 @@ Dashboard behavior to preserve:
Xtream movie/series items into `/workspace/global-favorites` or
`/workspace/global-recent` with collection detail pre-opened from navigation
state.
- When an Xtream series recent has a saved episode position, the dashboard hero
and Continue Watching card should include that exact series/episode target in
the navigation state. It is a one-shot playback request and must not leak into
- When a series recent (Xtream, or a Stalker row that
`resolvePortalActivityWatchKind` answers `series` for) has a saved episode
position, the dashboard hero and Continue Watching card should include that
exact series/episode target in the navigation state. It is a one-shot playback request and must not leak into
normal favorites, search, category, or collection-grid navigation. Only
position rows that name their parent `seriesXtreamId` produce a target:
episode-keyed recents make `item.xtream_id` an episode id, so legacy rows
+22
View File
@@ -277,3 +277,25 @@ For manual Docker smoke testing, run the Xtream and Stalker mock servers plus a
small M3U fixture, then verify in the browser that M3U, Xtream, and Stalker can
add sources, play an item, toggle favorites, populate global favorites,
populate recently viewed, and appear on the dashboard rails.
## Service factory and build bases
`DataService` in `libs/services/src/lib/data.service.ts` is the renderer service
contract. `DataFactory()` in `apps/web/src/app/app.config.ts` chooses
`ElectronService` for the desktop bridge and `PwaService` for browser HTTP and
IndexedDB work. This environment-level selection is not evidence for an
individual capability: Xtream data-source selection requires its complete
SQLite bridge, and feature visibility follows `RuntimeCapabilitiesService`.
The same workspace route tree is used in both runtimes.
Desktop relational data uses the canonical schema/connection in
`libs/shared/database`; the SQLite path is `~/.iptvnator/databases/iptvnator.db`.
Desktop Chromium settings/storage still exist alongside SQLite. PWA data uses
browser IndexedDB (with browser quotas); its structure is not the SQL schema.
Browser-selected file uploads remain possible even though native filesystem
access is Electron-only.
Web development and PWA use `baseHref="/"`; packaged Electron frontend uses
`baseHref="./"` so file URLs resolve. In `apps/web/project.json`, `production`
is the Electron frontend build, `pwa` is the web build, and `development` uses
the index base. Do not ship the Electron frontend build as a PWA deployment.
+154
View File
@@ -136,6 +136,11 @@ fails on an unused import.
## Highlight cards
For an imagegen announcement cover, use the reusable
[release cover artwork recipe](../development/release-cover-artwork.md).
It preserves the selected visual direction and exact 0.24 prompt; the
deterministic cards and screenshot requirements below still apply.
`tools/release/highlight-cards.mjs` plans and lays out;
`tools/release/generate-highlight-cards.mjs` renders through sharp. Output is 1200×630 (Open Graph), matching the website
palette in `apps/website/tailwind.config.mjs`.
@@ -276,6 +281,155 @@ References: [AppImage desktop keys](https://docs.appimage.org/reference/desktop-
[AppManager desktop parser](https://github.com/kem-a/AppManager/blob/v3.8.0/src/core/desktop_entry.vala),
[AppManager updater](https://github.com/kem-a/AppManager/blob/v3.8.0/src/core/updater.vala).
## Rolling test drafts
Every non-fork PR build publishes its artifacts to a rolling **draft** release
tagged `test-pr-<n>`; a non-PR, non-tag build (a dispatch on a branch) uses
`test-<branch>`, the shape master pushes used before the nightly channel took
over. The tag is stable per PR, so the draft is updated in place and a PR has
at most one.
`cleanup-pr-draft.yml` deletes a PR's draft when the PR closes. That event is
the fast path, not a guarantee: GitHub does not run a `pull_request: closed`
workflow when the head ref is already gone at event time, which is what
Dependabot does when it supersedes one of its own PRs — 15 drafts were
orphaned that way before this was noticed. A daily scheduled sweep in the same
workflow (also runnable with `gh workflow run cleanup-pr-draft.yml`) therefore
lists every `test-pr-<n>` draft, asks GitHub for that PR's live state, and
deletes the draft only when the PR is closed; anything else — an open PR, a
lookup failure, a `test-<branch>` draft — is left alone.
## Nightly channel
Every push to `master` of `4gray/iptvnator` is also a nightly. The same
`build-and-make.yaml` run that builds the matrix publishes its artifacts as a
**prerelease of `4gray/iptvnator-nightly`** instead of the rolling
`test-master` draft: a draft is invisible to anyone without write access and
to electron-updater, while a published prerelease is what the desktop app's
**Nightly** update channel installs. PR builds keep their `test-pr-<n>`
drafts; tag builds are unaffected.
**Version.** Each build job rewrites the `package.json` version before the
frontend and backend builds and before electron-builder reads it
(`tools/release/nightly-version.mjs --apply`):
```
0.23.0 → 0.23.1-nightly.20260915.1234
└ next patch ┘ └ commit date ┘ └ run number ┘
```
- Greater than the released `0.23.0`, so a stable user who switches channels
is offered it; smaller than `0.23.1` and `0.24.0`, so the next stable
release is offered to nightly users on either channel.
- The run number only grows, so nightlies order correctly within a day.
- The same `--apply` sets `publish[0].channel: nightly` in
`electron-builder.json`, which names the updater metadata
`nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`. electron-builder does
not derive that name from the prerelease tag for the GitHub provider (the
first nightly run produced `latest-*.yml` and the publish step refused
it). The artifact upload globs and the macOS metadata merge accept both
names.
- The root `package.json` is an Nx `sharedGlobals` input, so the rewritten
version reaches the `web` and `electron-backend` bundles (which embed it)
instead of a cache hit built from the released version.
- The base is the version in `package.json`. The patch is bumped only when
`v<base>` already exists on origin. A release cut commits the bump before
(or together with) its tag, and while that tag is missing the base is the
UPCOMING release, so the nightly keeps its patch
(`0.23.1` untagged → `0.23.1-nightly.<date>.<run>`): still above every
earlier nightly, still below the imminent `0.23.1`, so nightly users are
offered that release instead of skipping it.
- The version is computed once, in the leading `nightly-version` job, and
handed to every build job as `--version` — a tag pushed while the matrix
runs cannot give one run two different versions. The release job reads
the same output to name the tag `v<version>`.
**Publication** (steps at the end of the `create-release` job):
1. `NIGHTLY_RELEASE_TOKEN` — a fine-grained PAT with *Contents: read/write*
on `4gray/iptvnator-nightly` — is required. Without it the run only warns;
`GITHUB_TOKEN` cannot write to another repository. The nightly repository
needs one commit on its default branch, because `gh release create`
creates the release tag there.
2. The notes list the master commits since the previous nightly. That
nightly's source commit is read back from the `<!-- iptvnator-commit: … -->`
marker its own notes carry, then `compare` on the main repository (with
`GITHUB_TOKEN`) lists the range. A missing marker or a rewritten history
only drops the list.
3. The release is created as a draft, assets are uploaded, then it is
published in one edit, so electron-updater never sees a release whose
channel file is still missing. Missing `nightly-mac.yml`,
`nightly.yml` or `nightly-linux.yml` fails the step instead. A published
release is never deleted by a re-run: re-running after a successful
publish is a no-op, and only a draft left behind by a failed run is
replaced.
4. The release job is serialized per ref, but two master runs can finish out
of order. electron-updater takes the newest feed entry, so a nightly older
than the newest published one is dropped rather than published.
5. Only the newest `NIGHTLY_KEEP_RELEASES` (20) nightlies are kept; older
ones are deleted together with their tags.
**In the app.** `Settings.updateChannel` (`stable` / `nightly`, default
`stable`) is a normal renderer setting mirrored into the main-process config
by `SETTINGS_UPDATE` (`APP_UPDATE_CHANNEL`), because the startup check runs
before the renderer exists. `AppUpdateService` re-points electron-updater on
every check (`app-update-feed.ts`): the GitHub feed URL of the channel's
repository, `allowPrerelease` only for nightly, the channel name (`nightly`,
or the explicit `latest` for stable — the setter refuses `null` once set),
and `allowDowngrade = false` reset afterwards, since assigning a channel
silently enables downgrades and the constructor enables prereleases for any
prerelease build. Release notes and the manual-install fallback (Linux
without AppImage) read the channel's release list; notes for a nightly
version always come from the nightly repository, so a nightly build on the
stable channel still shows its own notes. `AppUpdateReleaseCatalogs`
(`app-update-release-notes.ts`) owns one catalog per channel and both reads
the updater performs on them (release notes with paging, newest release for
the manual-install fallback); the service only delegates. Each catalog is a
snapshot of the GitHub release list kept for the whole process, so `findIndex` reloads it
once when a version is missing from a fully paged list — the updater had
offered a nightly published after the catalog was first read, and "What's
new" answered "not found" for it — and `handleUpdateAvailable` drops every
catalog, since a newly found release proves the snapshots stale. Readers
of one catalog are serialized through `runExclusive` (both
`getReleaseNotes` and the manual-update check): a read dereferences an
index into `releases` after awaiting further pages, and that reload
rebuilds the array, so two overlapping readers must never interleave. The
not-found rejection carries the shared
`APP_UPDATE_RELEASE_NOTES_NOT_FOUND_MARKER` text: `ipcRenderer.invoke`
strips custom properties off rejections, so the dialog recognises the case
by that text, shows localized copy with the version, and links to the
channel's release list (`appUpdateReleasesListUrl`) instead of printing the
IPC wrapper; every other failure keeps its underlying reason under a
localized headline.
The About section keeps the select honest about that Save boundary. The
status block carries a badge naming the channel the verdict describes
(`status.verdictChannel`, see below), and while the select shows a channel
other than the saved one (`status.channel`) the
verdict is dimmed, a hint names both channels, and the plain "Check again"
button is replaced by a primary "Save and check for <channel> updates"
button that submits the settings form
(`SettingsAboutSectionComponent.saveAndCheckForAppUpdate` →
`SettingsComponent.onSubmit()`). No renderer-side check follows: the saved
channel reaches `persistAppUpdateChannel`, whose change listener calls
`AppUpdateService.setChannel`, which already re-checks an idle updater. A
download in flight or finished belongs to the previous channel and is kept
by `setChannel`, so in those states the plain check stays and only the hint
is shown. Because that kept download outlives the channel it was found on,
every check stamps `status.verdictChannel` with the channel it ran on and
`setChannel` leaves it alone: the badge names `verdictChannel`, not
`channel`, and while the two differ a hint says the shown update came from
the other channel and the saved one has not been checked yet. Checking the unsaved channel without saving was rejected on
purpose: the updater would then offer a download for a channel that is not
persisted.
Switching is forward-only on purpose: a nightly build stays installed until
a newer stable release exists, because a downgrade could land on a release
that does not understand the database schema a nightly migration already
applied. Nightly users therefore accept that everything merged into master
is de facto shipped — a migration on master can only be followed by another
migration, never reworked.
## After verification
Publishing the GitHub release is manual. That publication automatically
+11 -9
View File
@@ -374,15 +374,17 @@ webServer: [
Playwright waits for every server to be healthy before starting tests. If one is already running (e.g. in local dev), it reuses the existing instance.
**`MOCK_PORT` moves the CLIENT side only** — Playwright's health-check URL and
the `MOCK_SERVER` constants in the specs. The server's own port comes from
`PORT` (`main.ts`), which the `serve` and `serve-with-watch` targets pin to
`3210` in `project.json`, and nothing maps one variable to the other. Setting
`MOCK_PORT` alone therefore points Playwright at a port nothing is listening
on and the run times out waiting for `/health`. It is only useful against a
mock you started yourself on that port (`reuseExistingServer` is on outside
CI); relocating the Nx-managed one would need `MOCK_PORT` passed through as
`PORT`.
**`MOCK_PORT` relocates the whole run.** Playwright's health-check URL and the
`MOCK_SERVER` constants in the specs read it, and `main.ts` resolves the
server's port as `PORT`, then `MOCK_PORT`, then `3210` — so
`MOCK_PORT=3310 pnpm exec playwright test …` starts the Nx-managed mock on
3310 and points every spec at it, which is how two worktrees run E2E side by
side when one already holds 3210. This only works because the `serve` and
`serve-with-watch` targets no longer pin `PORT` in `project.json`: an `env`
entry in `nx:run-commands` overrides the shell (`{...process.env, ...env}`),
so a pinned value silently discarded every override. `PORT=3310 pnpm nx run
stalker-mock-server:serve` relocates the server alone. The Xtream mock has the
same shape with `XTREAM_MOCK_PORT`.
### Test Isolation
+6
View File
@@ -1278,6 +1278,12 @@ The Stalker live route and radio route intentionally share
in the channel header. Remote up/down, numeric selection and status retain
the captured playback order while browsing. See the
[queue and reveal contract](./remote-control.md#live-channel-return-and-playback-order).
- Arrival with `openStalkerLiveItemId` in history state (a collection's
"open in playlist") is served by `StalkerLiveAutoOpen`
(`stalker-live-auto-open.ts`): once the store serves the requested portal
and the full ITV list (below) is ready, the channel is located there, its
genre selected and playback started; see
[Portal Detail Navigation](./portal-detail-navigation.md).
## Full ITV Channel List Cache
+114 -27
View File
@@ -24,7 +24,8 @@ Related:
"Movie Recognition (VOD Detail View)" in
`docs/architecture/m3u-playlist-module.md`.
- Enrichment is **opt-in** via `Settings > Metadata (TMDB)` because it sends
movie/series titles to a third-party API. Default: disabled.
movie/series titles to a third-party API. Default: disabled. Users supply
their own TMDB API key; distributed builds ship without a shared key.
- The detail view renders provider data **immediately**; enrichment runs
asynchronously and patches the selected item once TMDB responds. A
staleness guard drops responses that arrive after the user navigated away.
@@ -44,7 +45,7 @@ store imports):
| File | Responsibility |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `tmdb-config.ts` | API/image base URLs, embedded default API key, cache TTLs, app-language → TMDB-language mapping |
| `tmdb-config.ts` | API/image base URLs, empty default API key placeholder, cache TTLs, app-language → TMDB-language mapping |
| `tmdb.types.ts` | TMDB v3 response shapes (search, details with credits) |
| `tmdb-api.service.ts` | Thin `fetch`-based client (TMDB supports CORS; works in Electron renderer and PWA). Accepts v3 keys (`api_key` param) and v4 tokens (Bearer) |
| `tmdb-matcher.ts` | Title normalization, year extraction, and the match-confidence gate (pure functions) |
@@ -140,7 +141,27 @@ Wrong metadata is worse than no metadata, so id resolution is conservative:
provider year — portals report the current season's year while TMDB's
`first_air_date` is the premiere. Without a year, the exact-title match
must be unambiguous (single hit).
4. No confident match → the provider data stays untouched, and the negative
4. Several admitted results are ranked by **year evidence first**
(`yearEvidenceTier`): the provider's exact year beats a year off by one,
which beats the series "premiered earlier" tolerance. Popularity
(`vote_count`, then `popularity`) only breaks ties inside the strongest
tier any candidate reached.
The tolerance is a last resort, not an equal. Ranked alongside the
exact-year tier it handed every new series its older, better-known
namesake: because TMDB returns titles in the request language, an
unrelated 2018 foreign series came back under the same `ru-RU` name as a
2026 local-language series and outvoted it, rendering its poster, cast
and genres. Over 400 Cyrillic series titles sampled from a real catalog,
20 normalized keys had a same-titled older series and 16 of those were
the more popular row.
The mirror case is accepted knowingly: a long-running show whose stated
season year happens to BE another same-titled show's premiere year now
resolves to the newer show. Separating those two needs the older show's
season air dates, which a search response does not carry — and the shape
needs three coincidences at once, against one that needs none.
5. No confident match → the provider data stays untouched, and the negative
verdict is cached (shorter TTL) so browsing back doesn't re-search.
The year filter is applied client-side rather than via TMDB's strict
@@ -247,6 +268,25 @@ routinely fill it with a bare cover-image URL —
`sanitizeProviderOverview` (`@iptvnator/shared/interfaces`) treats a
URL-only value as absent, and the stored TMDB overview fills the gap.
The same payload's `poster_path` — every TMDB season has its own poster,
distinct from the show poster on most shows — is stored as a full `w342`
URL (`tmdbSeasonPosterUrl`) in `tmdb_season_posters[seasonKey]` beside the
overview, under the same write-only-if-changed convergence guard. Stalker
keeps it in `StalkerSeriesTmdbSeasonsService.posters(tmdbId)` next to
`descriptions(tmdbId)`, dropped together with the entry when a replacement
fetch fails. Season posters resolve **TMDB-first**, like the show artwork
merge (`prefer(tmdbPoster, provider)`): `buildSeasonPosters`
(`libs/portal/xtream/feature/src/lib/serial-details/season-posters.util.ts`)
takes the stored TMDB poster and falls back to the provider's
`seasons[].cover_big`/`cover` from `get_series_info`, accepted only as a
trimmed http(s) URL that differs from the show poster because panels repeat
it on every season. Stalker has no provider season art, so it is TMDB-only.
The detail views render the selected season's poster as the season cover
beside the season tabs and in the fullscreen episode panel's season strip;
both are withheld for one-season items (that poster is the show poster) and
fold on a failed image request. Contract: "Two-State Detail Layout" in
`embedded-inline-playback.md`.
The season number `{n}` is the provider's episode season number, with one
correction (`resolveEnrichmentSeasonNumber` in
`libs/shared/interfaces/src/lib/season-marker.util.ts`): providers often
@@ -455,7 +495,7 @@ tmdb_metadata (
media_type 'movie' | 'tv' | 'person',
lookup_key 'id:<tmdbId>|v2' -- details payload row
'id:<tmdbId>|season:<n>' -- season payload row
'title:<normalized>|year:<y>|v2' -- search resolution row
'title:<query lowercased>|year:<y>|v4' -- search resolution row
'person:<personId>' -- person payload row
'trending:week' -- trending list row
'badProviderId:<tmdbId>' -- id confirmed 404 by TMDB
@@ -470,14 +510,51 @@ tmdb_metadata (
TTLs (enforced at read time in `TmdbCacheService.isFresh`): details and
positive matches 30 days, negative matches 7 days.
Search and details keys carry a `|v2` version suffix (`buildDetailsLookupKey`
in `tmdb-matcher.ts`): for search rows so normalization changes cannot reuse
stale positive or negative resolutions, for details rows because payloads now
include videos via `append_to_response` and pre-videos cache rows had to be
invalidated. Database startup deletes the obsolete
unversioned search rows once and records
`migration:tmdb-search-lookup-v2-cache-cleanup:v1` in `app_state`; details and
person cache rows are unaffected.
Search keys carry a `|v4` and details keys a `|v2` version suffix
(`buildSearchLookupKey` / `buildDetailsLookupKey` in `tmdb-matcher.ts`): for
search rows so normalization or query changes cannot reuse stale positive or
negative resolutions, for details rows because payloads now include videos
via `append_to_response` and pre-videos cache rows had to be invalidated.
Search v2 → v3 retired the rows written while the folded comparison key was
also the wire query (see "Search query vs. comparison key" below); v3 → v4
retired the rows resolved before year evidence was tiered, where a positive
row naming the wrong show would otherwise stay fresh for 30 days. Database
startup deletes the rows of every retired search-key generation once, each
under its own `app_state` marker so a skipped release still runs the cleanups
it missed (`migration:tmdb-search-lookup-v2-cache-cleanup:v1` for the
unversioned rows, `…-v3-…` for the `|v2` rows, `…-v4-…` for the `|v3` rows;
`LEGACY_TMDB_SEARCH_CACHE_CLEANUPS` in `connection.ts`); details and person
cache rows are unaffected.
### Search query vs. comparison key
`buildSearchTitleVariants` yields `{ query, normalized }` pairs. `normalized`
is `normalizeTitle` — the folded key used for `pickConfidentMatch`, where
both sides fold the same way. `query` is `cleanTitleForSearch`
(`libs/shared/interfaces`): the same tag, bracket, season and trailing-year
stripping, but the letters left as the provider wrote them. The search's
identity is the query with only its case removed (`searchQueryIdentity`):
variants are deduplicated by it and every attempted variant is cached under
its own key (`title:<identity>|year:<y>|v4`, in the language that variant
was searched in), never by the folded key and never only under the first
variant — "Леика" and "Лейка", an illustrative pair, fold to one key while
staying two different searches that can get different answers, so a verdict
for one must not be read back for the other;
a misspelled original title must not swallow the display title that TMDB
actually knows; and two items that share an original title but not a display
title walk different variant lists, so a row keyed on the first variant alone
would hand the second item the first one's answer, or its cached miss. The two must differ because folding is lossy outside Latin: NFD
splits Cyrillic "й" into "и" + a combining breve and "ё" into "е" + a
diaeresis, and Arabic hamza forms ("أ") into a bare alef + a combining hamza
that the punctuation step then turns into a space inside the word. The key
drops or splits on those marks, and TMDB's `/search` does not fold them the
same way, so a folded query matches nothing there. Under the old single-form
design that hit every Russian title carrying "й" or "ё" and every Arabic
title carrying a hamza form: each was searched folded, missed, and cached as
missing for the 7-day negative TTL. The Cyrillic and Arabic strings used
throughout this section are illustrative stand-ins chosen to fold the same
way, not the titles the failures were observed on. Compare results only
through `normalized`; never send it over the wire.
Electron IPC path (follows the standard DB worker contract, see
[SQLite DB Worker](./sqlite-db-worker.md)):
@@ -505,8 +582,7 @@ The panel's full path — settings button, preload bridge, DB worker, real
SQLite — is covered by `@settings @electron @persistence sizes and clears
the TMDB metadata cache` in `apps/electron-backend-e2e/src/settings.e2e.ts`.
It seeds a row through `dbSetTmdbMetadata` rather than through enrichment,
which needs an API key that builds outside the release pipeline do not
carry.
so the test does not depend on a TMDB API key or external API access.
The PWA uses a session-scoped in-memory map (acceptable for phase 1; TMDB
supports CORS so the PWA calls the API directly).
@@ -515,7 +591,7 @@ supports CORS so the PWA calls the API directly).
`Settings.tmdb?: { enabled: boolean; apiKey?: string }`
(`libs/shared/interfaces/src/lib/tmdb.interface.ts`). The settings page has
a "Metadata (TMDB)" section: enable toggle, optional API key override with a
a "Metadata (TMDB)" section: enable toggle, user-provided API key with a
"check key" button (validates against `/configuration`), the M3U
movie-recognition toggle (root-level `Settings.m3uVodDetails`, shown only
while TMDB is enabled and bound via `[formControl]` because it is not part of
@@ -525,19 +601,30 @@ lot. Sizing is a full table scan, so it runs only once that section is the
active one, and a failed read or clear says so instead of showing an empty
cache.
The embedded default key lives in `DEFAULT_TMDB_API_KEY`
(`libs/services/src/lib/tmdb/tmdb-config.ts`) and is an **empty placeholder
in the repository by design**: the real key is stored in the `TMDB_API_KEY`
GitHub Actions secret and injected at CI build time by
`tools/tmdb/inject-tmdb-key.mjs` (step "Inject TMDB API key" in
`build-and-make.yaml`, before the frontend build). Rationale: TMDB keys are
free and extractable from any client binary regardless, but keeping the key
out of the public repo prevents trivial scraping and fork propagation. Never
commit a real key; never reuse keys found in other repositories.
Distributed builds ship **without a shared TMDB API key**. To use
metadata enrichment, users enable it and enter their own key under
`Settings > Metadata (TMDB)`. Key registration is available through the
[TMDB account settings](https://www.themoviedb.org/settings/api); see the
[TMDB API FAQ](https://developer.themoviedb.org/docs/faq).
With no key available (empty default and no user override in settings),
enrichment stays inactive even when the toggle is on — fork PRs and local
dev builds fall into this mode automatically.
This is a project distribution policy, not a claim that TMDB's terms
categorically prohibit embedding an application key. A key shipped in a
client can be extracted and misused; revoking a shared key could interrupt
metadata access for everyone using it. TMDB also applies
[service rate limits](https://developer.themoviedb.org/docs/rate-limiting),
so a personal key does not remove the need to respect throttling.
`DEFAULT_TMDB_API_KEY` in `libs/services/src/lib/tmdb/tmdb-config.ts` is
empty by default. The build tooling still supports optional injection:
`tools/tmdb/inject-tmdb-key.mjs` reads `TMDB_API_KEY` during the "Inject
TMDB API key" step in `.github/workflows/build-and-make.yaml`. With no
non-empty secret, that step is a no-op. Its presence does not mean release
builds include a key; leave the secret unset for the no-shared-key policy.
Never commit a real key or reuse keys found in other repositories.
At runtime, a non-empty user key takes precedence over an injected default.
With neither available, enrichment stays inactive even when the toggle is
on. This applies to release, local development, and fork builds alike.
## Failure Behavior
+1 -1
View File
@@ -118,7 +118,7 @@ Canonical titles in grids, rating/year badges on cards, genre/decade browse, fam
**Blockers, all hard:**
- **The shared release key.** `.github/workflows/build-and-make.yaml:402-406` injects one `TMDB_API_KEY` into every installed copy. Bulk backfill on that key is exactly the traffic shape TMDB throttles ("upper limits to help mitigate needlessly high bulk scraping"). Must be **hard-gated to a user-supplied personal key**, not merely "explicit consent".
- **User-supplied key and throttling.** Distributed builds ship without a shared key; the workflow retains optional injection support (see [Settings and API Key](./tmdb-metadata-enrichment.md#settings-and-api-key)). Bulk backfill must require a user-supplied key even in a build with an injected default. A personal key does not exempt bulk traffic from [TMDB rate limits](https://developer.themoviedb.org/docs/rate-limiting); explicit consent alone is insufficient.
- **No 429/backoff machinery exists** (Theme A5 is a prerequisite).
- **Per-item IPC.** 40k warm writes = 40k+ worker round-trips. Batched cache ops are a prerequisite, not an optimization.
- **6-month retention** must ship _with_ it, not after — bulk warming is precisely a mechanism for maximizing held TMDB data.
+38
View File
@@ -11,6 +11,16 @@ pnpm nx show projects --withTarget lint
pnpm nx show projects --withTarget e2e
```
## Manual CI Runs
When a PR event does not start checks for the current head, dispatch CI and E2E
with `gh workflow run ci.yml --ref <branch>` and
`gh workflow run e2e-tests.yaml --ref <branch>`. CodeQL also supports
`gh workflow run codeql-analysis.yml --ref <branch>`; this analyzes the selected
branch commit instead of the PR merge commit. Verify each run's head SHA before
using its result as evidence. Docker validation can use
`gh workflow run docker.yml --ref <branch> -f push=false`.
## Unit And Type Checks
| Area | Command |
@@ -152,3 +162,31 @@ are gated by:
```bash
IPTVNATOR_TRACE_PLAYER=1 pnpm run serve:backend
```
## Test impact and completion
Before finishing a feature, bug fix, data-flow or UI workflow change, identify
the affected projects and choose unit, integration, E2E, build, lint and manual
checks. Bug fixes normally include regression coverage that fails before the
fix. If automation is impractical, explain why and report the strongest manual
validation. Update fixtures, mocks, routes and E2E flows when behavior changes.
Prefer extending the closest existing suite to introducing a parallel suite.
Run targeted unit checks first, then affected E2E for workflows, routing,
persistence, playback, portals, settings or import flows. Electron IPC, SQLite,
packaged runtime, external players, native files and Electron-only routes require
Electron E2E where available, otherwise CDP/manual verification using the
[debugging guide](../development/electron-debugging.md). Prefer atomized E2E
targets before broad suites. Final reports name tests changed, commands/results,
and skipped validation with reasons. Docs-only changes need Markdown validation,
not app unit/E2E. Tooling validation still requires its own focused tests.
## Agent guidance checks
`pnpm run agents:validate` checks root instruction budgets/imports and guidance
navigation links/anchors. `pnpm run skills:validate` checks skill frontmatter,
length, paths and release mirrors. Both tooling suites run through
`pnpm nx test repository-skills`; syntax checks use
`pnpm nx lint repository-skills`. The CI guidance check runs regardless of the
Nx affected set. Semantic preservation of moved contracts is a review task;
link validation alone cannot prove it.
+101 -15
View File
@@ -113,31 +113,106 @@ Render rules:
2. `continueWatchingCards` — maps `globalRecentVodItems()` to movie/series
cover cards. Portal playback positions are bulk-loaded per playlist so
hero and cards can show progress, remaining time, and series season/
episode badges. This includes Stalker VOD activity normalized to series
through `is_series`. Series lookup uses keyed maps for both direct
episode ids and parent series ids; card renders must not scan the full
playback-position map. The badge uses saved `seasonNumber` /
episode badges. Whether an item is looked up as a movie (one `vod`
row) or a series (episode rows under the parent id) is its WATCH
kind, `resolvePortalActivityWatchKind`, not its routing `type`. The
shape that needs the distinction is a Stalker embedded-VOD row: its
stored entry carries a `series[]` episode array but no `is_series`
flag, so `extractStalkerItemType` reports `movie` (deliberately — the
item belongs in the VOD catalog) while its progress lives in episode
rows. The mappers give both it and a lazy Ministra `is_series` row
(already typed `series`) `watch_kind: 'series'`. Series lookup uses
keyed maps for both direct episode ids and parent series ids; card
renders must not scan the full playback-position map. The badge uses saved `seasonNumber` /
`episodeNumber` metadata and does not infer it from provider payloads;
legacy rows without that metadata remain badge-less until replay.
Dashboard-originated Xtream series clicks also carry that exact episode
target through the global-recent inline-detail handoff. Once the series
metadata and playback positions load, the detail player consumes the
target once and resumes the saved episode. Opening the same item normally
from the global recent grid remains a detail-only action.
Dashboard-originated Xtream and Stalker series clicks also carry that
exact episode target through the global-recent inline-detail handoff.
Once the series metadata and playback positions load, the detail player
consumes the target once and resumes the saved episode. Opening the same
item normally from the global recent grid remains a detail-only action.
Continue Watching cards carry no provider/content-kind subtitle: their
meta row is the S·E chip plus a "N min left" label (`remainingLabel`,
from `formatRemainingLabel`), and the row is not rendered when both are
absent. The hero subtitle is the source name alone, through
`playlistDisplayLabel`.
3. `liveFavoriteCardsEnriched` and `recentLiveCardsEnriched` — two
independent rails (`dashboard-live-favorites-rail` and
`dashboard-recent-live-rail`); there is no fallback from one to the
other. M3U cards carry an `epg_lookup_key` using the app-wide XMLTV
fallback order (`tvg-id` -> `tvg-name` -> channel name); EPG enrichment
must use that key before falling back to the card title.
must use that key before falling back to the card title. Both rails are
enriched by `DashboardLiveEpgPresenter`, the one component-provided
facade for live EPG: it owns the XMLTV lookup described under "Scoped
lookups" in `m3u-playlist-module.md`, forwards everything portal-shaped
to `DashboardPortalLiveEpgPresenter`, and `enrich()` returns the cards
with their "now on air" row filled in.
Xtream and Stalker cards have no XMLTV key of their own; their "now on
air" line comes from the portal, **lazily and per card**:
- `buildDashboardPortalLiveEpgEntry` (dashboard data-access) turns a
live `PortalActivityItem` into the `UnifiedCollectionItem` the
collection pages hand `StreamResolverService.loadEpgForItems`, keyed
by the collection uid — favourites and recent rows of one channel
share the answer. Radio rows and rows without a usable provider id
get no entry. Cards carry that key as `liveEpgSourceKey`.
- `lib-dashboard-rail` reports the cards inside its track viewport
(plus ~one card of `rootMargin`) through `visibleCardsChanged`, from
an `IntersectionObserver` rooted at the track; without the API every
card counts as visible. Cards that leave the list are reported gone
at once.
- `DashboardPortalLiveEpgPresenter` (component-provided) unions the
visible keys of both rails with the pinned hero key and calls
`DashboardPortalLiveEpgService.sync()` with exactly those entries —
on every change, on the 30 s tick, and on a display-offset change.
It is reached through `DashboardLiveEpgPresenter`, which derives the
portal rows itself from the enabled rails and pins the hero, so the
page component only forwards what a rail can see. The queue lives in
the root service, so leaving the dashboard hands the wanted set back
(`sync([])` on destroy); otherwise the queue would keep asking for
cards on a page that is gone.
- `DashboardPortalLiveEpgService` (root) owns the queue: at most two
requests in flight, 200 ms between starts (the numbers
`EpgQueueService` proved against real panels), one card per request,
each answer published the moment it lands in `programs`, so the page
never waits and a slow portal delays no other card. Only wanted keys
are dequeued, so a card scrolled past before its turn is never
requested. A programme lives 60 s; a programme that ended is asked
again, but not within 30 s of the last answer (a portal may keep
returning the stale row). An answer with **no** programme lives only
30 s, because the resolver reports a failed portal and a guide-less
channel identically (it files per-channel failures as `null`), so
there is no failure cooldown to keep and the short TTL is what lets
an outage recover on the next tick.
- Every answer is "at the provider clock" and against one XMLTV source
set. A request captures both the display offset and
`EpgSourceSettingsService.revision()` — the same fence
`EpgService.guard()` uses — and a completion whose either fact moved
is discarded and requeued instead of published. That requeue has to
happen in the completion: while the key is in flight the retire pass
cannot queue a replacement, and without it the pre-change answer
would be trusted for a full TTL (the repo's late-result
invalidation contract).
- Desktop only in practice: the shared collection resolver is gated on
the local XMLTV bridge (`supportsProgramLookup`) and answers nothing
without it, so `sync()` returns immediately in the PWA rather than
filing an empty answer for every card. Lifting that gate for portal
lookups would change the collection pages too and is deliberately
out of scope here.
- `DashboardLiveEpgPresenter.enrich()` prefers the portal answer, falls
back to the XMLTV title match when the portal said "nothing on air",
and marks a card
`nowPlayingState: 'pending'` only before its FIRST answer — the
channel layout then shows a shimmer placeholder in the programme
slot; a refresh keeps the previous answer on screen.
4. `xtreamRecentlyAddedCards` — maps `xtreamRecentlyAddedItems()` to rail
cards. Aggregates newly added VOD and series across *all* Xtream
playlists via `DashboardDataService.reloadXtreamRecentlyAddedItems()`,
which calls `getGlobalRecentlyAdded('all', limit, 'xtream')` with the
DB-level `playlists.type = 'xtream'` filter. The rail is Electron-only
(PWA returns `[]`) and auto-hides when empty, so users without Xtream
playlists never see it. Cards carry a `playlist_name · type` subtitle
so users can tell which provider each item came from. Driven by an
playlists never see it. Cards carry the source name as their subtitle
(`playlistDisplayLabel`) so users can tell where each item was added;
the content kind is not repeated on every card. Driven by an
effect that re-runs whenever the Xtream playlist count changes, but the
first run waits for `globalFavoritesLoaded()` so the slower
recently-added DB query does not block the live favorites rail on
@@ -217,9 +292,10 @@ The welcome state is rendered via the existing
rails have data.
5. Navigation from a rail card must deep-link into the appropriate workspace
route without switching the active playlist in the header switcher.
6. Xtream series hero/Continue Watching clicks with a saved episode position
must resume that exact episode while preserving the collection-owned detail
and Back behavior. Do not apply autoplay to ordinary collection-grid clicks.
6. Xtream and Stalker series hero/Continue Watching clicks with a saved episode
position must resume that exact episode while preserving the
collection-owned detail and Back behavior. Do not apply autoplay to
ordinary collection-grid clicks.
7. `Recently Used Sources` reflects recent source usage across all provider
types, not just recent imports.
8. The live rail title key must match the rendered source: favorites use
@@ -251,3 +327,13 @@ Intentionally out of scope:
2. Freeform widget grid with collision management.
3. External data rails such as RSS, sports, or news adapters.
4. Per-user A/B variants of rail ordering.
## Source subscription expiry
Source cards show a passive subscription-expiry chip: amber within seven days,
error-toned once expired. Account details stay behind the Account info menu.
`DashboardSourceExpiryService` in `libs/workspace/dashboard/data-access` reads
Xtream expiry from cached `PortalStatusService.checkPortalStatusDetails()`
(`exp_date`). Stalker uses the persisted `stalkerAccountInfo` snapshot from the
playlist payload, not the metadata row; each source therefore needs one memoized
full-playlist read. The chip is not a separate account-refresh request.
+189 -26
View File
@@ -304,27 +304,28 @@ handlers in `apps/electron-backend/src/app/events/window.events.ts`):
snap, F11). The controls hide themselves while the window is
fullscreen.
Window state is **never re-read at event time**. `attachWindowStateEvents`
seeds `{ isMaximized, isFullScreen }` once at window creation and each
event patches only the flag it names; every push carries a copy of that
tracked state. On Windows both getters can still report the
pre-transition value while the matching event fires — `isFullScreen()`
stays `true` during an HTML fullscreen exit, and `isMaximized()` reads
`false` while the window is fullscreen. Because the renderer replaces
both flags on every push and no later event corrects a stale one,
polling left the controls hidden forever after leaving fullscreen and
stuck the maximize/restore glyph on the wrong icon. Regression coverage:
`app-window-state.spec.ts` and `window-controls.e2e.ts`.
Window state is **never re-read at event time**. `attachWindowStateEvents`
seeds `{ isMaximized, isFullScreen }` once at window creation and each
event patches only the flag it names; every push carries a copy of that
tracked state. On Windows both getters can still report the
pre-transition value while the matching event fires — `isFullScreen()`
stays `true` during an HTML fullscreen exit, and `isMaximized()` reads
`false` while the window is fullscreen. Because the renderer replaces
both flags on every push and no later event corrects a stale one,
polling left the controls hidden forever after leaving fullscreen and
stuck the maximize/restore glyph on the wrong icon. Regression coverage:
`app-window-state.spec.ts` and `window-controls.e2e.ts`.
The pushed `isFullScreen` is the OR of two flags tracked apart: native
(OS-level, fed by `enter/leave-full-screen`) and HTML-element (fed by
the `*-html-*` pair). Electron remembers when the window was already
natively fullscreen before the player entered HTML fullscreen and then
leaves ONLY the HTML state on exit — no `leave-full-screen` fires and the
window stays fullscreen — so a single flag cleared by
`leave-html-full-screen` would un-hide the controls over a window that
is still fullscreen. A fullscreen launch (below) or F11 followed by the
player's `F` → `Esc` makes that path routine on Windows/Linux.
The pushed `isFullScreen` is the OR of two flags tracked apart: native
(OS-level, fed by `enter/leave-full-screen`) and HTML-element (fed by
the `*-html-*` pair). Electron remembers when the window was already
natively fullscreen before the player entered HTML fullscreen and then
leaves ONLY the HTML state on exit — no `leave-full-screen` fires and the
window stays fullscreen — so a single flag cleared by
`leave-html-full-screen` would un-hide the controls over a window that
is still fullscreen. A fullscreen launch (below) or F11 followed by the
player's `F` → `Esc` makes that path routine on Windows/Linux.
3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`)
and, like the maximize toggle, reports the requested state and leaves
the `WINDOW:STATE_CHANGED` push authoritative. Because the transition is
@@ -406,6 +407,168 @@ Startup window mode (`Settings.startupWindowMode`, issue #1455):
`window.events.spec.ts`, and the startup-window-mode cases in
`settings.e2e.ts`.
Zoom level (Cmd/Ctrl and +/−/0, issue #1109):
1. The packaged renderer runs under `file://` with path routing. Chromium
keys per-host zoom by the FULL URL when a URL has no host, so every
`pushState` to another section owns a separate zoom entry: after a route
change `webContents.getZoomLevel()` already reports that entry (usually
0) and the next visual-properties sync — a window resize, a display
change — snaps the renderer back to it. Chromium persists those per-URL
entries in `Preferences` on its own, which is why the level used to
"appear briefly" on `index.html` at startup and then reset. Dev mode
(`http://localhost:4200`) is per-host and never shows this, so only a
packaged or `ELECTRON_IS_DEV=0` run can verify zoom behaviour.
2. Restore therefore happens in the preload, not the main process:
`applyPersistedZoomLevel` (`api/preload-zoom-level.ts`) asks for the
stored level over the synchronous `WINDOW:GET_ZOOM_LEVEL` IPC at preload
start and applies it with `webFrame.setZoomLevel`, which installs a
TEMPORARY, frame-bound zoom level. It survives in-page navigation and
resizes, the zoom shortcuts (point 4) step it through the same call, and
`getZoomLevel()` reports it regardless of the route.
`webContents.setZoomLevel` from the main process would write the per-URL
entry and re-create the bug. When nothing is stored the preload re-applies
the current level for the same reason: entering temporary mode makes the
first zoom shortcut URL-independent too. A failed request is swallowed
and only costs this load its restore.
The apply is deferred to `DOMContentLoaded` — never at preload start and
never from a `setTimeout`: on Linux and Windows a `webFrame.setZoomLevel`
that early leaves the hidden window without a first frame, `ready-to-show`
never fires, `show()` never runs, and the renderer gets no animation
frames (the splash `main.ts` removes in a `requestAnimationFrame` stays).
macOS is unaffected and CDP-driven tests force frames, so only the
packaged Linux/Windows E2E asserting the splash is gone caught it
(`legacy-playlist-migration.e2e.ts`, defer-epg). After the parser
finishes the call is harmless and still lands before the first Angular
paint. The preload then sends `WINDOW:ZOOM_LEVEL_APPLIED`.
3. Chromium never persists temporary zoom, so `services/window-zoom-level.ts`
owns the electron-conf key `ZOOM_LEVEL`: the applied acknowledgement (not
the request — between the two the sender's `getZoomLevel()` is still the
per-URL default) marks the sender
as owning the level, `persistZoomLevel` writes it back from the window
`close` and app `before-quit` handlers (the bounds-only saves of before,
folded into `persistWindowState`), and `attachZoomLevelPersistence` also
writes it on every main-frame cross-document `did-start-navigation` —
a reload drops the temporary level, and by `did-finish-load` the new
document's preload has already read whatever was stored. That
navigation also releases ownership until the next preload answers, so a
close mid-reload cannot save the per-URL default over the user's level.
4. The shortcuts are a renderer key binding, not a native menu: the
Windows/Linux window calls `setMenu(null)`, so no accelerator could reach
it there. `WorkspaceKeyboardShortcutsService` (`libs/workspace/shell`)
listens on the document like it does for F11 and resolves the chord with
`resolveZoomShortcutAction` (`libs/portal/shared/util`): Cmd on macOS,
Ctrl elsewhere, never Alt; `+`/`=` (so `Ctrl+=` and `Ctrl+Shift+=` both
zoom in), `-`/`_`, `0`, and the numpad `+`/`-`/`0` (by `code`, since a
NumLock-off `0` reports `Insert`). Keys are matched by `event.key`, so
non-US layouts zoom with their own `+`/`-` keys. Like F11 it is not gated
by the typing-target check — browsers zoom from any focus — and a key
another handler already `preventDefault`ed is left alone. The binding
calls the synchronous, preload-local `window.electron.adjustZoomLevel`
(`adjustFrameZoomLevel` in `api/preload-zoom-level.ts`), which steps the
frame's temporary level through the same `webFrame.setZoomLevel` as the
restore and returns the level applied — never a main-process
`webContents.setZoomLevel`, which would re-create the per-URL bug. The
step and limits live in `libs/shared/interfaces/src/lib/zoom-level.util.ts`
(`stepZoomLevel`): 0.5 per press, Electron's own `zoomIn`/`zoomOut` role
step (≈10 %), clamped to levels −4…6 (≈48 %…299 %, inside Chromium's
25–500 %), off-grid levels snapping to the next grid point in the pressed
direction; `Ctrl/Cmd+0` returns to level 0. A stored level already
outside the limits (the macOS menu roles never clamped) is never moved
against the request: a press further out leaves it, a press back in
lands on the limit. Persistence needs nothing
extra: the main process reads the live level back (point 3). On macOS the
default application menu still carries the `zoomIn`/`zoomOut`/`resetZoom`
roles, but Chromium hands a key equivalent to the web contents first and
Electron performs the menu equivalent only in
`WebContents::PlatformHandleKeyboardEvent`
(`shell/browser/api/electron_api_web_contents_mac.mm`, Electron 43.3.0),
the unhandled-keyboard-event hook — a `preventDefault`ed keydown never
gets there, so the binding keeps one press at one step. CDP-dispatched
keys (the E2E) never reach the menu at all.
Without a bridge (PWA) the browser keeps its own zoom, and the help
dialog lists the chords as Electron-only. Regression coverage:
`window-zoom-level.e2e.ts` presses the real shortcuts (in, out, numpad,
reset) and measures the rendered factor (content width ÷
`window.innerWidth`) across a section change, a resize, a reload and a
restart; key resolution and the bridge step are unit-tested in
`keyboard-shortcuts.spec.ts`, `workspace-keyboard-shortcuts.service.spec.ts`
and `preload-zoom-level.spec.ts`.
Reloading the renderer on an in-app route:
1. The packaged renderer is `dist/apps/web/index.html` over `file://` and
Angular routes by path (no hash strategy), so once the user is on a
section the document URL is `file:///…/web/workspace/sources` — a path
with no file behind it. A reload of that URL fails with
`ERR_FILE_NOT_FOUND` (-6) or is cancelled outright, depending on who
starts it. Two user-reachable triggers: the macOS default application
menu (nothing calls `Menu.setApplicationMenu`, so View › Reload / Force
Reload are live; Windows/Linux drop the menu bar via `setMenu(null)`),
and the settings unsaved-changes guard, which calls
`window.location.reload()` after the user confirms a reload intent on
`/workspace/settings/<section>`. Dev mode (`http://localhost:4200`) never
shows either — the dev server serves the index for every path.
2. Both legs live in `services/renderer-reload-fallback.ts` and end in the
same `restoreRendererRoute`: load the packaged index with the routed
URL's route — its path relative to the renderer root plus query and
fragment (`resolveRoutedRendererUrl`) — in the `restoreRoute` query
parameter.
- A main-process reload (`webContents.reload()`, the menu role,
DevTools) fires no `will-navigate`, so it cannot be redirected up
front: it fails, Chromium commits `chrome-error://chromewebdata/`
and `app-root` stays empty until the app restarts.
`attachRendererReloadFallback` recovers it after the fact from the
main-frame `did-fail-load` with `ERR_FILE_NOT_FOUND`
(`resolveReloadedRendererRoute`); other error codes, subframes and
non-`file:` URLs are left alone. The recovery load is deferred to
the error page's `dom-ready` and never issued from inside
`did-fail-load`: a `loadFile` started while Chromium is still
committing the error page yields a document that never receives
animation frames — the splash stays, nothing paints, while
`document.visibilityState` still says `visible` — and the same load
after `dom-ready` paints normally (Electron emits `did-fail-load`
before that `dom-ready`). A cross-document navigation starting in
between withdraws the pending recovery, so a stale `dom-ready` can
never re-load the index over a newer navigation.
- A renderer-initiated reload (`location.reload()`, the settings
guard) does fire `will-navigate`, where the routed URL is not the
trusted index and `handleRendererNavigation` would cancel it —
silently, so the confirmed reload simply never happened. The handler
now recognizes a routed renderer URL and sends it straight to the
index with its route, with no failed load in between; every other
untrusted navigation is still blocked (external URLs still open in
the browser).
A failed `index.html` itself is never re-requested (it would loop):
`resolveRoutedRendererUrl` rejects the index, and the recovery load
carries `index.html` as its path, so a second failure cannot recurse.
3. The renderer consumes the parameter before Angular bootstraps:
`apps/web/src/main.ts` calls `resolveRestoredRendererRoute`
(`libs/shared/interfaces/src/lib/renderer-reload-route.util.ts`, which
also owns the parameter name) and installs the result with
`history.replaceState`, so the router's initial navigation lands on the
route the user was on. The route is resolved against `document.baseURI`
(the packaged `<base href="./">`, i.e. the renderer directory — the same
prefix Angular strips from `location.pathname`), and anything that would
leave that directory (an absolute URL, another scheme, a `..` escape)
is dropped with only the parameter removed, so the app boots at its
default route instead of following an arbitrary target.
4. Zoom persistence is unaffected: the failed reload's
`did-start-navigation` already saved the level and released ownership,
the recovery load's `did-start-navigation` is then a no-op, and the new
document's preload restores the level as after any other reload. The
main-process close guard also treats the recovery like any full
navigation (`did-navigate` disarms it).
5. Regression coverage: `renderer-reload.e2e.ts` reloads from the main
process (`webContents.reload()`, the menu role) on Sources and from the
renderer (`window.location.reload()`, the settings guard) on a settings
section and asserts a NEW document is rendered on the same route with
the parameter gone (`renderer-reload.support.ts` marks the old document,
since the URL alone is identical before and after);
`window-zoom-level.e2e.ts` reloads the same way. Unit coverage:
`renderer-reload-fallback.spec.ts`, `renderer-reload-route.util.spec.ts`,
`app.spec.ts` ("renderer reload recovery").
Layout integration:
1. `document.body` gets a `frameless-platform` class (set in
@@ -439,15 +602,15 @@ Toolchain notes for the Electron 43 baseline:
implicit `node-gyp rebuild` bypasses those binaries and makes installation
depend on the host compiler toolchain. The old `node-abi` override belonged
to v12's removed `prebuild-install` path and is no longer required.
2. Local development supports **Node 22.13–22.x or Node >= 24**, declared in
`engines` as `^22.13.0 || >=24.0.0`.
The direct `@faker-js/faker` dependency and current lint tooling require
that floor. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
2. Local development supports **Node 22.22.3–22.x or 24.15.0–24.x**, declared in
`engines` as `^22.22.3 || ^24.15.0`. Use `.nvmrc` (currently 22.23.2) for
development and CI. Angular 22 sets this supported LTS range and requires
TypeScript `>=6.0 <6.1`. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
which requires Node 22.12 or newer, and the root `postinstall` runs
`install-app-deps` on every `pnpm install`. The 26.15.7 minimum also
carries the v26 backport that fully extracts the Snap template's `.tar.7z`
payload; 26.15.0–26.15.6 can
produce a Snap that is missing `desktop-init.sh`. CI already runs Node 22.
produce a Snap that is missing `desktop-init.sh`.
3. Dependabot keeps Electron, native database, packaging, EPG parser, and
version-locked Shaka/mpegts updates out of the shared npm minor/patch group.
Those dependencies require standalone PRs so their dedicated package,
@@ -457,7 +620,7 @@ Toolchain notes for the Electron 43 baseline:
Electron command instead of during package `postinstall`, so `electron`
must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local
`pnpm run serve:backend` can include a one-time download; use `pnpm exec
electron --version` to prewarm it before an offline run.
electron --version` to prewarm it before an offline run.
Known caveats:
+3 -1
View File
@@ -122,7 +122,9 @@ unauthenticated content, so it should not be reachable from other hosts by
accident. Set `HOST=0.0.0.0` explicitly to expose it, which is what you need
when driving the mock from a phone, an STB, a container, or another machine.
Control mode additionally *rejects* an explicitly configured non-loopback host. The Nx serve targets do not pin `PORT`, so an explicit shell
value reaches the parser; its no-value default remains `3211`.
value reaches the parser; without one it falls back to `XTREAM_MOCK_PORT` (the
client-side knob Playwright and the specs read, so one variable relocates the
mock and the tests together) and then to `3211`.
Use a dedicated port rather than the normal `3211` E2E server:
@@ -373,3 +373,31 @@ It reuses the canonical timeshift resolver and original timestamps, preserves
playback headers and does not change playback. See
[Download Manager](download-manager.md#xtream-archive-downloads) for identity,
restart, expiry and transport-completion limits.
## Store composition and catalog windowing
`XtreamStore` is the public facade built with `signalStore()`, composing
`signalStoreFeature()` features for portal, content, selection, search, EPG,
player, favorites, recent and playback positions. Most features live under
`libs/portal/xtream/data-access/src/lib/stores/features/`; favorites and recent
items live directly under the data-access library’s `src/lib/`.
Routed components consume that facade; features delegate persistence/networking
to `IXtreamDataSource`, selected through `provideXtreamDataSource()`. Complete
SQLite capability uses database-first cache reads and API fill; the PWA source
uses API requests and session memory. This does not move screen orchestration
into shared utility projects.
Catalog lazy loading: catalog grids scroll infinitely instead of paging.
`withSelection` keeps a `visibleCount` render window over the in-memory
catalog plus bounded per-selection scroll snapshots for detail/tab
round-trips; the shared `InfiniteScrollDirective`
(`libs/portal/shared/ui`) measures container overflow to auto-fill tall
viewports (terminating on lack of container growth, not on a load count)
and fires `loadMore` near the bottom. The search layout routes its results
container through the same directive (`nearEnd*` inputs). Stalker feeds the
same contract from server-paged appends: portal pages accumulate into one
deduplicated list, `hasMoreContent` derives from accumulated length vs
`total_items`, a failed append keeps loaded pages and offers a tail retry,
and the facade maps page 0 to the skeleton and later pages to the tail
spinner. These catalog/search surfaces use incremental loading instead of
page buttons.
+223
View File
@@ -0,0 +1,223 @@
# Agent development workflow
Common startup rules live in [AGENTS.md](../../AGENTS.md). This document holds
procedures and conventions to read when they apply, not extra startup imports.
## Maintaining canonical knowledge
After meaningful changes, assess documentation before declaring completion.
Meaningful changes include user-visible behavior, architecture/data flow,
maintenance/setup/debugging workflows, and subsystem contracts. Formatting,
behavior-preserving refactors and isolated test changes need no doc update.
Prefer the existing authoritative architecture doc or nearest module README;
README.md owns top-level user/developer entry points. Update stale routes,
paths, commands and contracts you encounter, or explicitly flag unresolved
claims in the final summary. Repo docs remain canonical regardless of authorship.
Keep AGENTS.md limited to repository-wide constraints and task routing. Do not
add feature histories, method lists, schema inventories or troubleshooting
procedures to it. CLAUDE.md imports AGENTS.md; do not mirror common prose by hand.
Use [the context map](../maintenance/agent-context-map.md) for the owning document
and relevant skill. Read multiple contracts for cross-domain work. Add a new
canonical document only when no existing owner fits; link it from the map.
When relocating knowledge, compare both sources and the destination, retain
unique exceptions and rationale, and record corrections with code evidence.
The [2026-09 migration ledger](../maintenance/agent-guidance-migration.md) records
the initial move; it is an audit artifact, not required reading for feature work.
Later normal edits maintain the canonical docs, not duplicate historical prose.
Run `pnpm run agents:validate` after guidance changes. It checks line/byte budgets,
root imports and local navigation links/anchors, including migration destinations.
Line budgets count LF, CRLF and standalone CR endings consistently.
Markdown navigation is parsed with the already-declared `marked` dependency;
undefined explicit references (including shortcut images) are errors, and code examples are excluded.
Backticked concrete paths in root guidance and the context map are checked from
the repository root, including unknown top-level directories and filenames.
Write generic filenames as prose; commands, templates, globs, URLs, package
aliases and dotted code symbols are excluded. Bare dotted names with conventional
file suffixes (such as .md, .json or .ts) are treated as filenames. Document formats
share the suffix set used by package-import guards, including PDF and AsciiDoc. Use a `./`
prefix or Markdown link for other ambiguous filenames that resemble code symbols.
Explicit relative literals denote paths, including spaces, filesystem punctuation and hyphenated words.
Put executable command examples in fenced code when their syntax also looks like a path.
Multi-part dotfiles are path candidates too.
Conventional extensionless filenames such as Dockerfile, Makefile and LICENSE
are also path candidates; use an explicit `./` prefix for other extensionless files.
Link paths and fragments are decoded separately so encoded filename delimiters
stay in the filename. Fenced and indented examples
do not count as root guidance imports or satisfy the required Claude import.
The required Claude import must be an unformatted standalone line in a top-level
paragraph; headings, quotes and list items do not satisfy it.
The parsed HTML tree also verifies that this paragraph is outside HTML containers,
including templates split across Markdown tokens. Generated HTML is inspected
in memory only; it is never executed or emitted.
Heading anchors decode HTML character references in text and use `github-slugger`
for GitHub-compatible character filtering and duplicate suffixes.
Only headings present outside inert HTML containers contribute slugs or duplicate counters.
Explicit HTML anchors use `parse5`, excluding comments, scripts, styles and template contents.
Rendered HTML anchor and image-map area hrefs and image sources use the same local-reference checks
as Markdown links, including decoded attributes and fragment validation.
URL attributes remove ASCII tabs/newlines throughout and discard surrounding
ASCII control/space characters before resolution.
Iframe/embed sources and object data attributes are document references and retain Markdown-target anchor checks.
Inline iframe srcdoc documents are traversed too, with their own fragment anchors.
The first active HTML base href sets reference resolution, including nested srcdoc bases.
Resolved file URLs use native filesystem conversion, including Windows drive paths.
Explicit srcset attributes must contain at least one parsed candidate.
Image and media references require a nonempty path that resolves to a file, not a directory.
Direct file URLs, Windows drive paths and HTML bases using either form are rejected; use portable repository-relative paths.
Image references check file existence without interpreting image fragments as
Markdown headings; document links keep anchor checks even when sharing a target.
SVG image/use hrefs (including xlink), HTML image-input, video, audio, source and track `src` assets and video posters use the same
existence checks as images. Entity decoding uses full HTML text/attribute rules,
including references whose semicolon may be omitted.
Inline guidance imports are rejected after punctuation as well as whitespace.
At-signs inside external URIs (including www autolinks and explicit opaque autolinks such as mailto) are excluded
per HTML text node, preserving adjacent imports. A colon directly before an import
does not make that import a URI. Opaque schemes are excluded only in parsed links
whose visible text equals their URI, so colon-labeled prose remains checked.
A closing bracket or matching enclosing quote followed by punctuation and an at-sign terminates a bare URL exclusion.
Extensionless inline candidates are also imports when they resolve to repository files,
checking the full filename before prefixes at ASCII/Unicode prose separators,
including opening parentheses, brackets and braces. Each at-sign candidate is
checked independently, including imports nested next to a package mention.
An at-sign inside a word (for example, foo@INSTRUCTIONS or an email address)
is not an import boundary, including within parenthetical prose.
Declared scoped dependencies, scope wildcards and matching TypeScript path aliases
are recognized as package/alias mentions. Traversal and document-file imports are
rejected before those exemptions, including document paths with fragments or queries.
All recognized Markdown extensions share the document-import guard; reStructuredText
and AsciiDoc, PDF, Word, OpenDocument, RTF, Org and TeX documents are also excluded
from package exemptions. Recognized extensionless guidance names (including AGENTS,
CLAUDE, INSTRUCTIONS, README, CONTRIBUTING and SECURITY, case-insensitively) are excluded in package subpaths too. URL-encoded
paths do not receive package exemptions. TypeScript configuration is parsed as JSONC.
Declared packages also permit safe subpaths; exact aliases stay exact.
Federated handles in the @user@host form are prose, not imports; trailing closing
ASCII/Unicode punctuation and possessive apostrophe-s suffixes are ignored. Opening
delimiters separate adjacent prose; nested imports remain checked. Extra at-signs do not qualify for that exemption.
Exact declared packages remain exempt after version normalization.
Declared package mentions may include a version (including semver comparators and wildcard ranges) or dist-tag qualifier.
Qualifier handling includes unscoped names; terminal sentence punctuation is
removed before matching a declared package, as are straight/curly apostrophe possessives.
Unicode punctuation and ASCII opening delimiters, commas, semicolons, colons, question/exclamation marks
separate package mentions from adjacent prose.
Markdown destinations decode HTML entities before URI parsing, matching rendered links.
Heading-anchor lookup is limited to Markdown targets. Source-file line fragments,
PDF page fragments and other non-Markdown fragments retain file-existence checks.
The import scan separates HTML block/table elements and includes visible text and literal backticks; it excludes parsed code nodes and non-rendered containers.
Navigation uses the parsed rendered tree too. Temporary in-memory markers retain
definition, unresolved-reference and literal-path metadata, so Markdown inside
inert templates is excluded consistently with raw HTML navigation.
Image source sets use `parse-srcset` to check each candidate URL. Root-relative
literals never suppress source-relative definition checks.
The Nx test hash includes `marked`, `parse5`, `github-slugger`, `parse-srcset` and `typescript`
so dependency changes invalidate parser coverage.
It cannot prove semantic equivalence; review changed contracts as well.
## Protected Markdown edits
Never run whole-file `prettier --write` on AGENTS.md, CLAUDE.md or docs/**.
Upstream formatting is not uniformly Prettier-clean: whole-file writes can
corrupt nested list indentation or change a literal continuation into a bullet.
Format only intended new lines. If accidental formatting occurred, reconstruct
from the pre-edit version and reapply only intended changes; preserve unrelated
user edits. Use the merge-base version only if it actually represents that
pre-edit state. Review the diff rather than blindly restoring an older branch.
## Plans and completion reports
Save only finalized plans in `.plans/YYYY-MM-DD-short-topic.md`; use numeric
suffixes for collisions. Do not save drafts or questions there. If an active
mode forbids writes, save the approved plan on entering execution.
Completion reports list changed docs, tests added/updated, commands and results,
skipped validation with reasons, and release-note status. A docs-only task needs
Markdown/link validation, not app unit/E2E tests. Tooling changes need their own tests.
## Repository skills
Repository skills live under `.codex/skills/`. Descriptions are trigger-only,
begin with `Use when`, and each skill is at most 500 words. Frontmatter owns
trigger descriptions; avoid copying them into navigation prose. Skills provide
workflow and links to authoritative contracts, not a second contract copy.
`release-cut` and `release-notes` have byte-identical `.claude/skills/` mirrors.
Run `pnpm run skills:validate` after changing a committed skill or a literal
path it documents. New guidance tooling belongs to the existing
`repository-skills` Nx project; no new project is needed for another validator.
## Angular conventions
Use signal-based queries (`viewChild`, `viewChildren`, `contentChild`,
`contentChildren`) and inputs/outputs (`input`, `output`). For required queries,
use `viewChild.required`. Unwrap signals when passing values in templates:
```typescript
readonly menu = viewChild.required<MatMenu>('menuRef');
readonly title = input.required<string>();
readonly size = input<number>(10);
readonly clicked = output<void>();
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
```
```html
<button [matMenuTriggerFor]="menu()">Open Menu</button>
```
Use `signal`, `computed`, `effect` and `linkedSignal` for reactive state. Existing
host bindings/listeners use `@HostBinding` and `@HostListener`; this relocation
does not change that convention. Prefer `@if`, `@for` (with a stable `track`),
and `@switch` over the legacy structural directives. A signal is a function;
passing `menu` instead of `menu()` to Material supplies the wrong value.
## Adding behavior across layers
For IPC, define the handler in the appropriate Electron events module,
register it in the event bootstrap, expose a typed preload method, and consume
it through the renderer service. Keep channel contracts typed in
`ElectronBridgeApi`; use the database worker contract for heavy database work.
See [Electron security](../architecture/electron-security.md) and
[DB worker ownership](../architecture/sqlite-db-worker.md).
For a playlist source, extend the shared playlist type, add the backend event
handler, add the import UI under `libs/playlist/import/feature`, and update
state actions/effects. Preserve runtime capabilities and migration behavior.
NgRx owns M3U global state; portal/feature state uses composed NgRx Signal Store;
component-local state uses Angular signals. Avoid expanding large classes:
extract components/services or `with*` store features before exceeding limits;
shared types belong in their own contract modules. The hard production limit
is 400 (not a variable 350–400); target under 300. See
[Nx file-size policy](../architecture/nx-workspace-boundaries.md#typescript-file-size).
## Build and serve commands
Use local `pnpm nx` for underlying project targets. Package scripts are the
supported entry points for composed tasks:
| Task | Command |
| --- | --- |
| Web development | `pnpm run serve:frontend` |
| PWA development | `pnpm nx serve web --configuration=pwa` |
| Electron development | `pnpm run serve:backend` |
| Electron frontend | `pnpm run build:frontend` |
| PWA frontend | `pnpm run build:frontend:pwa` |
| Electron backend | `pnpm run build:backend` |
| Package without installers | `pnpm run package:app` |
| Create installers | `pnpm run make:app` |
Electron backend build depends on the web build; outputs live under
`dist/apps/electron-backend` and `dist/apps/web` and packaging combines them.
Use [the validation map](../architecture/validation-map.md) for test/lint tasks
and [the release pipeline](../architecture/release-pipeline.md) for packaging.
## Shared search text folding
Use `foldSearchText` from `libs/shared/interfaces/src/lib/search-text-fold.util.ts`
on both sides of every in-memory search comparison. Channel lists, catalog and
category filters, command palette, sources and downloads must share this fold;
row filtering and count/queue sources must not diverge. It lowercases without a
locale, normalizes to NFC, then strips remaining combining marks. Composing first
keeps canonical spellings equivalent while preserving accents; the leftover dot
in Turkish dotted İ is stripped so it matches plain i. Do not substitute
`toLocaleLowerCase`. Electron content search uses the same fold and adds explicit
Turkish-locale İ variants to LIKE/GLOB queries because SQLite LIKE folds only ASCII.
+103
View File
@@ -0,0 +1,103 @@
# Electron debugging and tracing
Use this procedure for Electron/CDP tasks. Read the available `electron` skill
when automating the desktop app. These commands assume a bootstrapped worktree.
## Start and attach
- Start the Electron development app with: `pnpm nx serve electron-backend`
- Package-script equivalent: `pnpm run serve:backend`
- Electron is configured to start with: `--remote-debugging-port=9222`
- Connect Chrome DevTools Protocol tools to: `127.0.0.1:9222`
- For Electron automation/debugging tasks, use the `electron` skill
- Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via `ELECTRON_OPEN_DEVTOOLS=1`.
- If DevTools is open, `agent-browser --cdp 9222 ...` may attach to the DevTools page instead of the IPTVnator window. Symptoms: `tab list` shows `about:blank`, snapshots are empty, and screenshots are black.
- If that happens, inspect targets with `curl http://127.0.0.1:9222/json/list` and connect directly to the IPTVnator page websocket from the `webSocketDebuggerUrl` field.
- The app holds a single-instance lock (`acquireSingleInstanceLock` in `apps/electron-backend/src/app/services/single-instance.ts`): a second launch against the same `userData` quits immediately and focuses the running window. To attach a second CDP-enabled instance to the same profile, set `IPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1` — knowing that only one of the two processes will own the renderer's IndexedDB, so settings written by the other are lost. Before focusing, the guard forwards the second launch's argv to `onSecondInstance`, which is how a playlist path handed to an already-running app reaches the open queue.
### Trace / Debug Startup
- Full startup tracing:
```bash
IPTVNATOR_TRACE_STARTUP=1 pnpm nx serve electron-backend
```
- Narrower trace flags:
- `IPTVNATOR_TRACE_IPC=1` traces renderer `window.electron.*` bridge calls
- `IPTVNATOR_TRACE_DB=1` traces DB worker requests and request-scoped DB events
- `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in the main process and DB worker
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
- Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
`console.*`; never log raw credentials while debugging.
- If local Nx state gets weird before a rerun:
```bash
pnpm nx reset
```
### agent-browser (global install)
```bash
agent-browser --cdp 9222 tab list
agent-browser --cdp 9222 tab 1
agent-browser --cdp 9222 snapshot -i -c -d 4
agent-browser --cdp 9222 screenshot /tmp/iptvnator-cdp.png
```
### Fallback
```bash
npx --yes agent-browser --cdp 9222 tab list
```
### DevTools Workaround
```bash
ELECTRON_OPEN_DEVTOOLS=1 pnpm nx serve electron-backend
curl http://127.0.0.1:9222/json/list
agent-browser connect ws://127.0.0.1:9222/devtools/page/<iptvnator-page-id>
agent-browser screenshot /tmp/iptvnator-cdp.png
```
## E2E process cleanup and packaged diagnostics
Playwright launches Electron through `cmd.exe` on Windows. If graceful E2E
shutdown times out, terminate that process tree with `taskkill /T /F`; killing
only `electronApp.process()` can leave Electron holding the temporary profile
and make the next launch exit on the single-instance lock. The E2E workflow
checks this against a real Windows shell and child process. Test cleanup must
target only its own launch PID, never all Electron or IPTVnator processes.
Cleanup confirms process exit even if Playwright's close promise fails or
never settles. If termination fails, it retries and then throws instead of
allowing a relaunch against a potentially locked profile.
Capture the Node child-process handle immediately after launch and retain it
in a WeakMap keyed by the Electron application for cleanup and preparation
failures. This binding follows the application when restart callers replace
only the application/window fields on their fixture. After the last window
closes on Linux,
Electron can exit before cleanup begins; Playwright disposes its dispatcher,
so calling `electronApp.process()` at that point can throw even after a clean
exit. The retained handle still provides the actual exit code and signal.
The Linux portable build uploads `packaged-frame-copy-smoke` reports and traces
even when the smoke fails. Check the paused-frame screenshot and trace before
classifying a zero rendered-frame signal as an infrastructure flake.
## Main-process ownership
The entry point is `apps/electron-backend/src/main.ts`; it bootstraps the database,
registers events and creates the main window. The preload is
`apps/electron-backend/src/app/api/main.preload.ts`, with handlers under
`apps/electron-backend/src/app/events/`. The window follows the saved startup mode
(normal/maximized/fullscreen); `--fullscreen` overrides a single launch. Use
[workspace shell](../architecture/workspace-shell.md) for window behavior,
[DB worker](../architecture/sqlite-db-worker.md) for worker ownership and
[Electron security](../architecture/electron-security.md) for bridge boundaries.
+95
View File
@@ -0,0 +1,95 @@
# Release cover artwork
Use this recipe for IPTVnator announcement covers in the sculptural graphite
style selected for 0.24. It is a reusable visual direction; adapt it when the
user requests another style. Release ordering and publication remain governed
by the [release pipeline](../architecture/release-pipeline.md).
## References and inputs
- [Approved 0.24 cover](../../apps/website/public/blog/v0-24/announce.png):
composition, material, lighting and typography reference.
- [Application icon](../../apps/website/src/assets/logo.png): identity reference;
retain the TV outline, short stand, horizontal foot and Wi-Fi symbol.
- [Exact successful 0.24 edit prompt](release-cover-prompt-0.24.txt): archival
wording. Its Image 1 was an intermediate abstract-ribbon cover. For future
releases, use the approved final cover above with the reusable prompt below;
the intermediate image is not required.
Choose a display version and two or three short, verified release highlights.
For 0.24 the labels were “Programme guide”, “Fullscreen browsing” and “Stream
info”. The display version was **0.24**, while the package and tag remained
**0.24.0** and **v0.24.0**. Artwork text never changes the technical version.
## Generate and review
Use the imagegen skill and built-in image generation. Inspect both reference
images first and give each its stated role. Do not silently switch to an API
CLI or substitute a code-generated graphic. Produce one horizontal image.
Fill every placeholder in the prompt; omit the third caption when there are
only two highlights. Preserve the style reference's typography as closely as
possible: the named fonts describe visual character, not an exact font-file
guarantee from the image model.
```text
Use case: identity-preserve
Asset type: one horizontal 2:1 IPTVnator release cover, finished raster artwork.
Image 1: approved previous release cover; style and composition reference.
Image 2: authentic IPTVnator app icon; identity reference.
Create the next release cover in the visual language of Image 1. Use warm
off-white uncoated paper, graphite ink, brushed graphite metal and one restrained
blue accent matching the small application logo. Use subtle directional daylight,
delicate contact shadows, crisp geometry and generous negative space.
The main artwork is an unmistakable sculptural version of the TV-and-Wi-Fi icon.
Keep its broad rectangular TV outline with softly rounded corners, visible short
central stem and horizontal foot. Inside are three clearly separated nested
Wi-Fi arcs above a small diamond-like dot. Build the frame from thin layered
graphite ribbons. Extend the side edges into staggered parallel strips on the
left and a restrained flowing wave on the right. These extensions belong to one
continuous object; the TV, stand and Wi-Fi symbol must remain immediately legible.
Use slight relief depth and a mostly frontal editorial perspective.
Keep the small authentic blue icon and IPTVnator wordmark at upper left. Place
the display version at lower right, in a distinctive heavy compressed industrial
sans with squared mechanical numerals, like Druk or Monument Grotesk. Set the
feature captions as a compact left-aligned stack at lower left, in a precise
JetBrains Mono-like monospaced face. Use a refined Cabinet Grotesk-like wordmark.
All lettering is solid graphite. Keep the central sculpture clear of every label.
Render only these exact strings:
"IPTVnator"
"{{DISPLAY_VERSION}}"
"{{HIGHLIGHT_1}}"
"{{HIGHLIGHT_2}}"
"{{HIGHLIGHT_3}}"
No other words, numbers or pseudo-text. Preserve exact capitalization and spelling.
No generic thin geometric sans, serif, gradient lettering, neon edges, purple/blue
glow, glass cards, floating balls, dashboard, screenshots, literal video content,
landscapes, old CRT knobs or antenna rods. This is conceptual brand artwork,
not an application screenshot. Keep the paper-and-graphite composition breathable,
asymmetric and readable at social-preview size.
```
Review the result for exact text, recognizable icon silhouette, separated Wi-Fi
arcs, visible stand, clear captions, safe margins and the intended type contrast.
Inspect the actual image; prompt compliance alone is not visual verification.
Save candidates non-destructively in the release's asset directory, and retain
the exact filled prompt, input roles and generation mode alongside preparation
materials. Once a cover is selected, copy it to the canonical announcement asset
and update the blog hero reference if the blog uses it. Preserve earlier variants.
The usual path shapes are:
```text
apps/website/public/blog/<vX-Y>/announce-v<N>.png
apps/website/public/blog/<vX-Y>/announce.png
```
An imagegen cover complements the deterministic highlight cards and guarded
screenshots; it does not replace their release-pipeline steps. Cover generation
or selection does not authorize merging, tagging, publication or posting an
announcement.
@@ -0,0 +1,16 @@
Use case: identity-preserve
Asset type: horizontal 2:1 IPTVnator 0.24 release poster, finished raster artwork.
Edit target: Image 1, the off-white abstract graphite ribbon poster. Preserve its beautiful warm uncoated-paper background, monochrome graphite material, restrained blue logo accent, asymmetric editorial layout, lighting, precise typography and all five exact text strings.
Supporting identity reference: Image 2 is the actual IPTVnator app icon, a TV outline on a short stem and horizontal foot with a Wi-Fi symbol inside.
Primary change: Replace the ambiguous central rectangular ribbon aperture with an unmistakable SCULPTURAL VERSION OF THE IPTVnator TV-and-Wi-Fi ICON. Make the logo the central hero artwork in the same sophisticated folded graphite / brushed-metal lamella style.
Sculpture: a broad television outline, recognizably a wide rectangular TV frame with softly rounded corners, a visible short central stem and horizontal base/foot. Inside the TV is the exact familiar Wi-Fi motif, three clean nested curved arcs above a small diamond-like dot, built from the same refined graphite material. The arcs must remain clearly separated and immediately legible; do not fill the interior with random lines. The TV itself is formed from several thin layered metal ribbons with precise spacing and delicate shadows. Stretch its layered side edges horizontally into elegant parallel ribbons: longer staggered strips entering from the left, and a restrained flowing wave continuing to the right. The extension belongs naturally to the TV contour; keep the central TV shape, Wi-Fi symbol and stand intact and readable. The entire object is one sculptural physical artwork resting close to the paper plane, with subtle relief depth and soft directional daylight. Mostly front-facing, with a small editorial perspective angle. It should read first as the IPTVnator app icon, second as a layered abstract sculpture.
Composition invariants: keep the small original logo and “IPTVnator” wordmark at upper left. Keep the compressed heavy industrial “0.24” at lower right. Keep the three monospaced captions at lower left. Reserve clear whitespace around all lettering. The sculpture occupies the same central band, with TV stand visible above the lower caption/version region. Do not obscure the lettering.
Text must remain EXACTLY:
“IPTVnator”
“0.24”
“Programme guide”
“Fullscreen browsing”
“Stream info”
Keep the numeral's distinctive Druk/Monument-like condensed silhouette and the captions' precise monospaced character. Do not revert to thin geometric sans. No extra text.
Avoid: generic laptop, remote control, speaker, old CRT knobs, antenna rods, literal video or screen content, realistic app screenshot, technology dashboard, neon, glass panels, blue/purple glow, floating balls, new colors, decorative pseudo-text. Preserve the paper-and-graphite visual world of Image 1. The principal TV sculpture is graphite, with the existing small blue logo as the only color accent.
+59
View File
@@ -0,0 +1,59 @@
# Agent context map
Read the row for your task before editing. For a cross-domain change, read each
affected contract. This is navigation, not a request to load every linked file.
Common constraints remain in [AGENTS.md](../../AGENTS.md); Claude imports it.
Repository skill frontmatter defines triggers. If your client does not discover
a listed skill automatically, read its SKILL.md directly. Optional global tools
are not prerequisites for reading repository contracts.
## Development and maintenance
| Area / code ownership | Canonical documents | Repository skill |
| --- | --- | --- |
| Bootstrap, project placement, dependencies, aliases and lint configuration; root Nx config and project-local project.json files | [Nx boundaries](../architecture/nx-workspace-boundaries.md), [security overrides](../architecture/dependency-security-overrides.md) | [Nx architecture](../../.codex/skills/iptvnator-nx-architecture/SKILL.md) |
| Angular conventions; docs and skills maintenance | [Agent workflow](../development/agent-workflow.md) | Use the area's skill below |
| Unit, E2E, lint and coverage; `tools/coverage` | [Validation map](../architecture/validation-map.md) | Use the area's validation section |
| Electron entry/events/preload and CDP; `apps/electron-backend` | [Debugging and trace flags](../development/electron-debugging.md), [Electron security](../architecture/electron-security.md) | Use the available global electron skill for automation |
| Releases, notes, screenshots, native assets, Linux manager metadata; `tools/release` | [Release pipeline](../architecture/release-pipeline.md), [note format](../../.changes/README.md) | [Release notes](../../.codex/skills/release-notes/SKILL.md), [release cut](../../.codex/skills/release-cut/SKILL.md) |
## Data, sources and networking
| Area / code ownership | Required contracts | Repository skill |
| --- | --- | --- |
| SQLite schema/startup; `libs/shared/database`; Electron DB events/workers/operations | [DB worker](../architecture/sqlite-db-worker.md), [migration ownership and tests](../../libs/shared/database/README.md) | [SQLite worker](../../.codex/skills/iptvnator-sqlite-db-worker/SKILL.md) |
| M3U import/state/player, XMLTV, source lifecycle, startup readiness and OS file opening; `libs/m3u-state`, `libs/playlist`, `libs/epg` | [M3U module](../architecture/m3u-playlist-module.md), [adding sources across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Read both contracts when adding a source |
| Xtream API/store/data sources and routed views; `libs/portal/xtream` | [Xtream compatibility](../architecture/xtream-portal-compatibility.md), [category management](../architecture/category-management.md), [detail navigation](../architecture/portal-detail-navigation.md) | [Xtream](../../.codex/skills/xtream-electron/SKILL.md) |
| Stalker/Ministra protocol, identity, sessions and routed views; `libs/portal/stalker` | [Stalker portal](../architecture/stalker-portal.md), [Stalker EPG](../architecture/stalker-epg.md) for EPG work, [store API baseline](../architecture/stalker-store-api-baseline.md) for store API changes | [Stalker](../../.codex/skills/stalker-portal/SKILL.md) |
| Browser runtime, HTTP proxies, redirects and backend networking; `apps/web-backend`, `libs/shared/host-health` | [PWA/self-hosting](../architecture/pwa-self-hosted.md), [connectivity guard](../architecture/host-connectivity-guard.md), [Electron security](../architecture/electron-security.md) for desktop boundary changes | Read the affected runtime contract |
| Source health and selective cleanup; portal shared data access | [Desktop source health](../architecture/m3u-playlist-module.md#desktop-source-health), [subscription expiry](../architecture/workspace-dashboard.md#source-subscription-expiry) | Read the affected provider skill |
| Backup and restore; playlist persistence | [Backup/restore](../architecture/playlist-backup-restore.md), [database migrations](../../libs/shared/database/README.md) | Read the affected persistence skill |
## Playback, navigation and UI
| Area / code ownership | Required contracts | Repository skill |
| --- | --- | --- |
| Web engines, controls, tracks, PiP, radio and display sleep; `libs/ui/playback` | [Player controls](../architecture/player-controls-contract.md), [inline playback/diagnostics/recovery](../architecture/embedded-inline-playback.md) | Read the contract directly |
| Embedded MPV, platform engines, addon, pinned runtime and packaging; Electron native services, `tools/embedded-mpv` | [Native MPV](../architecture/embedded-mpv-native.md), [runtime build and licensing](../../tools/embedded-mpv/README.md) | Read the contract directly |
| Live panels, keyboard focus, grid/layout conventions; shared UI and portal views | [UI guidelines](../architecture/iptvnator-ui-guidelines.md), [detail navigation](../architecture/portal-detail-navigation.md) | [UI design](../../.codex/skills/iptvnator-ui-design/SKILL.md), [theme/style](../../.codex/skills/iptvnator-theme-style/SKILL.md) |
| Workspace routes, title bar, switcher, collections and dashboard; `libs/workspace` | [Workspace shell](../architecture/workspace-shell.md), [dashboard](../architecture/workspace-dashboard.md), [collection/detail navigation](../architecture/portal-detail-navigation.md) | UI/theme skills for visible changes |
| Remote control, playback queue, channel return and shortcuts; `libs/ui/remote-control`, `apps/remote-control-web` | [Remote control](../architecture/remote-control.md) | Provider skill when queue ownership changes |
| Downloads, offline details, catch-up and file availability; `libs/portal/downloads` | [Download manager](../architecture/download-manager.md), provider contract for URL resolution | Read the affected provider skill |
| VOD source discovery, factual metadata and failover; `libs/portal/shared/data-access` | [VOD multi-source](../architecture/vod-multi-source.md) | [Xtream](../../.codex/skills/xtream-electron/SKILL.md) |
| TMDB enrichment, artwork, actors and recommendations; `libs/services/src/lib/tmdb` | [TMDB contracts](../architecture/tmdb-metadata-enrichment.md), [dashboard](../architecture/workspace-dashboard.md) | UI skill for rendering changes |
| Timezones, catch-up formatting and EPG display offsets | [Date handling](../architecture/date-handling.md), [Xtream compatibility](../architecture/xtream-portal-compatibility.md), [M3U EPG](../architecture/m3u-playlist-module.md) | Affected provider skill |
| Website, blog and download pages; `apps/website` | [Website README](../../apps/website/README.md) | Use an available website skill |
| Mock servers and fictional release fixtures; `apps/stalker-mock-server`, `apps/xtream-mock-server` | [Stalker mock](../architecture/stalker-mock-server.md), [Xtream mock](../architecture/xtream-mock-server.md), [release screenshot contract](../architecture/release-pipeline.md) | Release-cut for release captures |
## Maintenance rules
Keep unique behavior contracts in the authoritative document, procedures in a
skill or development guide, and universal constraints in AGENTS.md. Update this
map when ownership or a canonical destination changes. Preserve release skill
mirrors; do not create extra copies of other contracts for individual agents.
Use normal Markdown links for document destinations so `pnpm run agents:validate`
can check them. Do not add root imports for the linked documents.
The [migration ledger](agent-guidance-migration.md) explains how the original
root instructions were accounted for. It is historical audit evidence, not
another source of current runtime policy or required task context.
@@ -0,0 +1,802 @@
# Agent guidance migration ledger
Issue #1643, 2026-09-20. Immutable source commit: `d4df0fd81a71f0bf196ceac2b45efe3697feb973`.
Original files: [AGENTS.md](https://github.com/4gray/iptvnator/blob/d4df0fd81a71f0bf196ceac2b45efe3697feb973/AGENTS.md)
(1,217 lines, 88,565 bytes) and [CLAUDE.md](https://github.com/4gray/iptvnator/blob/d4df0fd81a71f0bf196ceac2b45efe3697feb973/CLAUDE.md)
(2,022 lines, 231,858 bytes). Table coordinates refer to these immutable files,
not the shortened roots. This is audit evidence; it is not a required startup document.
## Method and coverage
Every non-empty source block was inventoried. Headings, standalone labels,
separator lines and Nx markers are structural; their contents are represented
below. Lists are split at each top-level item; code fences stay intact. The
17,847-character CLAUDE.md line 1315 is split into sentence-level entries so
individual constraints do not disappear behind one row. Repeated entries from
the two agents intentionally retain separate source references.
716 content entries are accounted for below; there are no unassigned
source blocks. "Existing contract" means the canonical document already carries
the behavior and was reviewed instead of copying another summary. "Added" and
"Moved" identify knowledge incorporated during this change. Destination sections
are entry points into the owning contract; adjacent subheadings cover supporting
exceptions and examples. The [context map](agent-context-map.md) is the live
navigation surface; this ledger records the one-time relocation.
## Corrections and consolidation decisions
- Root growth/mirroring requirements are replaced by one source of common rules
and topic-specific maintenance in [agent workflow](../development/agent-workflow.md#maintaining-canonical-knowledge).
Root limits do not apply to canonical reference docs; those load on demand.
- Stalker static URL wording was oversimplified. Missing flag evidence still
requires minting a link (with the directly playable radio exception), as
specified in [playback link resolution](../architecture/stalker-portal.md#playback-link-resolution)
and implemented by the existing Stalker link-semantics utilities. Keep the
authoritative decision table, not the old "otherwise static" shorthand.
- Generic Electron detection is not an individual capability gate. Desktop also
has Chromium settings storage, PWA supports browser-selected uploads, and SQL
and IndexedDB schemas are not identical. Preserve the existing DataFactory
boundary with these corrections in [service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases).
- Browser wake-lock auto-release clears the sentinel; the next media/visibility/
PiP sync reacquires. Preserve actual renderer behavior, not an implication of
immediate unconditional reacquisition; see [display sleep](../architecture/player-controls-contract.md#display-sleep-during-playback).
- Full-file formatting recovery must preserve unrelated user edits. Replace the
unconditional merge-base overwrite recipe with [safe reconstruction](../development/agent-workflow.md#protected-markdown-edits).
- The TypeScript hard limit is 400, not a variable 350–400; target under 300.
HostBinding/HostListener conventions are retained, not silently modernized.
- Nx tools and global skills are optional. Retain discovery fallbacks and the
managed markers in [AGENTS.md](../../AGENTS.md#general-guidelines-for-working-with-nx).
- Build/serve examples use local pnpm Nx or package scripts. Packaging uses
`package:app` (`make --prepackageOnly`), not the obsolete `electron-backend:package`
target suggested in the old alternative command.
- Version-specific signing patch rationale was missing from canonical docs and
now lives in [the dependency contract](../architecture/nx-workspace-boundaries.md#electron-builder-signing-patch).
- Exhaustive trees, diagrams and code examples are consolidated into the owning
architecture/maintenance sections; they do not become a second project map.
Existing snapshot inventories are navigation aids, not instructions to duplicate
every new project in the root. Plan saving respects active no-write modes.
## Block inventory
| Original source | Section and contract / example | Canonical destination | Disposition |
| --- | --- | --- | --- |
| AGENTS.md:3–3 | AGENTS.md — This file provides guidance to coding agents working in this repository. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:7–7 | Plan Mode — When an agent is in Plan Mode and produces a final &lt;proposed_plan&gt;, it must also save that finalized plan… | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:8–8 | Plan Mode — Save only finalized plans. Do not write interim exploration, questions, or draft revisions to .plans/. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:9–9 | Plan Mode — Use the filename pattern YYYY-MM-DD-short-topic.md such as .plans/2026-03-12-channel-filtering.md. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:10–10 | Plan Mode — If the intended filename already exists, append a numeric suffix such as -2, -3, and so on. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:14–14 | Agent Bootstrap — In a fresh worktree, run pnpm install --frozen-lockfile before relying on Nx project discovery, lint,… | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| AGENTS.md:15–15 | Agent Bootstrap — Re-run the install whenever the checkout moves — git pull, git reset --hard, a rebase, or a worktree… | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| AGENTS.md:16–16 | Agent Bootstrap — Never run prettier --write on CLAUDE.md, AGENTS.md or docs/. These files are not Prettier-clean upstream,… | [Protected Markdown edits](../development/agent-workflow.md#protected-markdown-edits) | Corrected safe restore |
| AGENTS.md:17–17 | Agent Bootstrap — After dependencies are installed, verify workspace discovery with pnpm nx show projects. | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| AGENTS.md:18–18 | Agent Bootstrap — Use scoped path aliases from tsconfig.base.json such as @iptvnator/services, @iptvnator/shared/interfaces,… | [Import Aliases and Public APIs](../architecture/nx-workspace-boundaries.md#import-aliases-and-public-apis) | Existing contract |
| AGENTS.md:19–19 | Agent Bootstrap — Every Nx project should keep scope:, domain:, and type: tags in project.json so… | [Project Tags](../architecture/nx-workspace-boundaries.md#project-tags) | Existing contract |
| AGENTS.md:20–20 | Agent Bootstrap — See docs/architecture/nx-workspace-boundaries.md for the current Nx tag and alias policy. | [Import Aliases and Public APIs](../architecture/nx-workspace-boundaries.md#import-aliases-and-public-apis) | Existing contract |
| AGENTS.md:21–22 | Agent Bootstrap — Keep nx and every official @nx/ package on the same exact version; run pnpm run deps:nx:validate after… | [Nx Dependency Updates](../architecture/nx-workspace-boundaries.md#nx-dependency-updates) | Existing contract |
| AGENTS.md:23–24 | Agent Bootstrap — Use the Node version in .nvmrc for development and CI. Angular 22 requires Node ^22.22.3 &#124;&#124; ^24.15.0 and… | [Angular 22 Toolchain Compatibility](../architecture/nx-workspace-boundaries.md#angular-22-toolchain-compatibility) | Existing contract |
| AGENTS.md:25–29 | Agent Bootstrap — Vite 8.1.5, resolved through Angular's build tooling, retains upstream precise matchers and adds bounded… | [Vite Dev-Server Patch](../architecture/nx-workspace-boundaries.md#vite-dev-server-patch) | Existing contract |
| AGENTS.md:30–40 | Agent Bootstrap — app-builder-lib 26.15.7 (electron-builder's macOS signing) is patched in… | [Electron Builder signing patch](../architecture/nx-workspace-boundaries.md#electron-builder-signing-patch) | Added missing detail |
| AGENTS.md:41–47 | Agent Bootstrap — node-gyp is a declared root devDependency because apps/electron-backend/build-embedded-mpv.js resolves it… | [Packaging State](../architecture/embedded-mpv-native.md#packaging-state) | Existing contract |
| AGENTS.md:48–52 | Agent Bootstrap — nx-electron@22.0.0 uses a local Nx 23 export-path patch and an explicit webpack-node-externals package… | [Angular 22 Toolchain Compatibility](../architecture/nx-workspace-boundaries.md#angular-22-toolchain-compatibility) | Existing contract |
| AGENTS.md:53–59 | Agent Bootstrap — A directory holding files consumed by other projects must be an Nx project. Nx builds its graph from… | [Shared Stylesheets and Cache Inputs](../architecture/nx-workspace-boundaries.md#shared-stylesheets-and-cache-inputs) | Existing contract |
| AGENTS.md:60–63 | Agent Bootstrap — Update Nx with pnpm nx migrate nx@&lt;target&gt; --skipInstall, regenerate the lockfile, run generated… | [Nx Dependency Updates](../architecture/nx-workspace-boundaries.md#nx-dependency-updates) | Existing contract |
| AGENTS.md:64–64 | Agent Bootstrap — ESLint enforces max-lines on TypeScript files: production code targets under 300 with a hard maximum of… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| AGENTS.md:65–65 | Agent Bootstrap — Project lint targets that shell out to eslint must quote the glob, e.g. eslint "apps/&lt;project&gt;//.ts". An… | [Command-Based Lint Targets](../architecture/nx-workspace-boundaries.md#command-based-lint-targets) | Existing contract |
| AGENTS.md:66–66 | Agent Bootstrap — Repository-specific skills live under .codex/skills/. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:67–68 | Agent Bootstrap — Frontmatter descriptions are trigger-only and begin with Use when; keep each skill at or below 500 words. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:69–70 | Agent Bootstrap — Run pnpm run skills:validate after editing a committed skill or a literal path it documents. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:71–72 | Agent Bootstrap — Keep .codex and .claude copies of release-notes and release-cut byte-identical. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:76–76 | Documentation After Changes — After implementing a meaningful change, agents must assess whether canonical repo docs need updates before… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:77–77 | Documentation After Changes — Meaningful changes include new or changed user-visible behavior, architecture or data-flow changes,… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:78–78 | Documentation After Changes — Skip doc updates for trivial refactors with unchanged behavior, formatting-only edits, and isolated… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:79–82 | Documentation After Changes — Prefer updating an existing authoritative doc before creating a new one: 1. README.md for top-level… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:83–83 | Documentation After Changes — Keep the root CLAUDE.md and this file up to date. They are living documents: whenever a change touches… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:84–84 | Documentation After Changes — When adding a new feature area, check whether the Architecture or Key Features sections of CLAUDE.md… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:85–85 | Documentation After Changes — Do not let CLAUDE.md or AGENTS.md drift: a stale path or route in these files poisons the context of every… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:86–86 | Documentation After Changes — Repo docs are canonical even when they were originally drafted by an LLM. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:87–87 | Documentation After Changes — Final task summaries should state whether docs were updated and which doc changed. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| AGENTS.md:91–91 | Release Notes For User-Visible Changes — Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change… | [File](../../.changes/README.md#file) | Existing contract |
| AGENTS.md:92–92 | Release Notes For User-Visible Changes — Name it &lt;area&gt;-&lt;short-slug&gt;.md; area matches the conventional-commit scope. There is no version field —… | [File](../../.changes/README.md#file) | Existing contract |
| AGENTS.md:93–93 | Release Notes For User-Visible Changes — Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| AGENTS.md:94–94 | Release Notes For User-Visible Changes — type: internal records invisible maintenance. Internal notes stay collapsed in CHANGELOG.md, are omitted… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| AGENTS.md:95–95 | Release Notes For User-Visible Changes — highlight: &lt;short headline&gt; (max 60 characters, rejected on type: internal) marks a note as one of the… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| AGENTS.md:96–96 | Release Notes For User-Visible Changes — Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior… | [When a note is not needed](../../.changes/README.md#when-a-note-is-not-needed) | Existing contract |
| AGENTS.md:97–97 | Release Notes For User-Visible Changes — CI enforces this: the "Release note gate" job in .github/workflows/ci.yml fails PRs that change runtime… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| AGENTS.md:98–98 | Release Notes For User-Visible Changes — The release-notes skill covers writing notes; the release-cut skill covers the full release sequence.… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| AGENTS.md:99–99 | Release Notes For User-Visible Changes — Validate before finishing: pnpm run release:notes:validate. | [Commands](../../.changes/README.md#commands) | Existing contract |
| AGENTS.md:100–100 | Release Notes For User-Visible Changes — Announcement drafts and highlight cards are built from the same notes: pnpm --silent run… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| AGENTS.md:101–101 | Release Notes For User-Visible Changes — Pushes to master and v can publish Docker images. A v tag build creates a draft GitHub release. | [Two phases](../architecture/release-pipeline.md#two-phases) | Existing contract |
| AGENTS.md:102–102 | Release Notes For User-Visible Changes — pnpm run release:verify:draft waits for that tag build (polling until the run is indexed, then gh run… | [Draft verification](../architecture/release-pipeline.md#draft-verification) | Existing contract |
| AGENTS.md:103–103 | Release Notes For User-Visible Changes — Publishing the GitHub release verifies its Snap assets and automatically uploads them to edge;… | [After verification](../architecture/release-pipeline.md#after-verification) | Existing contract |
| AGENTS.md:104–104 | Release Notes For User-Visible Changes — Release-post screenshots come only from the release capture script running against the mock servers. Never… | [Screenshots](../../.changes/README.md#screenshots) | Existing contract |
| AGENTS.md:105–105 | Release Notes For User-Visible Changes — Final task summaries should state whether a release note was added or why it was skipped. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| AGENTS.md:109–114 | AppImage Manager Metadata — AppManager full-download discovery uses appImage.desktop.entry URL fields. Electron Builder generates the… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| AGENTS.md:118–118 | Upgrade And Migration Compatibility — Users may skip releases. The application must apply all required migrations in dependency order when… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| AGENTS.md:119–119 | Upgrade And Migration Compatibility — Preserve migration paths for existing persisted data. Do not make deleting a database/profile or… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| AGENTS.md:120–120 | Upgrade And Migration Compatibility — Create required tables first, add missing columns before dependent indexes/triggers/queries, and make… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| AGENTS.md:121–121 | Upgrade And Migration Compatibility — For persistence changes, test real SQLite initialization with representative historical schemas and data,… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| AGENTS.md:122–122 | Upgrade And Migration Compatibility — See libs/shared/database/README.md for SQLite migration ownership and validation guidance. | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| AGENTS.md:126–126 | Regression Prevention And Test Updates — Before the final summary for any feature, behavior change, bug fix, data-flow change, Electron… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:127–127 | Regression Prevention And Test Updates — Bug fixes must normally include regression coverage that fails on the old behavior and passes with the… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:128–128 | Regression Prevention And Test Updates — Feature work and behavior changes must update existing tests when assertions, fixtures, mocks, routes, or… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:129–133 | Regression Prevention And Test Updates — Default validation ladder: 1. Run targeted unit tests for directly affected projects with pnpm nx test… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:134–134 | Regression Prevention And Test Updates — Electron-specific changes affecting IPC, SQLite, packaged runtime, external players, native file access,… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:135–135 | Regression Prevention And Test Updates — Final task summaries must list tests added or updated, validation commands run with results, and any… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| AGENTS.md:139–146 | Legacy Desktop Profile Migration — electron-profile-bootstrap.ts selects the known v0.19 electron-backend profile before eager main-process… | [Desktop upgrades from legacy profiles](../architecture/m3u-playlist-module.md#desktop-upgrades-from-legacy-profiles) | Existing contract |
| AGENTS.md:148–154 | Legacy Desktop Profile Migration — Startup shows AppStartupStatusComponent until the initial route and source inventory are ready, including… | [Desktop upgrades from legacy profiles](../architecture/m3u-playlist-module.md#desktop-upgrades-from-legacy-profiles) | Existing contract |
| AGENTS.md:158–158 | Electron Debugging (CDP) — Start the Electron development app with: nx serve electron-backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:159–159 | Electron Debugging (CDP) — Package-script equivalent: pnpm run serve:backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:160–160 | Electron Debugging (CDP) — Electron is configured to start with: --remote-debugging-port=9222 | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:161–161 | Electron Debugging (CDP) — Connect Chrome DevTools Protocol tools to: 127.0.0.1:9222 | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:162–162 | Electron Debugging (CDP) — For Electron automation/debugging tasks, use the electron skill | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:163–163 | Electron Debugging (CDP) — Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:164–164 | Electron Debugging (CDP) — If DevTools is open, agent-browser --cdp 9222 ... may attach to the DevTools page instead of the IPTVnator… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:165–165 | Electron Debugging (CDP) — If that happens, inspect targets with curl http://127.0.0.1:9222/json/list and connect directly to the… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:166–166 | Electron Debugging (CDP) — The app holds a single-instance lock (acquireSingleInstanceLock in… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:170–170 | Trace / Debug Startup — Full startup tracing: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:172–174 | Trace / Debug Startup — bash IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:176–184 | Trace / Debug Startup — Narrower trace flags: - IPTVNATOR_TRACE_IPC=1 traces renderer window.electron. bridge calls -… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:186–188 | Trace / Debug Startup — Settings, portal request/response, and trace payloads must use @iptvnator/shared/logging or the redacting… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:190–190 | Trace / Debug Startup — If local Nx state gets weird before a rerun: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:192–194 | Trace / Debug Startup — bash pnpm nx reset | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:198–203 | agent-browser (global install) — bash agent-browser --cdp 9222 tab list agent-browser --cdp 9222 tab 1 agent-browser --cdp 9222 snapshot -i… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:207–209 | Fallback — bash npx --yes agent-browser --cdp 9222 tab list | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:213–218 | DevTools Workaround — bash ELECTRON_OPEN_DEVTOOLS=1 nx serve electron-backend curl http://127.0.0.1:9222/json/list agent-browser… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| AGENTS.md:222–226 | Xtream Category Management — The Electron Live TV, Movies, and Series category dialog applies Select/Deselect to search results while a… | [Behavior Notes](../architecture/category-management.md#behavior-notes) | Existing contract |
| AGENTS.md:230–234 | XMLTV Response Compression — Electron decodes HTTP compression before the gzip file layer. For .gz/gzip metadata plus HTTP gzip, a… | [XMLTV response compression](../architecture/m3u-playlist-module.md#xmltv-response-compression) | Existing contract |
| AGENTS.md:238–256 | XMLTV Source Removal — Saving Settings → EPG reconciles cached XMLTV with committed global URLs and all enabled M3U playlist… | [XMLTV source lifecycle](../architecture/m3u-playlist-module.md#xmltv-source-lifecycle) | Existing contract |
| AGENTS.md:260–269 | Web Backend Provider Redirects — All four provider proxy routes use ValidatedHttpClient: automatic redirects are disabled, the initial URL… | [Web Backend](../architecture/pwa-self-hosted.md#web-backend) | Existing contract |
| AGENTS.md:273–278 | Portal Connectivity Preference — Half-open trial slots follow the complete request lifetime with no elapsed-time expiry. All four… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| AGENTS.md:279–286 | Portal Connectivity Preference — Desktop Settings &gt; General &gt; Portal connections exposes default-on Settings.portalConnectivityGuard. Only… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| AGENTS.md:287–289 | Portal Connectivity Preference — Both account-info dialogs explain guard refusals with localized paused-request copy and Retry now; Stalker… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| AGENTS.md:293–299 | Live Channel Return — Xtream and Stalker (including radio) capture displayed playback order on explicit selection. Remote… | [Live channel return and playback order](../architecture/remote-control.md#live-channel-return-and-playback-order) | Existing contract |
| AGENTS.md:303–309 | Stalker Live Search — ITV sidebar and fullscreen searches independently filter the complete selected category; only All Items… | [Full ITV Channel List Cache](../architecture/stalker-portal.md#full-itv-channel-list-cache) | Existing contract |
| AGENTS.md:313–339 | Live TV Panel Levels — Portal live layouts (Xtream live, Stalker itv/radio) fold their panels from the outside in, in three… | [Collapsible Live Sidebar](../architecture/iptvnator-ui-guidelines.md#collapsible-live-sidebar) | Existing contract |
| AGENTS.md:343–358 | Channel and Detail Keyboard Scrolling — Channel scroll owners use ChannelScrollFocusDirective; pointer selection focuses the viewport, native… | [Detail Scroll and Focus](../architecture/portal-detail-navigation.md#detail-scroll-and-focus) | Existing contract |
| AGENTS.md:362–370 | Xtream Connection Test — Add/Edit source Test HTTPS and HTTP discloses plaintext credential use before the click and can replace an… | [Explicit protocol discovery](../architecture/xtream-portal-compatibility.md#explicit-protocol-discovery) | Existing contract |
| AGENTS.md:374–382 | Xtream Live Auto Format — The routed Xtream live host supplies liveAutoTsUrl only for Auto with explicit HLS+TS account evidence,… | [Initial Auto HLS failure](../architecture/xtream-portal-compatibility.md#initial-auto-hls-failure) | Existing contract |
| AGENTS.md:386–402 | Xtream Catch-Up Server Timezone — The {Y-m-d:H-M} segment of a timeshift URL is read by the panel in ITS timezone (server_info.timezone),… | [Catch-Up Playback URLs](../architecture/xtream-portal-compatibility.md#catch-up-playback-urls) | Existing contract |
| AGENTS.md:406–406 | Radio / Audio Player — M3U playlists can contain radio channels identified by the radio="true" attribute on #EXTINF lines. When a… | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:408–408 | Radio / Audio Player — The dedicated AudioPlayerComponent (libs/ui/playback/src/lib/audio-player/) renders instead of a video player | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:409–409 | Radio / Audio Player — The audio player always uses the built-in inline player — external player settings (MPV/VLC) are ignored | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:410–410 | Radio / Audio Player — The EPG panel is hidden (radio streams have no EPG data) | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:411–411 | Radio / Audio Player — The layout uses a cinematic hero pattern: the station logo is blurred as a full-area backdrop with a… | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:412–412 | Radio / Audio Player — Volume is shared with the video player via localStorage key 'volume' | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:413–413 | Radio / Audio Player — Keyboard shortcuts: ArrowUp/ArrowDown (volume +/-5%), M (mute toggle) | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:414–414 | Radio / Audio Player — Radio detection in the video player template: activeChannel.radio === 'true' — this is a string… | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:416–416 | Radio / Audio Player — Key files: | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:418–418 | Radio / Audio Player — libs/ui/playback/src/lib/audio-player/audio-player.component.ts — the audio player component | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:419–419 | Radio / Audio Player — libs/ui/playback/src/lib/audio-player/audio-player.component.scss — cinematic hero styling | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:420–420 | Radio / Audio Player — libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html — template conditionals… | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:421–421 | Radio / Audio Player — libs/shared/interfaces/src/lib/channel.interface.ts — radio: string field on Channel interface | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Existing contract |
| AGENTS.md:425–434 | M3U Playback Mode — isLikelyM3uVod in libs/shared/m3u-utils recognizes video-file extensions and exact /movie/, /movies/,… | [M3U Playback Mode](../architecture/m3u-playlist-module.md#m3u-playback-mode) | Existing contract |
| AGENTS.md:438–440 | M3U URL User-Agent — PlaylistsService.getPlaylist() joins the per-playlist mutation queue so a route opened during refresh… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| AGENTS.md:441–445 | M3U URL User-Agent — The URL import form accepts an optional User-Agent and stores it as Playlist.userAgent. Electron sends it… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| AGENTS.md:446–448 | M3U URL User-Agent — Reuse the existing source editor and channel-over-playlist playback header precedence. Contract:… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| AGENTS.md:452–466 | Shared Player Controls — Stream info popover: an info button in the top-right corner of the shared controls overlay shows live… | [Stream info popover](../architecture/player-controls-contract.md#stream-info-popover) | Existing contract |
| AGENTS.md:468–473 | Shared Player Controls — The Embedded MPV native-view dock follows app theme tokens as a solid app surface, including Material… | [Player And EPG Theme Boundaries](../architecture/iptvnator-ui-guidelines.md#player-and-epg-theme-boundaries) | Existing contract |
| AGENTS.md:475–478 | Shared Player Controls — libs/ui/playback/src/lib/player-controls/ contains the additive, engine-neutral PlayerController contract,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| AGENTS.md:479–494 | Shared Player Controls — The subtitle menu carries capability-gated advanced subtitle support (#1408): external subtitle file… | [Advanced subtitle support](../architecture/player-controls-contract.md#advanced-subtitle-support) | Existing contract |
| AGENTS.md:495–501 | Shared Player Controls — In fullscreen, app-player-controls shows a pointer-transparent media-title overlay at the top while… | [Fullscreen media title](../architecture/player-controls-contract.md#fullscreen-media-title) | Existing contract |
| AGENTS.md:502–524 | Shared Player Controls — Auto-hide pauses while the pointer is over the controls bar or keyboard focus is inside it, but only… | [Keyboard ownership](../architecture/player-controls-contract.md#keyboard-ownership) | Existing contract |
| AGENTS.md:525–534 | Shared Player Controls — Persisted Settings.webPlayerSharedControls is default-ON (absent stored values coerce with !== false; only… | [Current status](../architecture/player-controls-contract.md#current-status) | Added missing detail |
| AGENTS.md:535–543 | Shared Player Controls — Settings.showCaptions is deliberately outside this rollout gate: it is engine state, not controls UI.… | [Current status](../architecture/player-controls-contract.md#current-status) | Existing contract |
| AGENTS.md:544–554 | Shared Player Controls — The modes differ in how long the preference is enforced. Shared controls are authoritative for the… | [Caption preference in both modes](../architecture/player-controls-contract.md#caption-preference-in-both-modes) | Existing contract |
| AGENTS.md:555–563 | Shared Player Controls — Shared controls include a per-session quality menu (Auto + "1080p"-style levels via setQualityLevel;… | [Quality (bitrate/level) selection](../architecture/player-controls-contract.md#quality-bitratelevel-selection) | Existing contract |
| AGENTS.md:564–568 | Shared Player Controls — Embedded MPV ignores the web-player preference. Frame-copy always uses shared DOM controls through its… | [Embedded MPV rendering constraints](../architecture/player-controls-contract.md#embedded-mpv-rendering-constraints) | Existing contract |
| AGENTS.md:569–576 | Shared Player Controls — Frame-copy shared controls own DOM surface interactions, shortcuts, fullscreen, and recording feedback.… | [Embedded MPV rendering constraints](../architecture/player-controls-contract.md#embedded-mpv-rendering-constraints) | Existing contract |
| AGENTS.md:577–660 | Shared Player Controls — WebPlayerViewComponent renders app-fullscreen-channel-panel… | [Fullscreen channel panel](../architecture/player-controls-contract.md#fullscreen-channel-panel) | Existing contract |
| AGENTS.md:661–670 | Shared Player Controls — Embedded MPV seek steps (arrow keys, ±10 s buttons, PlayerController.seekBy) go through the relative… | [Resume And Track Handling](../architecture/embedded-mpv-native.md#resume-and-track-handling) | Existing contract |
| AGENTS.md:671–676 | Shared Player Controls — M3U Favorites and Recently Viewed resolve Channel.drm into ResolvedPortalPlayback.drm through… | [DASH + ClearKey Playback](../architecture/m3u-playlist-module.md#dash--clearkey-playback) | Existing contract |
| AGENTS.md:677–694 | Shared Player Controls — DASH (.mpd) sources play through a lazily imported Shaka Player source engine… | [DASH + ClearKey Playback](../architecture/m3u-playlist-module.md#dash--clearkey-playback) | Existing contract |
| AGENTS.md:695–701 | Shared Player Controls — mpegts.js 1.8.1 errors from HTML5, Video.js, and ArtPlayer cross one version-locked structured evidence… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| AGENTS.md:702–834 | Shared Player Controls — Browser playback diagnostics and recovery policy live in libs/playback/util and are exported by… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| AGENTS.md:835–852 | Shared Player Controls — The built-in HTML5/hls.js player is the second guarded consumer. HtmlVideoPlayerComponent provides a… | [Web adapter and web-engine bridges](../architecture/player-controls-contract.md#web-adapter-and-web-engine-bridges) | Existing contract |
| AGENTS.md:853–886 | Shared Player Controls — Video.js is the third guarded consumer. VjsPlayerComponent provides a component-scoped… | [Web adapter and web-engine bridges](../architecture/player-controls-contract.md#web-adapter-and-web-engine-bridges) | Existing contract |
| AGENTS.md:887–904 | Shared Player Controls — ArtPlayer is the fourth guarded consumer. ArtPlayerComponent provides a component-scoped… | [Web adapter and web-engine bridges](../architecture/player-controls-contract.md#web-adapter-and-web-engine-bridges) | Existing contract |
| AGENTS.md:905–914 | Shared Player Controls — Shared web picture-in-picture stays inside that default-on rollout. PlayerController exposes capability… | [Standard element picture-in-picture](../architecture/player-controls-contract.md#standard-element-picture-in-picture) | Existing contract |
| AGENTS.md:915–933 | Shared Player Controls — WebVideoControlsAdapter supplies its current video and binding generation to… | [Standard element picture-in-picture](../architecture/player-controls-contract.md#standard-element-picture-in-picture) | Existing contract |
| AGENTS.md:934–935 | Shared Player Controls — Canonical docs: docs/architecture/player-controls-contract.md and docs/architecture/embedded-mpv-native.md | [Web adapter and web-engine bridges](../architecture/player-controls-contract.md#web-adapter-and-web-engine-bridges) | Existing contract |
| AGENTS.md:939–946 | Display Sleep During Playback — PlaybackKeepAwakeService (apps/web/src/app/services/playback-keep-awake.service.ts) watches every &lt;video&gt;… | [Display sleep during playback](../architecture/player-controls-contract.md#display-sleep-during-playback) | Added missing detail |
| AGENTS.md:947–953 | Display Sleep During Playback — Electron: a main-process powerSaveBlocker behind window.electron.setPlaybackKeepAwake… | [Display sleep during playback](../architecture/player-controls-contract.md#display-sleep-during-playback) | Added missing detail |
| AGENTS.md:954–956 | Display Sleep During Playback — Radio's &lt;audio&gt; deliberately never blocks display sleep. Embedded MPV holds its own blocker in… | [Display sleep during playback](../architecture/player-controls-contract.md#display-sleep-during-playback) | Added missing detail |
| AGENTS.md:960–962 | Windows Embedded MPV Pin Maintenance — PR, master, and tag builds resolve the Windows runtime only from… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| AGENTS.md:963–964 | Windows Embedded MPV Pin Maintenance — Validate the checked-in schema and provenance with pnpm embedded-mpv:windows-runtime-pin:check. | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| AGENTS.md:965–968 | Windows Embedded MPV Pin Maintenance — Prepare a manual rotation with pnpm embedded-mpv:windows-runtime-pin:refresh -- --force. The weekly… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| AGENTS.md:969–972 | Windows Embedded MPV Pin Maintenance — The PAT-backed refresh job must keep every third-party action pinned to a full commit. Do not mirror the… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Added missing detail |
| AGENTS.md:976–979 | Linux Embedded MPV Packaging — Official Linux frame-copy artifacts are x64-only. AppImage, DEB, RPM, Pacman, Snap, and Flatpak are… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:980–986 | Linux Embedded MPV Packaging — Packaging runs three isolated profiles: - system: DEB/RPM/Pacman, no private native/lib, with package… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:987–990 | Linux Embedded MPV Packaging — Flatpak is an isolated packaging pass and keeps iptvnator as the real Electron ELF so Electron Builder's… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:991–993 | Linux Embedded MPV Packaging — The DEB system-runtime contract is Ubuntu 24.04+ (libmpv2). Ubuntu 22.04 provides libmpv1, so use the x64… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:994–997 | Linux Embedded MPV Packaging — Only iptvnator_mpv_helper may link libmpv. The Electron executable, Electron libraries, embedded_mpv.node,… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:998–1002 | Linux Embedded MPV Packaging — electron-backend/native{,//} is excluded from app.asar; afterPack exclusively writes the… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:1003–1005 | Linux Embedded MPV Packaging — Packaged addon, frame-reader, and helper discovery is package-owned app.asar.unpacked only. Writable… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:1006–1010 | Linux Embedded MPV Packaging — Pristine afterPack/unpacked layouts scan Electron libraries recursively. Extracted Snap payloads exclude… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:1011–1015 | Linux Embedded MPV Packaging — Linux frame-copy availability is fail-closed. The packaged manifest, artifact modes, declared bundled… | [Linux Support Matrix](../architecture/embedded-mpv-native.md#linux-support-matrix) | Existing contract |
| AGENTS.md:1016–1027 | Linux Embedded MPV Packaging — Snap is core22/strict and uses an exact private shared-memory plug plus the graphics-core22 content plug… | [Frame-Copy Engine (Experimental, Apple Silicon, Linux and Windows)](../architecture/embedded-mpv-native.md#frame-copy-engine-experimental-apple-silicon-linux-and-windows) | Existing contract |
| AGENTS.md:1028–1054 | Linux Embedded MPV Packaging — The probe and playback helper share one sanitized loader environment: ambient audit, preload, library,… | [Frame-Copy Engine (Experimental, Apple Silicon, Linux and Windows)](../architecture/embedded-mpv-native.md#frame-copy-engine-experimental-apple-silicon-linux-and-windows) | Existing contract |
| AGENTS.md:1055–1059 | Linux Embedded MPV Packaging — In the exact packaged Flatpak /app context, reconstruct only Freedesktop Platform 24.08's immutable… | [Frame-Copy Engine (Experimental, Apple Silicon, Linux and Windows)](../architecture/embedded-mpv-native.md#frame-copy-engine-experimental-apple-silicon-linux-and-windows) | Existing contract |
| AGENTS.md:1060–1063 | Linux Embedded MPV Packaging — The packaged x64 Playwright smoke runs its fixture-contract target first and passes Chromium… | [Same-Version Desktop Release Gate](../architecture/embedded-mpv-native.md#same-version-desktop-release-gate) | Existing contract |
| AGENTS.md:1064–1116 | Linux Embedded MPV Packaging — Bundled Linux releases must publish the exact source archives/git records, checksums, licenses, flags,… | [Same-Version Desktop Release Gate](../architecture/embedded-mpv-native.md#same-version-desktop-release-gate) | Existing contract |
| AGENTS.md:1120–1120 | Repo Skills — .codex/skills/iptvnator-nx-architecture/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1121–1121 | Repo Skills — .codex/skills/iptvnator-sqlite-db-worker/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1122–1122 | Repo Skills — .codex/skills/iptvnator-theme-style/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1123–1123 | Repo Skills — .codex/skills/iptvnator-ui-design/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1124–1124 | Repo Skills — .codex/skills/release-cut/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1125–1125 | Repo Skills — .codex/skills/release-notes/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1126–1126 | Repo Skills — .codex/skills/stalker-portal/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1127–1127 | Repo Skills — .codex/skills/xtream-electron/SKILL.md | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1129–1130 | Repo Skills — Descriptions and trigger conditions are canonical in each skill's frontmatter; do not duplicate them here. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| AGENTS.md:1137–1137 | General Guidelines for working with Nx — For navigating/exploring the workspace, invoke the nx-workspace skill first when it is available - it has… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1138–1138 | General Guidelines for working with Nx — When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through nx… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1139–1139 | General Guidelines for working with Nx — Prefix nx commands with the workspace's package manager (e.g., pnpm nx build, npm exec nx test) - avoids… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1140–1140 | General Guidelines for working with Nx — You have access to the Nx MCP server and its tools, use them to help the user | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1141–1141 | General Guidelines for working with Nx — For Nx plugin best practices, check node_modules/@nx/&lt;plugin&gt;/PLUGIN.md. Not all plugins have this file -… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1142–1142 | General Guidelines for working with Nx — NEVER guess CLI flags - always check nx_docs or --help first when unsure | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1146–1146 | Scaffolding & Generators — For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the nx-generate skill… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1150–1150 | When to use nx_docs — USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1151–1151 | When to use nx_docs — DON'T USE for: basic generator syntax (nx g @nx/react:app), standard commands, things you already know | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1152–1152 | When to use nx_docs — The nx-generate skill handles generator discovery internally - don't call nx_docs just to look up… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| AGENTS.md:1158–1162 | Catch-Up URL Copying — EPG timeline/list programme details expose Copy archive URL for supported Xtream/M3U archives, including… | [Copy archive URL](../architecture/m3u-playlist-module.md#copy-archive-url) | Existing contract |
| AGENTS.md:1166–1196 | Xtream Archive Downloads — Desktop Xtream Live TV programme details can enqueue completed catch-up as contentType: catchup. The queue… | [Xtream archive downloads](../architecture/download-manager.md#xtream-archive-downloads) | Existing contract |
| AGENTS.md:1200–1205 | Desktop Source Health — Electron switcher/source rows share bounded, cached Xtream/Stalker/M3U URL checks through… | [Desktop source health](../architecture/m3u-playlist-module.md#desktop-source-health) | Existing contract |
| AGENTS.md:1207–1213 | Desktop Source Health — Desktop Sources also offers library-wide selective cleanup through dialog-scoped SourceCleanupService.… | [Desktop inactive-source cleanup](../architecture/m3u-playlist-module.md#desktop-inactive-source-cleanup) | Existing contract |
| AGENTS.md:1215–1217 | Desktop Source Health — Startup source auto-refresh uses SourceActivityService to protect busy IDs from cleanup. Late batch… | [Desktop inactive-source cleanup](../architecture/m3u-playlist-module.md#desktop-inactive-source-cleanup) | Existing contract |
| CLAUDE.md:3–3 | CLAUDE.md — This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:5–5 | CLAUDE.md — &gt; The process sections below (Plan Mode, Documentation After Changes, Upgrade And Migration Compatibility,… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:9–9 | Plan Mode — When Claude Code is in Plan Mode and produces a final &lt;proposed_plan&gt;, it must also save that finalized… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:10–10 | Plan Mode — Save only finalized plans. Do not write interim exploration, question turns, or draft revisions to .plans/. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:11–11 | Plan Mode — Use the filename pattern YYYY-MM-DD-short-topic.md such as .plans/2026-03-12-channel-filtering.md. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:12–12 | Plan Mode — If the intended filename already exists, append a numeric suffix such as -2, -3, and so on. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:16–16 | Documentation After Changes — After implementing a meaningful change, Claude Code must assess whether canonical repo docs need updates… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:17–17 | Documentation After Changes — Meaningful changes include new or changed user-visible behavior, architecture or data-flow changes,… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:18–18 | Documentation After Changes — Skip doc updates for trivial refactors with unchanged behavior, formatting-only edits, and isolated… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:19–22 | Documentation After Changes — Prefer updating an existing authoritative doc before creating a new one: 1. README.md for top-level… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:23–23 | Documentation After Changes — Keep this file (CLAUDE.md) itself up to date. It is a living document: whenever a change touches something… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:24–24 | Documentation After Changes — When adding a new feature area, check whether the Architecture or Key Features sections of CLAUDE.md… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:25–25 | Documentation After Changes — Do not let CLAUDE.md drift: a stale path or route in this file poisons the context of every future agent… | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:26–26 | Documentation After Changes — Repo docs are canonical even when they were originally drafted by an LLM. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:27–27 | Documentation After Changes — Final task summaries should state whether docs were updated and which doc changed. | [Maintaining canonical knowledge](../development/agent-workflow.md#maintaining-canonical-knowledge) | Moved / consolidated |
| CLAUDE.md:31–31 | Release Notes For User-Visible Changes — Any change a user could notice — new behavior, changed behavior, bug fix, performance win, breaking change… | [File](../../.changes/README.md#file) | Existing contract |
| CLAUDE.md:32–32 | Release Notes For User-Visible Changes — Name it &lt;area&gt;-&lt;short-slug&gt;.md; area matches the conventional-commit scope. There is no version field —… | [File](../../.changes/README.md#file) | Existing contract |
| CLAUDE.md:33–33 | Release Notes For User-Visible Changes — Write the body for a user, not a reviewer: "the player now remembers volume between episodes", not "hoist… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| CLAUDE.md:34–34 | Release Notes For User-Visible Changes — type: internal records invisible maintenance. Internal notes stay collapsed in CHANGELOG.md, are omitted… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| CLAUDE.md:35–35 | Release Notes For User-Visible Changes — highlight: &lt;short headline&gt; (max 60 characters, rejected on type: internal) marks a note as one of the… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| CLAUDE.md:36–36 | Release Notes For User-Visible Changes — Skip the note for test-only changes, docs, CI/workflow plumbing, and pure refactors with no behavior… | [When a note is not needed](../../.changes/README.md#when-a-note-is-not-needed) | Existing contract |
| CLAUDE.md:37–37 | Release Notes For User-Visible Changes — CI enforces this: the "Release note gate" job in .github/workflows/ci.yml fails PRs that change runtime… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| CLAUDE.md:38–38 | Release Notes For User-Visible Changes — The release-notes skill covers writing notes; the release-cut skill covers the full release sequence.… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| CLAUDE.md:39–39 | Release Notes For User-Visible Changes — Validate before finishing: pnpm run release:notes:validate. | [Commands](../../.changes/README.md#commands) | Existing contract |
| CLAUDE.md:40–40 | Release Notes For User-Visible Changes — Announcement drafts and highlight cards are built from the same notes: pnpm --silent run… | [Writing the body](../../.changes/README.md#writing-the-body) | Existing contract |
| CLAUDE.md:41–41 | Release Notes For User-Visible Changes — Pushes to master and v can publish Docker images. A v tag build creates a draft GitHub release. | [Two phases](../architecture/release-pipeline.md#two-phases) | Existing contract |
| CLAUDE.md:42–42 | Release Notes For User-Visible Changes — pnpm run release:verify:draft waits for that tag build (polling until the run is indexed, then gh run… | [Draft verification](../architecture/release-pipeline.md#draft-verification) | Existing contract |
| CLAUDE.md:43–43 | Release Notes For User-Visible Changes — Publishing the GitHub release verifies its Snap assets and automatically uploads them to edge;… | [After verification](../architecture/release-pipeline.md#after-verification) | Existing contract |
| CLAUDE.md:44–44 | Release Notes For User-Visible Changes — Release-post screenshots come only from the release capture script running against the mock servers. Never… | [Screenshots](../../.changes/README.md#screenshots) | Existing contract |
| CLAUDE.md:45–45 | Release Notes For User-Visible Changes — Final task summaries should state whether a release note was added or why it was skipped. | [Plans and completion reports](../development/agent-workflow.md#plans-and-completion-reports) | Moved / consolidated |
| CLAUDE.md:49–54 | AppImage Manager Metadata — AppManager full-download discovery uses appImage.desktop.entry URL fields. Electron Builder generates the… | [Surfaces built from one set of notes](../architecture/release-pipeline.md#surfaces-built-from-one-set-of-notes) | Existing contract |
| CLAUDE.md:58–58 | Upgrade And Migration Compatibility — Users may skip releases. The application must apply all required migrations in dependency order when… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:59–59 | Upgrade And Migration Compatibility — Preserve migration paths for existing persisted data. Do not make deleting a database/profile or… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:60–60 | Upgrade And Migration Compatibility — Create required tables first, add missing columns before dependent indexes/triggers/queries, and make… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:61–61 | Upgrade And Migration Compatibility — For persistence changes, test real SQLite initialization with representative historical schemas and data,… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:62–62 | Upgrade And Migration Compatibility — See libs/shared/database/README.md for SQLite migration ownership and validation guidance. | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:66–66 | Regression Prevention And Test Updates — Before the final summary for any feature, behavior change, bug fix, data-flow change, Electron… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:67–67 | Regression Prevention And Test Updates — Bug fixes must normally include regression coverage that fails on the old behavior and passes with the… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:68–68 | Regression Prevention And Test Updates — Feature work and behavior changes must update existing tests when assertions, fixtures, mocks, routes, or… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:69–73 | Regression Prevention And Test Updates — Default validation ladder: 1. Run targeted unit tests for directly affected projects with pnpm nx test… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:74–74 | Regression Prevention And Test Updates — Electron-specific changes affecting IPC, SQLite, packaged runtime, external players, native file access,… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:75–75 | Regression Prevention And Test Updates — Final task summaries must list tests added or updated, validation commands run with results, and any… | [Test impact and completion](../architecture/validation-map.md#test-impact-and-completion) | Existing contract |
| CLAUDE.md:79–79 | Project Overview — IPTVnator is a cross-platform IPTV player application built with Angular and Electron, supporting M3U/M3U8… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Existing contract |
| CLAUDE.md:81–81 | Project Overview — Dual Environment Support: The application is designed to work in both Electron and as a Progressive Web… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Existing contract |
| CLAUDE.md:87–90 | Agent Bootstrap — bash pnpm install --frozen-lockfile pnpm nx show projects | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| CLAUDE.md:92–92 | Agent Bootstrap — Run the install step in a fresh worktree before relying on Nx discovery, lint, test, or build commands.… | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| CLAUDE.md:93–93 | Agent Bootstrap — Re-run the install whenever the checkout moves — git pull, git reset --hard, a rebase, or a worktree… | [Fresh Worktree Bootstrap and Discovery](../architecture/nx-workspace-boundaries.md#fresh-worktree-bootstrap-and-discovery) | Existing contract |
| CLAUDE.md:94–94 | Agent Bootstrap — Never run prettier --write on CLAUDE.md, AGENTS.md or docs/. These files are not Prettier-clean upstream,… | [Protected Markdown edits](../development/agent-workflow.md#protected-markdown-edits) | Corrected safe restore |
| CLAUDE.md:95–95 | Agent Bootstrap — Use scoped path aliases from tsconfig.base.json such as @iptvnator/services, @iptvnator/shared/interfaces,… | [Import Aliases and Public APIs](../architecture/nx-workspace-boundaries.md#import-aliases-and-public-apis) | Existing contract |
| CLAUDE.md:96–96 | Agent Bootstrap — Do not add new imports from legacy bare aliases such as services, shared-interfaces, components,… | [Import Aliases and Public APIs](../architecture/nx-workspace-boundaries.md#import-aliases-and-public-apis) | Existing contract |
| CLAUDE.md:97–97 | Agent Bootstrap — Every Nx project should keep scope:, domain:, and type: tags in project.json. | [Project Tags](../architecture/nx-workspace-boundaries.md#project-tags) | Existing contract |
| CLAUDE.md:98–98 | Agent Bootstrap — See docs/architecture/nx-workspace-boundaries.md for the current Nx tag and alias policy. | [Import Aliases and Public APIs](../architecture/nx-workspace-boundaries.md#import-aliases-and-public-apis) | Existing contract |
| CLAUDE.md:99–100 | Agent Bootstrap — Keep nx and every official @nx/ package on the same exact version; run pnpm run deps:nx:validate after… | [Nx Dependency Updates](../architecture/nx-workspace-boundaries.md#nx-dependency-updates) | Existing contract |
| CLAUDE.md:101–102 | Agent Bootstrap — Use the Node version in .nvmrc for development and CI. Angular 22 requires Node ^22.22.3 &#124;&#124; ^24.15.0 and… | [Angular 22 Toolchain Compatibility](../architecture/nx-workspace-boundaries.md#angular-22-toolchain-compatibility) | Existing contract |
| CLAUDE.md:103–107 | Agent Bootstrap — Vite 8.1.5, resolved through Angular's build tooling, retains upstream precise matchers and adds bounded… | [Vite Dev-Server Patch](../architecture/nx-workspace-boundaries.md#vite-dev-server-patch) | Existing contract |
| CLAUDE.md:108–118 | Agent Bootstrap — app-builder-lib 26.15.7 (electron-builder's macOS signing) is patched in… | [Electron Builder signing patch](../architecture/nx-workspace-boundaries.md#electron-builder-signing-patch) | Added missing detail |
| CLAUDE.md:119–125 | Agent Bootstrap — node-gyp is a declared root devDependency because apps/electron-backend/build-embedded-mpv.js resolves it… | [Packaging State](../architecture/embedded-mpv-native.md#packaging-state) | Existing contract |
| CLAUDE.md:126–130 | Agent Bootstrap — nx-electron@22.0.0 uses a local Nx 23 export-path patch and an explicit webpack-node-externals package… | [Angular 22 Toolchain Compatibility](../architecture/nx-workspace-boundaries.md#angular-22-toolchain-compatibility) | Existing contract |
| CLAUDE.md:131–137 | Agent Bootstrap — A directory holding files consumed by other projects must be an Nx project. Nx builds its graph from… | [Shared Stylesheets and Cache Inputs](../architecture/nx-workspace-boundaries.md#shared-stylesheets-and-cache-inputs) | Existing contract |
| CLAUDE.md:138–141 | Agent Bootstrap — Update Nx with pnpm nx migrate nx@&lt;target&gt; --skipInstall, regenerate the lockfile, run generated… | [Nx Dependency Updates](../architecture/nx-workspace-boundaries.md#nx-dependency-updates) | Existing contract |
| CLAUDE.md:142–142 | Agent Bootstrap — Repository-specific skills live under .codex/skills/. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| CLAUDE.md:143–144 | Agent Bootstrap — Frontmatter descriptions are trigger-only and begin with Use when; keep each skill at or below 500 words. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| CLAUDE.md:145–146 | Agent Bootstrap — Run pnpm run skills:validate after editing a committed skill or a literal path it documents. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| CLAUDE.md:147–148 | Agent Bootstrap — Keep .codex and .claude copies of release-notes and release-cut byte-identical. | [Repository skills](../development/agent-workflow.md#repository-skills) | Moved / consolidated |
| CLAUDE.md:152–192 | Building and Serving — bash # Serve the Angular web app only (development mode, baseHref="/") pnpm run serve:frontend # or nx… | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:196–198 | Windows Embedded MPV Pin Maintenance — PR, master, and tag builds resolve the Windows runtime only from… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| CLAUDE.md:199–200 | Windows Embedded MPV Pin Maintenance — Validate the checked-in schema and provenance with pnpm embedded-mpv:windows-runtime-pin:check. | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| CLAUDE.md:201–204 | Windows Embedded MPV Pin Maintenance — Prepare a manual rotation with pnpm embedded-mpv:windows-runtime-pin:refresh -- --force. The weekly… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| CLAUDE.md:205–208 | Windows Embedded MPV Pin Maintenance — The PAT-backed refresh job must keep every third-party action pinned to a full commit. Do not mirror the… | [Windows CI pin lifecycle](../../tools/embedded-mpv/README.md#windows-ci-pin-lifecycle) | Existing contract |
| CLAUDE.md:212–212 | Electron CDP Debugging — Start Electron in dev mode with: nx serve electron-backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:213–213 | Electron CDP Debugging — Package-script equivalent: pnpm run serve:backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:214–214 | Electron CDP Debugging — The workspace is configured to always launch Electron with: --remote-debugging-port=9222 | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:215–215 | Electron CDP Debugging — Use CDP clients (Chrome DevTools Protocol tools) against: 127.0.0.1:9222 | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:216–216 | Electron CDP Debugging — When the task is Electron automation/debugging, use the electron skill | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:217–217 | Electron CDP Debugging — Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:218–218 | Electron CDP Debugging — If DevTools is open, agent-browser --cdp 9222 ... may attach to the DevTools page instead of the IPTVnator… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:219–219 | Electron CDP Debugging — The app holds a single-instance lock (acquireSingleInstanceLock in… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:221–221 | Electron CDP Debugging — For startup tracing or white-screen debugging: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:223–225 | Electron CDP Debugging — bash IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:227–227 | Electron CDP Debugging — Useful narrower flags: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:229–229 | Electron CDP Debugging — IPTVNATOR_TRACE_IPC=1 traces renderer window.electron. bridge calls | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:230–230 | Electron CDP Debugging — IPTVNATOR_TRACE_DB=1 traces DB worker requests and DB progress events | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:231–231 | Electron CDP Debugging — IPTVNATOR_TRACE_SQL=1 traces SQLite statements in both main and worker connections | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:232–232 | Electron CDP Debugging — IPTVNATOR_TRACE_WINDOW=1 traces BrowserWindow navigation/load lifecycle | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:233–233 | Electron CDP Debugging — IPTVNATOR_TRACE_PLAYER=1 traces external-player activity, bounded Embedded MPV runtime-probe stderr, and… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:234–234 | Electron CDP Debugging — IPTVNATOR_TRACE_RENDERER_CONSOLE=1 mirrors renderer console logs into the Electron terminal | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:235–235 | Electron CDP Debugging — IPTVNATOR_PERF_CAPTURE=1 enables development/test-only, redacted M3U and Xtream preload IPC… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:236–236 | Electron CDP Debugging — IPTVNATOR_PERF_WORKER_PROFILING=1 enables development/test-only, request-scoped worker… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:238–240 | Electron CDP Debugging — Settings, portal request/response, and trace payloads must use @iptvnator/shared/logging or the redacting… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:242–242 | Electron CDP Debugging — If the Nx daemon gets into a bad state before rerunning Electron: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:244–246 | Electron CDP Debugging — bash pnpm nx reset | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:248–248 | Electron CDP Debugging — Use global agent-browser (preferred): | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:250–263 | Electron CDP Debugging — bash # Verify CDP targets agent-browser --cdp 9222 tab list # Switch to the app tab and inspect… | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:265–265 | Electron CDP Debugging — If agent-browser is not in PATH, use: | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:267–269 | Electron CDP Debugging — bash npx --yes agent-browser --cdp 9222 tab list | [Start and attach](../development/electron-debugging.md#start-and-attach) | Moved / consolidated |
| CLAUDE.md:273–294 | Testing — bash # Run frontend tests pnpm run test:frontend # or pnpm nx test web # Run backend tests pnpm run… | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:296–296 | Testing — Before finishing behavior changes or bug fixes, follow Regression Prevention And Test Updates above and… | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:300–307 | Linting — bash # Lint all projects (CI runs this on master; PRs lint affected projects) pnpm run lint # Lint a… | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:309–315 | Linting — CI lints affected projects on PRs (nx affected) and every project on master pushes… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:317–317 | Linting — Production TypeScript: hard maximum 400 lines. | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:318–324 | Linting — Tests: 1200. /.spec.ts, /.spec-data.ts, /.e2e.ts and everything under apps/-e2e/ — a spec is a flat list… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:325–326 | Linting — Blank lines and comments are not counted (skipBlankLines, skipComments), so a docblock is never the reason… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:328–340 | Linting — Pre-existing oversized files are baselined in tools/eslint/max-lines-baseline.mjs; regenerate the baseline… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:342–349 | Linting — Project lint targets that shell out to eslint must quote the glob, e.g. eslint "apps/&lt;project&gt;//.ts". An… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:353–360 | Legacy Desktop Profile Migration — electron-profile-bootstrap.ts selects the known v0.19 electron-backend profile before eager main-process… | [Desktop upgrades from legacy profiles](../architecture/m3u-playlist-module.md#desktop-upgrades-from-legacy-profiles) | Existing contract |
| CLAUDE.md:362–368 | Legacy Desktop Profile Migration — Startup shows AppStartupStatusComponent until the initial route and source inventory are ready, including… | [Desktop upgrades from legacy profiles](../architecture/m3u-playlist-module.md#desktop-upgrades-from-legacy-profiles) | Existing contract |
| CLAUDE.md:374–374 | Monorepo Structure (Nx Workspace) — This is an Nx monorepo with the following structure: | [Placement Decision](../architecture/nx-workspace-boundaries.md#placement-decision) | Existing contract |
| CLAUDE.md:376–376 | Monorepo Structure (Nx Workspace) — apps/web - Angular application (frontend, shared by Electron and PWA) | [Placement Decision](../architecture/nx-workspace-boundaries.md#placement-decision) | Existing contract |
| CLAUDE.md:377–377 | Monorepo Structure (Nx Workspace) — apps/electron-backend - Electron main process | [Main-process ownership](../development/electron-debugging.md#main-process-ownership) | Moved / consolidated |
| CLAUDE.md:378–378 | Monorepo Structure (Nx Workspace) — apps/web-backend - HTTP backend for the self-hosted PWA (/parse, /parse-xml, /xtream, /stalker CORS proxy… | [Web Backend](../architecture/pwa-self-hosted.md#web-backend) | Existing contract |
| CLAUDE.md:379–379 | Monorepo Structure (Nx Workspace) — apps/remote-control-web - Mobile remote-control web app served by the Electron backend | [Remote Web App](../architecture/remote-control.md#remote-web-app) | Existing contract |
| CLAUDE.md:380–380 | Monorepo Structure (Nx Workspace) — apps/web-e2e - Playwright E2E tests against the web app | [E2E](../architecture/validation-map.md#e2e) | Existing contract |
| CLAUDE.md:381–381 | Monorepo Structure (Nx Workspace) — apps/electron-backend-e2e - Playwright E2E tests against the Electron app | [E2E](../architecture/validation-map.md#e2e) | Existing contract |
| CLAUDE.md:382–382 | Monorepo Structure (Nx Workspace) — apps/stalker-mock-server - Mock Stalker/Ministra portal for dev and E2E | [Stalker Mock Server Architecture](../architecture/stalker-mock-server.md#stalker-mock-server-architecture) | Existing contract |
| CLAUDE.md:383–383 | Monorepo Structure (Nx Workspace) — apps/xtream-mock-server - Mock Xtream Codes API for dev and E2E | [Xtream Mock Server — Architecture](../architecture/xtream-mock-server.md#xtream-mock-server--architecture) | Existing contract |
| CLAUDE.md:384–384 | Monorepo Structure (Nx Workspace) — apps/website - Astro + Tailwind landing page, blog (guides carry faq: frontmatter → FAQPage JSON-LD and… | [IPTVnator Website](../../apps/website/README.md#iptvnator-website) | Existing contract |
| CLAUDE.md:385–411 | Monorepo Structure (Nx Workspace) — libs/ - Shared libraries: - epg/data-access - EPG services, runtime bridge, program normalization -… | [Placement Decision](../architecture/nx-workspace-boundaries.md#placement-decision) | Existing contract |
| CLAUDE.md:415–415 | Frontend Architecture (Angular) — State Management: Uses NgRx for playlist state management: | [State Management (libs/m3u-state/)](../architecture/m3u-playlist-module.md#state-management-libsm3u-state) | Existing contract |
| CLAUDE.md:417–417 | Frontend Architecture (Angular) — Store configuration in apps/web/src/app/app.config.ts | [State Management (libs/m3u-state/)](../architecture/m3u-playlist-module.md#state-management-libsm3u-state) | Existing contract |
| CLAUDE.md:418–418 | Frontend Architecture (Angular) — Playlist state, actions, effects, and reducers in libs/m3u-state/ | [State Management (libs/m3u-state/)](../architecture/m3u-playlist-module.md#state-management-libsm3u-state) | Existing contract |
| CLAUDE.md:419–419 | Frontend Architecture (Angular) — Entity adapter pattern for managing playlists collection | [State Management (libs/m3u-state/)](../architecture/m3u-playlist-module.md#state-management-libsm3u-state) | Existing contract |
| CLAUDE.md:420–420 | Frontend Architecture (Angular) — Router store integration for route-based state | [State Management (libs/m3u-state/)](../architecture/m3u-playlist-module.md#state-management-libsm3u-state) | Existing contract |
| CLAUDE.md:422–422 | Frontend Architecture (Angular) — XtreamStore Architecture (Signal Store with Feature Composition): | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:424–424 | Frontend Architecture (Angular) — The Xtream Codes module uses NgRx Signal Store with a layered architecture: | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:426–455 | Frontend Architecture (Angular) — ┌─────────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ Components… | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:457–457 | Frontend Architecture (Angular) — File structure: | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:459–492 | Frontend Architecture (Angular) — libs/portal/xtream/ ├── data-access/src/lib/ │ ├── stores/ │ │ ├── features/ │ │ │ ├──… | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:494–494 | Frontend Architecture (Angular) — Key patterns: | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:496–496 | Frontend Architecture (Angular) — Feature stores: Each with.feature.ts uses signalStoreFeature() for focused functionality | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:497–497 | Frontend Architecture (Angular) — Facade pattern: XtreamStore composes all features, maintaining backward compatibility | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:498–499 | Frontend Architecture (Angular) — Data source abstraction: IXtreamDataSource has SQLite-backed and API/in-memory implementations | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:500–503 | Frontend Architecture (Angular) — Factory injection: provideXtreamDataSource() selects ElectronXtreamDataSource only when… | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:504–516 | Frontend Architecture (Angular) — Catalog lazy loading: catalog grids scroll infinitely instead of paging. withSelection keeps a… | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:518–518 | Frontend Architecture (Angular) — Xtream data strategies by runtime capability: | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:520–523 | Frontend Architecture (Angular) — &#124; Capability &#124; Strategy &#124; &#124; --------------------------------- &#124;… | [Store composition and catalog windowing](../architecture/xtream-portal-compatibility.md#store-composition-and-catalog-windowing) | Added / consolidated |
| CLAUDE.md:527–527 | M3U Playlist Module Architecture: — The M3U playlist module handles traditional M3U/M3U8 playlists with support for 90,000+ channels. | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:529–543 | M3U Playlist Module Architecture: — ┌─────────────────────────────────────────────────────────────────────┐ │ VIDEO PLAYER PAGE │ │… | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:545–545 | M3U Playlist Module Architecture: — The live EPG panel is a horizontal timeline ribbon under the player (app-epg-timeline,… | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:547–547 | M3U Playlist Module Architecture: — Collapsible live channel rail (M3U player, Xtream/Stalker live layouts, unified favorites/recent live… | [Collapsible Live Sidebar](../architecture/iptvnator-ui-guidelines.md#collapsible-live-sidebar) | Existing contract |
| CLAUDE.md:549–549 | M3U Playlist Module Architecture: — Cover grids (Xtream/Stalker VOD + series catalogs, favorites/recent, dashboard rails): sized by… | [Cover Grids](../architecture/iptvnator-ui-guidelines.md#cover-grids) | Existing contract |
| CLAUDE.md:551–551 | M3U Playlist Module Architecture: — Radio Channel Layout (when channel.radio === 'true'): | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:553–567 | M3U Playlist Module Architecture: — ┌─────────────────────────────────────────────────────────────────────┐ │ ┌─────────────┐… | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:569–569 | M3U Playlist Module Architecture: — Key radio behavior: | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:571–571 | M3U Playlist Module Architecture: — Detection: channel.radio === 'true' (string from M3U radio attribute) | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:572–572 | M3U Playlist Module Architecture: — The audio player always renders inline — shouldShowInlinePlayer is bypassed for radio | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:573–573 | M3U Playlist Module Architecture: — EPG panel is conditionally hidden in the template when radio is active | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:574–574 | M3U Playlist Module Architecture: — Volume is shared with video player via localStorage key 'volume' | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:575–575 | M3U Playlist Module Architecture: — Keyboard: ArrowUp/Down adjusts volume by 5%, M toggles mute | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:576–576 | M3U Playlist Module Architecture: — Component: libs/ui/playback/src/lib/audio-player/audio-player.component.ts | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:578–591 | M3U Playlist Module Architecture: — M3U Movie Recognition (VOD detail instead of the EPG zone): an M3U entry recognized as a movie FILE swaps… | [Movie Recognition (VOD Detail View)](../architecture/m3u-playlist-module.md#movie-recognition-vod-detail-view) | Existing contract |
| CLAUDE.md:593–601 | M3U Playlist Module Architecture: — M3U playback mode is independent of this metadata gate: isLikelyM3uVod recognizes video-file extensions… | [M3U Playback Mode](../architecture/m3u-playlist-module.md#m3u-playback-mode) | Existing contract |
| CLAUDE.md:603–603 | M3U Playlist Module Architecture: — Channel List Component Structure (parent coordinator pattern): | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:605–613 | M3U Playlist Module Architecture: — libs/ui/components/src/lib/channel-list-container/ ├── channel-list-container.component.ts # Parent -… | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:615–615 | M3U Playlist Module Architecture: — Key patterns: | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:617–617 | M3U Playlist Module Architecture: — EnrichedChannel: Pre-computed EPG data attached to channels for performance | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:618–618 | M3U Playlist Module Architecture: — Parent coordinator: Manages shared signals (channelEpgMap, progressTick, favoriteIds) | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:619–619 | M3U Playlist Module Architecture: — Virtual scrolling: CDK virtual scroll for 90,000+ channel lists | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:620–620 | M3U Playlist Module Architecture: — Infinite scroll: IntersectionObserver in groups view loads 50 items at a time | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:621–621 | M3U Playlist Module Architecture: — Global progress tick: Single 30s interval instead of per-item intervals | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:623–623 | M3U Playlist Module Architecture: — State management via NgRx (libs/m3u-state/): | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:625–625 | M3U Playlist Module Architecture: — PlaylistActions: loadPlaylists, addPlaylist, removePlaylist, parsePlaylist | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:626–626 | M3U Playlist Module Architecture: — ChannelActions: setChannels, setActiveChannel, setAdjacentChannelAsActive | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:627–627 | M3U Playlist Module Architecture: — EpgActions: setActiveEpgProgram, setCurrentEpgProgram, setEpgAvailableFlag | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:628–628 | M3U Playlist Module Architecture: — FavoritesActions: updateFavorites, setFavorites, hydrateFavorites | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:630–630 | M3U Playlist Module Architecture: — See docs/architecture/m3u-playlist-module.md for complete documentation. | [Channel List Container](../architecture/m3u-playlist-module.md#channel-list-container) | Existing contract |
| CLAUDE.md:632–632 | M3U Playlist Module Architecture: — Routing: Lazy-loaded routes in apps/web/src/app/app.routes.ts. All user-facing routes are nested under the… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:634–634 | M3U Playlist Module Architecture: — Dashboard: /workspace/dashboard; sources overview: /workspace/sources | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:635–635 | M3U Playlist Module Architecture: — M3U player: /workspace/playlists/:id (children: favorites, recent, :view) — routes in… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:636–636 | M3U Playlist Module Architecture: — Xtream Codes: /workspace/xtreams/:id (children: live, vod, series, search, actor/:personId, discover,… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:637–637 | M3U Playlist Module Architecture: — Stalker portal: /workspace/stalker/:id (children: itv, vod, radio, series, favorites, recent, search,… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:638–638 | M3U Playlist Module Architecture: — Global collections: /workspace/global-favorites, /workspace/global-recent | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:639–639 | M3U Playlist Module Architecture: — Global search: /workspace/search (Electron-only; a guard redirects the PWA to /workspace/sources) | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:640–644 | M3U Playlist Module Architecture: — Downloads: /workspace/downloads with focused /workspace/downloads/:downloadId; source-scoped equivalents… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:645–645 | M3U Playlist Module Architecture: — Settings: /workspace/settings/:section — one page per section (general, playback, epg, dashboard,… | [Route Contract](../architecture/workspace-shell.md#route-contract) | Existing contract |
| CLAUDE.md:647–647 | M3U Playlist Module Architecture: — Service Architecture (Factory Pattern): | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:649–649 | M3U Playlist Module Architecture: — Abstract DataService class in libs/services/src/lib/data.service.ts defines the contract | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:650–652 | M3U Playlist Module Architecture: — Two environment-specific implementations: - ElectronService… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:653–659 | M3U Playlist Module Architecture: — Factory function DataFactory() in apps/web/src/app/app.config.ts determines which implementation to… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:663–666 | Data Storage (Environment-Specific): — Electron: SQLite database via Drizzle ORM (better-sqlite3 driver) - Location:… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:667–670 | Data Storage (Environment-Specific): — PWA (Web): IndexedDB via ngx-indexed-db - Browser-based NoSQL storage - Same schema structure but… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:674–678 | TypeScript File Size Rule: — Keep production TypeScript files under 300 lines. Hard maximum is 350–400 lines, and CI enforces the 400.… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:680–680 | TypeScript File Size Rule: — When creating new files, design them to stay within this limit from the start. | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:681–681 | TypeScript File Size Rule: — When adding a feature to an existing file that would push it past 350 lines, refactor first: extract… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:682–682 | TypeScript File Size Rule: — When you notice a file already exceeds 350 lines, proactively suggest a refactoring (or perform it if the… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:684–684 | TypeScript File Size Rule: — Typical split strategies: | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:686–686 | TypeScript File Size Rule: — Angular components: extract child components, move logic to a dedicated service or store feature | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:687–687 | TypeScript File Size Rule: — Signal store features: split into smaller with feature functions in separate files | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:688–688 | TypeScript File Size Rule: — Services: split by responsibility (e.g. separate API, transformation, and state concerns) | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:689–689 | TypeScript File Size Rule: — Utility files: group by domain and export from a barrel index.ts | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:691–691 | TypeScript File Size Rule: — This rule exists to keep the codebase navigable and reviewable. A 150-line file is always preferable to a… | [TypeScript File Size](../architecture/nx-workspace-boundaries.md#typescript-file-size) | Existing contract |
| CLAUDE.md:697–697 | Angular Coding Standards: — This project uses modern Angular signal-based APIs and patterns. ALWAYS use the following: | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:699–699 | Angular Coding Standards: — Component Queries: Use viewChild(), viewChildren(), contentChild(), contentChildren() instead of… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:701–709 | Angular Coding Standards: — typescript // ✅ Correct - Signal-based readonly menu = viewChild.required&lt;MatMenu&gt;('menuRef'); readonly… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:711–711 | Angular Coding Standards: — Important: When using signals in templates with properties that expect non-signal values, unwrap the… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:713–719 | Angular Coding Standards: — html &lt;!-- ✅ Correct - Unwrap the signal --&gt; &lt;button [matMenuTriggerFor]="menu()"&gt;Open Menu&lt;/button&gt; &lt;!-- ❌… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:721–721 | Angular Coding Standards: — Component Inputs/Outputs: Use input() and output() functions instead of @Input() and @Output() decorators | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:723–733 | Angular Coding Standards: — typescript // ✅ Correct - Signal-based readonly title = input.required&lt;string&gt;(); readonly size =… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:735–735 | Angular Coding Standards: — Reactive State: Use signal primitives for reactive state management | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:737–747 | Angular Coding Standards: — typescript // ✅ Use signal(), computed(), effect(), linkedSignal() readonly count = signal(0); readonly… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:749–749 | Angular Coding Standards: — Host Bindings: Use @HostBinding() and @HostListener() decorators (these don't have signal equivalents yet) | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:751–754 | Angular Coding Standards: — typescript @HostBinding('class.active') get isActive() { return this.active(); } @HostListener('click')… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:756–756 | Angular Coding Standards: — Control Flow: Use @if, @for, @switch instead of ngIf, ngFor, ngSwitch | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:758–771 | Angular Coding Standards: — typescript // ✅ Correct - Modern syntax @if (isLoggedIn()) { &lt;p&gt;Welcome!&lt;/p&gt; } @for (item of items();… | [Angular conventions](../development/agent-workflow.md#angular-conventions) | Moved / consolidated |
| CLAUDE.md:775–775 | Backend Architecture (Electron) — Main Entry: apps/electron-backend/src/main.ts | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:777–777 | Backend Architecture (Electron) — Bootstraps Electron app and initializes database | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:778–778 | Backend Architecture (Electron) — Registers event handlers for IPC communication | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:779–779 | Backend Architecture (Electron) — Creates the main window per the startup window mode (app/app.ts initMainWindow, resolver in… | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:780–780 | Backend Architecture (Electron) — Persists the app zoom level (issue #1109): the preload restores it with webFrame.setZoomLevel (temporary,… | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:781–781 | Backend Architecture (Electron) — Recovers a renderer reload on an in-app route: the packaged renderer is index.html over file:// with path… | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:782–782 | Backend Architecture (Electron) — Holds a single-instance lock (app/services/single-instance.ts), requested after the userData override so… | [Window Chrome And Custom Title Bar](../architecture/workspace-shell.md#window-chrome-and-custom-title-bar) | Existing contract |
| CLAUDE.md:786–786 | Database: — ORM: Drizzle ORM with better-sqlite3 (local SQLite file) | [Exports](../../libs/shared/database/README.md#exports) | Existing contract |
| CLAUDE.md:787–787 | Database: — Location: ~/.iptvnator/databases/iptvnator.db (avoids spaces in path) | [Exports](../../libs/shared/database/README.md#exports) | Existing contract |
| CLAUDE.md:788–801 | Database: — Schema (libs/shared/database/src/lib/schema.ts — canonical;… | [Exports](../../libs/shared/database/README.md#exports) | Existing contract |
| CLAUDE.md:802–805 | Database: — Connection: libs/shared/database/src/lib/connection.ts - createTables() auto-creates tables on init… | [Exports](../../libs/shared/database/README.md#exports) | Existing contract |
| CLAUDE.md:809–812 | IPC Communication: — Preload script: apps/electron-backend/src/app/api/main.preload.ts - Exposes window.electron API via… | [Preload API Type Contract](../architecture/electron-security.md#preload-api-type-contract) | Existing contract |
| CLAUDE.md:813–823 | IPC Communication: — Event handlers: apps/electron-backend/src/app/events/ - database.events.ts - Database CRUD operations -… | [Preload API Type Contract](../architecture/electron-security.md#preload-api-type-contract) | Existing contract |
| CLAUDE.md:825–825 | IPC Communication: — Workers (apps/electron-backend/src/app/workers/): | [Current Ownership](../architecture/sqlite-db-worker.md#current-ownership) | Existing contract |
| CLAUDE.md:827–827 | IPC Communication: — EPG parsing: epg-parser.worker.ts; main-process worker lifecycle is coordinated from… | [Current Ownership](../architecture/sqlite-db-worker.md#current-ownership) | Existing contract |
| CLAUDE.md:828–828 | IPC Communication: — Non-EPG SQLite work: database.worker.ts (see docs/architecture/sqlite-db-worker.md). Catalog deletes and… | [Current Ownership](../architecture/sqlite-db-worker.md#current-ownership) | Existing contract |
| CLAUDE.md:829–829 | IPC Communication: — Playlist refresh: playlist-refresh.worker.ts; explicit cancellation is main-process-owned and terminates… | [Current Ownership](../architecture/sqlite-db-worker.md#current-ownership) | Existing contract |
| CLAUDE.md:833–837 | Xtream Category Management — The Electron Live TV, Movies, and Series category dialog applies Select/Deselect to search results while a… | [Behavior Notes](../architecture/category-management.md#behavior-notes) | Existing contract |
| CLAUDE.md:843–851 | Xtream Connection Test — Add/Edit source Test HTTPS and HTTP discloses plaintext credential use before the click and can replace an… | [Connection Input](../architecture/xtream-portal-compatibility.md#connection-input) | Existing contract |
| CLAUDE.md:855–863 | Xtream Live Auto Format — The routed Xtream live host supplies liveAutoTsUrl only for Auto with explicit HLS+TS account evidence,… | [Initial Auto HLS failure](../architecture/xtream-portal-compatibility.md#initial-auto-hls-failure) | Existing contract |
| CLAUDE.md:867–883 | Xtream Catch-Up Server Timezone — The {Y-m-d:H-M} segment of a timeshift URL is read by the panel in ITS timezone (server_info.timezone),… | [Catch-Up Playback URLs](../architecture/xtream-portal-compatibility.md#catch-up-playback-urls) | Existing contract |
| CLAUDE.md:887–889 | M3U URL User-Agent — PlaylistsService.getPlaylist() joins the per-playlist mutation queue so a route opened during refresh… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:890–894 | M3U URL User-Agent — The URL import form accepts an optional User-Agent and stores it as Playlist.userAgent. Electron sends it… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:895–897 | M3U URL User-Agent — Reuse the existing source editor and channel-over-playlist playback header precedence. Contract:… | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:901–901 | Playlist Support: — M3U/M3U8 files (local or URL) | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:902–902 | Playlist Support: — Xtream Codes API (username, password, serverUrl) | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:903–903 | Playlist Support: — Stalker portal (macAddress, url) | [User-Agent for URL sources](../architecture/m3u-playlist-module.md#user-agent-for-url-sources) | Existing contract |
| CLAUDE.md:905–920 | Playlist Support: — Stalker playback links: create_link runs only when the catalog row sets use_http_tmp_link or… | [Playback Link Resolution](../architecture/stalker-portal.md#playback-link-resolution) | Corrected; see decisions |
| CLAUDE.md:922–941 | Playlist Support: — Opening a playlist from the OS (Electron only): a .m3u/.m3u8 path passed on the command line, opened… | [Opening playlists from the operating system](../architecture/m3u-playlist-module.md#opening-playlists-from-the-operating-system) | Added / consolidated |
| CLAUDE.md:943–959 | Playlist Support: — The OS-level registration that makes those paths reachable is fileAssociations in electron-builder.json —… | [Opening playlists from the operating system](../architecture/m3u-playlist-module.md#opening-playlists-from-the-operating-system) | Added / consolidated |
| CLAUDE.md:963–968 | Video Players: — The Embedded MPV native-view dock follows app theme tokens as a solid app surface, including Material… | [Player And EPG Theme Boundaries](../architecture/iptvnator-ui-guidelines.md#player-and-epg-theme-boundaries) | Existing contract |
| CLAUDE.md:970–977 | Video Players: — Built-in web players: HTML5+hls.js, Video.js, and ArtPlayer. The HTML5 player and ArtPlayer pick their… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:978–986 | Video Players: — mpegts.js 1.8.1 errors from all three built-in players cross one version-locked structured evidence… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:987–1119 | Video Players: — Browser playback diagnostics and recovery policy live in libs/playback/util and are exported by… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1120–1125 | Video Players: — M3U Favorites and Recently Viewed resolve Channel.drm into ResolvedPortalPlayback.drm through… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1126–1150 | Video Players: — DASH + ClearKey (M3U module): .mpd channels play through a lazily loaded Shaka Player source engine inside… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1151–1165 | Video Players: — Stream info popover: an info button in the top-right corner of the shared controls overlay shows live… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1166–1166 | Video Players: — External players: MPV, VLC (via IPC to Electron backend) | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1167–1181 | Video Players: — Display sleep during playback: PlaybackKeepAwakeService… | [Codec And Container Diagnostics](../architecture/embedded-inline-playback.md#codec-and-container-diagnostics) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Two per-session knobs are captured at session creation from the main-process settings mirror… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Contract: docs/architecture/embedded-mpv-native.md ("Session Options", "Network Auto-Reconnect"). macOS… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Windows uses in-process libmpv with --wid against an app-owned child HWND; | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Linux spawns an out-of-process mpv --wid=&lt;x11-window&gt; controlled over a JSON IPC socket (X11/XWayland… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Renderer bounds are CSS pixels; the service converts them to native units in the main process… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Arrow-key and ±10 s button steps go through the relative seekEmbeddedMpvBy IPC (mpv seek &lt;delta&gt;… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1182–1182 | Video Players: — Service: apps/electron-backend/src/app/services/embedded-mpv-native.service.ts; full architecture:… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1183–1314 | Video Players: — Embedded MPV frame-copy engine (experimental, macOS Apple Silicon + Linux x64 + Windows; enabled via… | [How It Is Embedded](../architecture/embedded-mpv-native.md#how-it-is-embedded) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Shared player-controls layer: libs/ui/playback/src/lib/player-controls/ exports the engine-neutral… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Its subtitle menu carries capability-gated advanced subtitle support (#1408): external subtitle file… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — HTML5/ArtPlayer implement it through the neutral source bridge (.srt/.vtt via a DOM file picker with… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Embedded MPV frame-copy implements it through new helper protocol commands… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Video.js shared mode, vendor-chrome paths, native-view, and the Linux out-of-process path advertise no… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Contract details: docs/architecture/player-controls-contract.md ("Advanced subtitle support"). | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Shared controls include a per-session quality menu (Auto + “1080p”-style levels via setQualityLevel; | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — AUTO_QUALITY_LEVEL_ID restores ABR): the capability derives from the manifest — advertised only when the… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — In fullscreen, app-player-controls shows a pointer-transparent media-title overlay at the top while… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Persisted Settings.webPlayerSharedControls is default-ON (absent stored values coerce with !== false in… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The shared surface has explicit touch semantics (ControlsSurface.wasTouchInteraction): viewport taps… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Only keyboard-originated focus pins the bar open: Chromium also focuses a clicked &lt;button&gt;, so… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — A completed pointer click then releases the focus it left on the control (onBarClick →… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Keyboard activation (empty pointerType) keeps focus, only buttons and range sliders are released, Chromium… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Deliberately dropped vs. vendor chrome (opt-out retains them): Video.js spatial navigation, ArtPlayer… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — WebPlayerViewComponent snapshots the preference into the immutable token for each new player host. | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The parent /workspace route awaits the initial SettingsStore load, including cold-start direct links,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Saving applies to the next host without an application restart; an existing session never changes controls… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Embedded MPV ignores the web-player preference: frame-copy always uses shared DOM controls through… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The Embedded MPV host selects exactly one controls UI for its reported engine. | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — showControls=false detaches the shared surface, modal overlays gate frame-copy playback shortcuts,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Same-session IPC replies yield to a broadcast snapshot received while the command was pending, so a… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The built-in HTML5/hls.js player is the second guarded consumer: HtmlVideoPlayerComponent provides a… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — HtmlVideoElementSession owns native video-event lifecycle, persisted volume, and start-time/time/ended… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Video.js is the third guarded consumer: VjsPlayerComponent provides a component-scoped… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Reset-driven raw MPEG-TS changes pause first, coalesce to the latest desired source, preserve actual… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — In shared-controls mode, Video.js native controls, click/double-click/hotkey actions, and spatial… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — ArtPlayer is the fourth guarded consumer: ArtPlayerComponent provides a component-scoped… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — ArtPlayerSourceSession owns HLS/DASH(Shaka)/MPEG-TS/native sources, the neutral web-video bridge, exact… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Shared ArtPlayer mode uses authoritative live/VOD metadata, HLS/Shaka/native tracks and caption… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Vendor chrome/hotkeys are disabled, and a transparent capture layer gives shared controls exclusive click… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — WebPlayerViewComponent.resolvedIsLive supplies authoritative metadata; visible playback diagnostics… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The view also renders app-fullscreen-channel-panel beside the engine, staged on fullscreenSurface and… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Providers: M3U VideoPlayerComponent (app-m3u-fullscreen-channel-list, a local icon-only… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Xtream's two PortalChannelsListComponent instances relay favorite toggles through… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — CDK overlays follow the fullscreen element via FullscreenOverlayContainer in app.config.ts. | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Series playback gets the same panel as an episode list: PortalInlinePlayerComponent — the component both… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Movies never get it (contentType !== 'episode' → null), external MPV/VLC never mount the inline player,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — M3U also zaps with PageUp/PageDown, yielding to already-handled events and menu/dialog or scrollable-list… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — While the live web-player host owns fullscreen (itself, or through the nested surface a legacy player… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Contract section "Fullscreen channel panel" in docs/architecture/player-controls-contract.md. | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — On the preference-off path, all three web players retain their existing controls, source behavior, and… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The legacy Video.js chrome also releases the focus a pointer interaction leaves on a control… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — It is driven mainly by focusin, not the click, because choosing a menu item moves focus to the menu button… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The release is scoped to .vjs-control-bar so the caption-settings dialog (a modal sibling of the bar)… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Settings.showCaptions is deliberately outside this rollout gate: it is engine state, so the preference-off… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — The two modes differ in how long it is enforced: shared controls are authoritative for the session (user… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Mode selection is the optional playbackStarted probe the legacy owners pass to all three helpers (HLS,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — WebPlayerViewComponent reads it from SettingsStore instead of a host input so every host (M3U,… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1315–1315 | Video Players: — Contract: docs/architecture/player-controls-contract.md. | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1316–1325 | Video Players: — Shared web picture-in-picture stays inside that default-on rollout. PlayerController exposes capability… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1326–1344 | Video Players: — WebVideoControlsAdapter supplies its current video and binding generation to… | [Landed architecture](../architecture/player-controls-contract.md#landed-architecture) | Existing contract |
| CLAUDE.md:1348–1376 | Download Manager: — Fresh Xtream movie and series-episode downloads propagate the playlist's User-Agent, Referer, and Origin,… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1377–1380 | Download Manager: — The desktop-only manager shares one global download store across the global, Xtream-scoped, and… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1381–1420 | Download Manager: — Series details route individual and selected-season episode downloads through the provider-neutral… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1421–1438 | Download Manager: — Episode ownership uses normalized episode.id as the canonical xtreamId for both providers; Stalker… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1439–1444 | Download Manager: — Ready cards (movies, grouped series, and standalone episodes) open a focused local detail; local file… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1445–1449 | Download Manager: — Downloads capture a versioned metadata snapshot from the rendered Xtream or Stalker movie/episode detail… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1450–1459 | Download Manager: — View in portal resolves a concrete Xtream category/item route. Stalker accepts a recently-viewed shape… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1460–1462 | Download Manager: — Download rows and local files survive source deletion. The global offline library remains visible with no… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1463–1465 | Download Manager: — If a finalized file disappears while a focused detail is open, the authoritative download list refreshes… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1466–1497 | Download Manager: — Live-TV recordings (Embedded MPV stream-record) are tracked beside downloads in their own recordings table… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1498–1499 | Download Manager: — Canonical contract: docs/architecture/download-manager.md; provider handoff:… | [Queuing, persistence, and UX notes](../architecture/download-manager.md#queuing-persistence-and-ux-notes) | Existing contract |
| CLAUDE.md:1501–1501 | Download Manager: — Collection Detail Portal Handoff (View in portal for inline details): | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1503–1507 | Download Manager: — Details opened outside portal category context — /workspace/global-favorites, /workspace/global-recent… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1508–1515 | Download Manager: — Visibility is DI-gated, never URL-sniffed: app-view-in-portal-action… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1516–1521 | Download Manager: — Targets come from getUnifiedCollectionDetailNavigation()… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1522–1558 | Download Manager: — Stalker section resolution mirrors resolveStalkerCollectionDetailMode()… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1559–1561 | Download Manager: — Unlike the download handoff this bridge does NOT pass detailPresentation: 'provider-only' — the item… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1562–1562 | Download Manager: — Contract: docs/architecture/portal-detail-navigation.md. | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1566–1566 | VOD/Series Detail Pages (two-state layout): — Xtream and Stalker detail pages use the shared PortalDetailShellComponent… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1567–1567 | VOD/Series Detail Pages (two-state layout): — The inline player (PortalInlinePlayerComponent) renders a full-width theater stage… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1568–1568 | VOD/Series Detail Pages (two-state layout): — For inline series playback on wide windows the stage instead docks the player left and shows an "Up Next"… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1569–1569 | VOD/Series Detail Pages (two-state layout): — Watch state derives from inlinePlayback() !== null only; external MPV/VLC playback keeps the browse… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1570–1570 | VOD/Series Detail Pages (two-state layout): — Xtream VOD treats metadata presentation and playability as separate contracts. Empty or sparse… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1571–1571 | VOD/Series Detail Pages (two-state layout): — A successful external MPV/VLC episode launch immediately persists the selected episode as the latest… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1572–1572 | VOD/Series Detail Pages (two-state layout): — Stalker preserves this contract for regular /series, embedded VOD series[], and lazy Ministra VOD… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1573–1573 | VOD/Series Detail Pages (two-state layout): — Hosts pass hero chips/meta/actions as appDetailTags/appDetailMeta/appDetailActions templates; the shell… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1574–1574 | VOD/Series Detail Pages (two-state layout): — Seasons are tabs (SeasonTabsComponent, dropdown beyond 6 seasons; the dropdown's menu rows and closed… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1575–1575 | VOD/Series Detail Pages (two-state layout): — The season header hosts a bulk watched toggle next to "Download season" (both portals): marking writes… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1576–1576 | VOD/Series Detail Pages (two-state layout): — Movies get the same manual toggle in the detail action row (Xtream: icon square after Favorite,… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1577–1577 | VOD/Series Detail Pages (two-state layout): — The dashboard hero CTA and the Continue Watching cards' explicit "Resume episode" ⋮ action for an Xtream… | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1578–1578 | VOD/Series Detail Pages (two-state layout): — See docs/architecture/embedded-inline-playback.md ("Two-State Detail Layout") | [Two-State Detail Layout (Browse ↔ Watch)](../architecture/embedded-inline-playback.md#two-state-detail-layout-browse--watch) | Existing contract |
| CLAUDE.md:1580–1580 | VOD/Series Detail Pages (two-state layout): — VOD Multi-Source (alternative sources for a movie): | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Finds the same movie in the user's other imported playlists and adds a "Sources N" chip to the Xtream VOD… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — The chip opens a 660px anchored CDK-overlay popover (libs/ui/components/src/lib/vod-sources/; not MatMenu,… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — It opens ABOVE the chip (right edges aligned, pressed state on the chip while open), height-capped by the… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — A row's language is vodSourceLanguage (libs/shared/interfaces/src/lib/vod-source-language.util.ts): the… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Unicode lookalikes, bracketed, or ALL-CAPS spaced-dash form; | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Latin/Cyrillic 2–4 letters + MULTI; only the legacy pipe form is permissive — bracket/dash matches must… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Both forms are parsed guesses: browse filter and chips only, never ranking/failover/dub-warning inputs. | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Recognition alone is not enough — normalizeTitleKeys must STRIP the same tag or the copy is never… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — It goes no further on purpose: a wrong guess costs a filter option, a wrong strip corrupts identity, and… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — The one shape that cannot decide itself is a strip leaving NO real word behind — decided by running the… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Every vocabulary entry is one the catalog proves prefixes hundreds of ordinary titles — never one that… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Verify such widenings against the real catalog before shipping them, over movies AND series: a movie-only… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Checks run through a 4-slot queue and settled verdicts are cached 10 min per movie+source… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — Both chips are handed the same matchKind and vodAutoFailover and both write the setting back. | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — The details-page chip badge counts TOTAL copies across all playlists (the in-player chip still counts… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1582–1582 | VOD/Series Detail Pages (two-state layout): — The action row's Favorites and Download buttons are icon-only 64px squares: filled red heart when… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1583–1583 | VOD/Series Detail Pages (two-state layout): — Scope v1 is Xtream ↔ Xtream, movies only, Electron only. Stalker never reaches the content table and M3U… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1584–1584 | VOD/Series Detail Pages (two-state layout): — Metadata provenance is the core contract. Every field is {value, provenance} where api/probe are facts… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — Discovery (DB_FIND_TITLE_SOURCES, trigram FTS over content_title_fts) is lazy and returns only what the… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — A source that is never read looks exactly like one that does not exist, so: the current playlist is… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — The year gate covers BOTH match tiers: normalizeTitleKeys strips bracketed segments, so "Dune (1984)"… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — A non-ASCII token cannot be folded by LOWER() (ASCII-only) but CAN be by a GLOB character class (UTF-8… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — The movie's own year comes from releaseTagYear (bracketed or trailing only), never extractYear: a year… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — One row inside the excluded playlist is kept when the caller names it (keepContentId), because a pin can… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1585–1585 | VOD/Series Detail Pages (two-state layout): — Resolution is deferred to click/pin/check because content stores no container_extension and… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1586–1586 | VOD/Series Detail Pages (two-state layout): — Switching = one inlinePlayback.set({...next, startTime}), never null-then-set, so the player and engine… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1587–1587 | VOD/Series Detail Pages (two-state layout): — Pins are keyed portal-agnostically (tmdb:{id} else title:{base}:{year} else the yearless title:{base}:,… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1588–1588 | VOD/Series Detail Pages (two-state layout): — Claims in the present tense (the "Playing from" caption and the source row's Playing badge) are gated on… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1589–1589 | VOD/Series Detail Pages (two-state layout): — Pins are included in playlist backup as the optional sourcePins collection, carried under the playlist… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1590–1590 | VOD/Series Detail Pages (two-state layout): — Auto-failover is Settings.vodAutoFailover, opt-in and off by default, web engines only — the toggle is… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1591–1591 | VOD/Series Detail Pages (two-state layout): — HEAD probe reuses the main-process handler extracted to… | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1592–1592 | VOD/Series Detail Pages (two-state layout): — See docs/architecture/vod-multi-source.md | [VOD Multi-Source](../architecture/vod-multi-source.md#vod-multi-source) | Existing contract |
| CLAUDE.md:1596–1596 | Radio Player: — Dedicated audio player for channels with radio="true" M3U attribute | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1597–1597 | Radio Player: — Cinematic layout: blurred station logo as backdrop, floating artwork card, transport controls | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1598–1598 | Radio Player: — Always uses the built-in inline player — external player settings (MPV/VLC) are ignored for radio | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1599–1599 | Radio Player: — EPG panel is hidden for radio channels (radio streams have no EPG data) | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1600–1600 | Radio Player: — Volume synced with video player via shared localStorage key 'volume' | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1601–1601 | Radio Player: — Keyboard shortcuts: ArrowUp/ArrowDown (volume), M (mute) | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1602–1602 | Radio Player: — Component: libs/ui/playback/src/lib/audio-player/audio-player.component.ts | [Radio audio player](../architecture/player-controls-contract.md#radio-audio-player) | Added / consolidated |
| CLAUDE.md:1606–1606 | EPG (Electronic Program Guide): — XMLTV format support, from http(s) links or local files (Electron only): a file: URL, an absolute POSIX… | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1607–1607 | EPG (Electronic Program Guide): — Background parsing in worker thread; HTTP/file gzip compatibility follows… | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1608–1608 | EPG (Electronic Program Guide): — Stored in database for quick lookup | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1609–1609 | EPG (Electronic Program Guide): — Global display-time offset (Settings.epgOffsetMinutes, Settings → EPG, ±720 min, Electron only):… | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1610–1610 | EPG (Electronic Program Guide): — Programme guide (Electron, M3U): app-epg-guide in libs/ui/epg fed by the host-provided EPG_GUIDE_SOURCE;… | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1611–1611 | EPG (Electronic Program Guide): — Manual EPG mapping (Electron only): right-click a channel in any list (M3U views, Xtream portal list,… | [EPG Integration](../architecture/m3u-playlist-module.md#epg-integration) | Existing contract |
| CLAUDE.md:1613–1613 | EPG (Electronic Program Guide): — TMDB Metadata Enrichment (opt-in): | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1615–1615 | EPG (Electronic Program Guide): — Enriches Xtream and Stalker VOD/series detail views with TMDB data (plot, cast with avatar chips,… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1616–1616 | EPG (Electronic Program Guide): — The M3U player consumes it too: entries recognized as movie files open in the VOD detail shell fed purely… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1617–1617 | EPG (Electronic Program Guide): — "Similar" rail in ALL detail views: TMDB recommendations matched against the provider catalog by… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1618–1618 | EPG (Electronic Program Guide): — Season/episode enrichment: opening a season lazily fetches /tv/{id}/season/{n} and overlays real episode… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Dashboard: opt-in "Trending this week" rail (weekly TMDB trending matched against imported Xtream… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Electron-only, dashboardRails.tmdbTrending toggle), a "Because you watched" recommendations rail… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — TMDB has no account-free "for you" endpoint, so DashboardRecommendationsService seeds per-title… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — The hero lookup must carry the same identity the detail view used, not just the display title —… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — The lookup key is the WHOLE attempt sequence, since two rows can share title/year/id yet differ in whether… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Stalker items never reach the content table, so their backdrop rides in the stored entry… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Xtream rows carry the same identity on the content row: the detail views back-fill… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Writes are per-column and never overwrite (enrichment supplies the pieces at different times, so a… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — release_year is the year the PROVIDER stated, never one read out of the title (readers still apply that… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1619–1619 | EPG (Electronic Program Guide): — Both sides validate through normalizeContentMetadataPatch (libs/shared/interfaces), so legacy rows,… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1620–1620 | EPG (Electronic Program Guide): — Series detail views show a TMDB production-status chip (tmdb_status, e.g. Ended / Returning) — TMDB sends… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1621–1621 | EPG (Electronic Program Guide): — Actor pages: cast avatar chips are clickable (TMDB person id) and open actor/:personId inside the current… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1622–1622 | EPG (Electronic Program Guide): — Discover pages (clickable metadata chips, issue #1449): year, genre, and country chips on all four detail… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1623–1623 | EPG (Electronic Program Guide): — Actor page "All portals" scope (Electron only): batched DB_MATCH_TITLES worker op (trigram FTS over all… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1624–1624 | EPG (Electronic Program Guide): — All DB_MATCH_TITLES consumers (Trending rail, "Because you watched" recommendations rail, cross-portal… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1625–1625 | EPG (Electronic Program Guide): — Opt-in via Settings &gt; Metadata (TMDB) (sends titles to TMDB); the section also has a "check key" button… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1626–1626 | EPG (Electronic Program Guide): — Match confidence: a provider tmdb_id is a strong hint, not gospel — its payload is weighed against the… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1627–1627 | EPG (Electronic Program Guide): — Detail views render provider data immediately; enrichment patches the selection asynchronously… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1628–1628 | EPG (Electronic Program Guide): — Cached in SQLite tmdb_metadata (Electron, via DB worker ops DB_GET/SET_TMDB_METADATA, plus… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1629–1629 | EPG (Electronic Program Guide): — Service layer: libs/services/src/lib/tmdb/; store glue:… | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1630–1630 | EPG (Electronic Program Guide): — TMDB attribution (logo + disclaimer) is required and shown in the settings TMDB section and About | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1631–1631 | EPG (Electronic Program Guide): — See docs/architecture/tmdb-metadata-enrichment.md | [TMDB Metadata Enrichment](../architecture/tmdb-metadata-enrichment.md#tmdb-metadata-enrichment) | Existing contract |
| CLAUDE.md:1635–1635 | Portal Account Info: — Both portal types expose an account-info dialog through the same entry points: header playlist switcher… | [Account Info Dialog](../architecture/stalker-portal.md#account-info-dialog) | Existing contract |
| CLAUDE.md:1636–1636 | Portal Account Info: — Xtream: AccountInfoComponent (libs/portal/xtream/feature/src/lib/account-info/), queries get_account_info… | [Account Info Dialog](../architecture/stalker-portal.md#account-info-dialog) | Existing contract |
| CLAUDE.md:1637–1637 | Portal Account Info: — Stalker: StalkerAccountInfoComponent (libs/portal/stalker/feature/src/lib/stalker-account-info/),… | [Account Info Dialog](../architecture/stalker-portal.md#account-info-dialog) | Existing contract |
| CLAUDE.md:1638–1638 | Portal Account Info: — Dashboard source cards carry a passive subscription-expiry chip (amber within 7 days, error-toned once… | [Source subscription expiry](../architecture/workspace-dashboard.md#source-subscription-expiry) | Added / consolidated |
| CLAUDE.md:1642–1642 | Stalker Portal Mode and Endpoint Discovery: — Every resolved Edit commit is guarded by the source connection authority captured when Edit began.… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1643–1643 | Stalker Portal Mode and Endpoint Discovery: — Portal mode (full vs. simple) follows OBSERVED behavior, never a URL substring. The single predicate is… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1644–1644 | Stalker Portal Mode and Endpoint Discovery: — Import requires an explicit HTTP(S) scheme but accepts a bare host, /c, or a concrete .php address. It… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — The playlist-info Edit dialog loads the complete persisted Stalker row before enabling the form, because… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — A metadata-only Save omits connection/mode fields from its queued update, so the stored connection stays… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — A persisted portalUrl keeps the row on the Stalker save path even if legacy Xtream fields remain. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Changing URL, MAC, credentials, serial, device IDs or signatures blocks duplicate saves, disables dialog… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Before discovery, PWA acquires a shared playlist-authority barrier plus an exclusive origin-wide… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Add/delete, backup restore, and bulk replacement take the same row lock, while Delete All takes the… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — A concurrent Edit or stale dialog fails before remote discovery; a replacement waits for the current owner. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Same-tab Save first publishes its local authentication owner, drains an existing lazy repair through… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — PWA fails closed if Web Locks are unavailable, while Electron relies on its single-instance local owner. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — The reservation blocks every new authentication (including fingerprint-equivalent URL edits) and repair,… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — If discovery returns after its bounded drain while an abandoned authentication is still on the wire, that… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Once Save starts, navigation or dialog destruction does not discard a later successful result: get_profile… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — That late commit uses transformPlaylistMeta() inside the per-playlist write queue to merge only… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Success uses one awaited write to atomically replace endpoint, mode, normalized identity and session… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — This preserves playback headers and other metadata absent from the form. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — Runtime configuration authority covers the observed full/simple mode as well as the session fingerprint,… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — A changed authority may rebase only when the persisted row proves that it owns the same playlist ID,… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1645–1645 | Stalker Portal Mode and Endpoint Discovery: — The transient PlaylistMetaUpdate.stalkerSessionPatch preserves on absence, clears on null, and fully… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — executeStalkerRequest() (stores/utils/stalker-request.utils.ts) is the choke point for catalog, content… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Four callers are deliberately outside it because they run below or before the thing it routes on —… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — They are exempt from the routing, not from the repair it hooks, but only fetchViaProfile() wires… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Anything new that is not auth or discovery belongs on executeStalkerRequest(). | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Existing playlists are repaired LAZILY (StalkerPortalRepairService) — only after a request fails with a… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Before an unrecorded repair reads the persisted source or calls discovery, PWA takes the same… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — This prevents repair in another tab from authenticating alongside Edit or crossing delete/restore. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — The persisted-row preflight still verifies that the caller owns the failing source, so a late pre-Edit… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Its in-session override is bound to source endpoint, mode, device identity, and credentials; an Edit or… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — Each repair installs a session-level authentication fence synchronously, drains the existing token slot… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1646–1646 | Stalker Portal Mode and Endpoint Discovery: — There is deliberately no eager one-shot migration: a portal that works is never re-probed. | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1647–1647 | Stalker Portal Mode and Endpoint Discovery: — Explicit Edit advances the repair generation before installing its resolved session. Lazy repair captures… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1648–1648 | Stalker Portal Mode and Endpoint Discovery: — Both transports build the wire format from the same shared builders in @iptvnator/shared/interfaces —… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1649–1649 | Stalker Portal Mode and Endpoint Discovery: — Simple portals skip the auth lifecycle (no handshake, token or watchdog) but their requests are not… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1650–1650 | Stalker Portal Mode and Endpoint Discovery: — Contract: docs/architecture/stalker-portal.md ("Portal Mode and Endpoint Discovery", "Request Transport… | [Portal Mode and Endpoint Discovery](../architecture/stalker-portal.md#portal-mode-and-endpoint-discovery) | Existing contract |
| CLAUDE.md:1654–1654 | Stalker Session Authentication: — Full portals authenticate through StalkerSessionService… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1655–1655 | Stalker Session Authentication: — get_profile's js.status decodes as: full profile/0 = OK, 1 = refused (device-conflict when the message… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1656–1656 | Stalker Session Authentication: — Refusals throw StalkerPortalError (login-required / login-rejected / device-conflict / blocked /… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1657–1657 | Stalker Session Authentication: — Auth failures are HTTP 200 + plain text (Authorization failed. / Access denied. / Unauthorized request.),… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1658–1658 | Stalker Session Authentication: — The handshake is idempotent, so Playlist.stalkerToken is re-presented and get_profile is skipped when it… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1659–1659 | Stalker Session Authentication: — Watchdog: get_events immediately (init=1), then every watchdog_timeout s (default 120, clamped 30–3600)… | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1660–1660 | Stalker Session Authentication: — Full contract: docs/architecture/stalker-portal.md ("Session Authentication Lifecycle"). | [Session Authentication Lifecycle](../architecture/stalker-portal.md#session-authentication-lifecycle) | Existing contract |
| CLAUDE.md:1664–1664 | Stalker Identity Hardening: — The MAC is canonicalized to 00:1A:79:XX:XX:XX by normalizeStalkerMacAddress (@iptvnator/shared/interfaces)… | [Stalker Identity Policy](../architecture/stalker-portal.md#stalker-identity-policy) | Existing contract |
| CLAUDE.md:1665–1665 | Stalker Identity Hardening: — Format is enforced, the Infomir OUI is advisory only: hasInfomirMacOui drives a hint, never a rejection.… | [Stalker Identity Policy](../architecture/stalker-portal.md#stalker-identity-policy) | Existing contract |
| CLAUDE.md:1666–1666 | Stalker Identity Hardening: — deriveStalkerDeviceIdsFromMac returns the StbEmu / stalker-to-m3u PAIR: SHA256(MAC) for device_id and… | [Stalker Identity Policy](../architecture/stalker-portal.md#stalker-identity-policy) | Existing contract |
| CLAUDE.md:1667–1667 | Stalker Identity Hardening: — get_profile reports one coherent MAG250 via STALKER_STB_PROFILE_PARAMS (ver, stb_type — previously empty… | [Stalker Identity Policy](../architecture/stalker-portal.md#stalker-identity-policy) | Existing contract |
| CLAUDE.md:1668–1668 | Stalker Identity Hardening: — Contract: docs/architecture/stalker-portal.md ("Stalker Identity Policy"). | [Stalker Identity Policy](../architecture/stalker-portal.md#stalker-identity-policy) | Existing contract |
| CLAUDE.md:1672–1672 | Favorites and Recently Viewed: — Per-playlist favorites and global favorites | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1673–1673 | Favorites and Recently Viewed: — Recently viewed tracks watch history | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1674–1690 | Favorites and Recently Viewed: — Live channels in the unified favorites/recent live tab (global collections and a portal's own tabs) carry… | [Summary](../architecture/portal-detail-navigation.md#summary) | Existing contract |
| CLAUDE.md:1694–1694 | Internationalization: — Uses @ngx-translate with 19 language files in apps/web/src/assets/i18n/ | [Features](../../README.md#features) | Existing contract |
| CLAUDE.md:1700–1700 | Environment Detection and Dual-Mode Architecture — The app determines whether it's running in Electron or as a PWA by checking: | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1702–1704 | Environment Detection and Dual-Mode Architecture — typescript window.electron; // truthy in Electron, undefined in browser | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1707–1707 | Why Dual Mode? — IPTVnator supports both Electron (desktop app) and PWA (web browser) to provide flexibility: | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1709–1709 | Why Dual Mode? — Electron: Full-featured desktop experience with local database, external player support (MPV/VLC), and… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1710–1710 | Why Dual Mode? — PWA: Lightweight web version that runs in any browser without installation | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1714–1714 | Environment-Specific Behavior: — app.config.ts - DataFactory() selects DataService implementation based on environment | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1715–1715 | Environment-Specific Behavior: — app.routes.ts - Same /workspace/... route tree in both environments; guards keep Electron-only routes… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1716–1718 | Environment-Specific Behavior: — Storage layer switches automatically: - Electron → SQLite/Drizzle ORM →… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1719–1719 | Environment-Specific Behavior: — External player support (MPV/VLC) only available in Electron | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1720–1720 | Environment-Specific Behavior: — File system operations only available in Electron (uploading playlists from disk) | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1723–1723 | Base Href Configuration: — The app uses different base href values depending on the build target: | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1725–1727 | Base Href Configuration: — Development & PWA: baseHref="/" (from index.html) - Used by: pnpm run serve:frontend, pnpm run… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1728–1730 | Base Href Configuration: — Electron Production: baseHref="./" (overridden in build config) - Used by: pnpm run build:backend, pnpm… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1732–1732 | Base Href Configuration: — Build configurations in apps/web/project.json: | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1734–1734 | Base Href Configuration: — production: Electron build with baseHref="./" | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1735–1735 | Base Href Configuration: — pwa: Web deployment with baseHref="/" | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1736–1736 | Base Href Configuration: — development: Dev mode with baseHref="/" from index.html | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1739–1739 | Factory Pattern Implementation: — The factory pattern ensures a single codebase works in both environments without conditional checks… | [Service factory and build bases](../architecture/pwa-self-hosted.md#service-factory-and-build-bases) | Corrected; see decisions |
| CLAUDE.md:1742–1742 | Build Commit In About: — CI injects the git commit into apps/web/src/environments/build-commit.ts via… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — Master pushes are the nightly channel. | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — The leading nightly-version job computes one &lt;patch&gt;-nightly.&lt;commit date&gt;.&lt;run number&gt; version per run… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — Settings.updateChannel (Settings → About, Electron only, default stable) is mirrored into the main-process… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — AppUpdateService applies the channel to electron-updater before every check (app-update-feed.ts: feed… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — Switching back to stable is forward-only: the nightly stays until a newer stable release exists, because a… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — The About section's status badge names the channel the verdict describes (status.verdictChannel, stamped… | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — setChannel re-checks on its own) — a check against an unsaved channel is deliberately not offered. | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1745–1745 | Nightly Builds And Update Channel: — Contract: docs/architecture/release-pipeline.md ("Nightly channel"). | [Nightly channel](../architecture/release-pipeline.md#nightly-channel) | Existing contract |
| CLAUDE.md:1749–1749 | Testing Strategy — Unit tests: Jest with jest-preset-angular and ng-mocks | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:1750–1750 | Testing Strategy — E2E tests: Playwright testing the web app and Electron app | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:1751–1751 | Testing Strategy — Backend tests use standard Jest | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:1752–1752 | Testing Strategy — Bug fixes should add focused regression coverage unless there is a documented reason not to. | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:1753–1753 | Testing Strategy — Use the impact-based validation policy in Regression Prevention And Test Updates to choose targeted unit… | [Unit And Type Checks](../architecture/validation-map.md#unit-and-type-checks) | Existing contract |
| CLAUDE.md:1757–1757 | Nx Commands — Use nx CLI for better performance: | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1759–1763 | Nx Commands — bash pnpm nx run &lt;project&gt;:&lt;target&gt; # Example: pnpm nx run web:build # Example: pnpm nx run… | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1765–1765 | Nx Commands — To run multiple projects: | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1767–1769 | Nx Commands — bash pnpm nx run-many --target=test --all | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1773–1773 | Electron Build Process — The Electron backend depends on the web app being built first: | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1775–1775 | Electron Build Process — electron-backend:build depends on web:build | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1776–1776 | Electron Build Process — Output goes to dist/apps/electron-backend (backend) and dist/apps/web (frontend) | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1777–1777 | Electron Build Process — Packaging combines both into distributable | [Build and serve commands](../development/agent-workflow.md#build-and-serve-commands) | Moved / consolidated |
| CLAUDE.md:1781–1781 | Database Migrations — Database initialization is owned by libs/shared/database/src/lib/connection.ts. createTables() creates… | [Upgrade Compatibility And Migrations](../../libs/shared/database/README.md#upgrade-compatibility-and-migrations) | Existing contract |
| CLAUDE.md:1787–1790 | IPC Communication: — 1. Define handler in appropriate events file (e.g., database.events.ts) 2. Register with ipcMain.handle()… | [Adding behavior across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Moved / consolidated |
| CLAUDE.md:1794–1797 | Adding New Playlist Source: — 1. Add type to libs/shared/interfaces/src/lib/playlist.interface.ts 2. Create event handler in… | [Adding behavior across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Moved / consolidated |
| CLAUDE.md:1801–1801 | State Management: — Use NgRx for global application state (M3U playlists, libs/m3u-state) | [Adding behavior across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Moved / consolidated |
| CLAUDE.md:1802–1802 | State Management: — Use NgRx Signal Store with signalStoreFeature() composition for portal/feature state (XtreamStore,… | [Adding behavior across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Moved / consolidated |
| CLAUDE.md:1803–1803 | State Management: — Use NgRx signals for reactive data streams | [Adding behavior across layers](../development/agent-workflow.md#adding-behavior-across-layers) | Moved / consolidated |
| CLAUDE.md:1810–1810 | General Guidelines for working with Nx — For navigating/exploring the workspace, invoke the nx-workspace skill first when it is available - it has… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1811–1811 | General Guidelines for working with Nx — When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through nx… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1812–1812 | General Guidelines for working with Nx — Prefix nx commands with the workspace's package manager (e.g., pnpm nx build, npm exec nx test) - avoids… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1813–1813 | General Guidelines for working with Nx — You have access to the Nx MCP server and its tools, use them to help the user | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1814–1814 | General Guidelines for working with Nx — For Nx plugin best practices, check node_modules/@nx/&lt;plugin&gt;/PLUGIN.md. Not all plugins have this file -… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1815–1815 | General Guidelines for working with Nx — NEVER guess CLI flags - always check nx_docs or --help first when unsure | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1819–1819 | Scaffolding & Generators — For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the nx-generate skill… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1823–1823 | When to use nx_docs — USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1824–1824 | When to use nx_docs — DON'T USE for: basic generator syntax (nx g @nx/react:app), standard commands, things you already know | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1825–1825 | When to use nx_docs — The nx-generate skill handles generator discovery internally - don't call nx_docs just to look up… | [General Guidelines for working with Nx](../../AGENTS.md#general-guidelines-for-working-with-nx) | Retained; tools conditional |
| CLAUDE.md:1831–1835 | XMLTV Response Compression — Electron decodes HTTP compression before the gzip file layer. For .gz/gzip metadata plus HTTP gzip, a… | [XMLTV response compression](../architecture/m3u-playlist-module.md#xmltv-response-compression) | Existing contract |
| CLAUDE.md:1839–1857 | XMLTV Source Removal — Saving Settings → EPG reconciles cached XMLTV with committed global URLs and all enabled M3U playlist… | [XMLTV source lifecycle](../architecture/m3u-playlist-module.md#xmltv-source-lifecycle) | Existing contract |
| CLAUDE.md:1861–1870 | Web Backend Provider Redirects — All four provider proxy routes use ValidatedHttpClient: automatic redirects are disabled, the initial URL… | [Web Backend](../architecture/pwa-self-hosted.md#web-backend) | Existing contract |
| CLAUDE.md:1874–1879 | Portal Connectivity Preference — Half-open trial slots follow the complete request lifetime with no elapsed-time expiry. All four… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| CLAUDE.md:1880–1887 | Portal Connectivity Preference — Desktop Settings &gt; General &gt; Portal connections exposes default-on Settings.portalConnectivityGuard. Only… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| CLAUDE.md:1888–1890 | Portal Connectivity Preference — Both account-info dialogs explain guard refusals with localized paused-request copy and Retry now; Stalker… | [Desktop preference and account feedback](../architecture/host-connectivity-guard.md#desktop-preference-and-account-feedback) | Existing contract |
| CLAUDE.md:1894–1920 | Live TV Panel Levels — Portal live layouts (Xtream live, Stalker itv/radio) fold their panels from the outside in, in three… | [Collapsible Live Sidebar](../architecture/iptvnator-ui-guidelines.md#collapsible-live-sidebar) | Existing contract |
| CLAUDE.md:1924–1930 | Live Channel Return — Xtream and Stalker (including radio) capture displayed playback order on explicit selection. Remote… | [Live channel return and playback order](../architecture/remote-control.md#live-channel-return-and-playback-order) | Existing contract |
| CLAUDE.md:1934–1940 | Stalker Live Search — ITV sidebar and fullscreen searches independently filter the complete selected category; only All Items… | [Full ITV Channel List Cache](../architecture/stalker-portal.md#full-itv-channel-list-cache) | Existing contract |
| CLAUDE.md:1944–1959 | Channel and Detail Keyboard Scrolling — Channel scroll owners use ChannelScrollFocusDirective; pointer selection focuses the viewport, native… | [Detail Scroll and Focus](../architecture/portal-detail-navigation.md#detail-scroll-and-focus) | Existing contract |
| CLAUDE.md:1963–1967 | Catch-Up URL Copying — EPG timeline/list programme details expose Copy archive URL for supported Xtream/M3U archives, including… | [Copy archive URL](../architecture/m3u-playlist-module.md#copy-archive-url) | Existing contract |
| CLAUDE.md:1971–2001 | Xtream Archive Downloads — Desktop Xtream Live TV programme details can enqueue completed catch-up as contentType: catchup. The queue… | [Xtream archive downloads](../architecture/download-manager.md#xtream-archive-downloads) | Existing contract |
| CLAUDE.md:2005–2010 | Desktop Source Health — Electron switcher/source rows share bounded, cached Xtream/Stalker/M3U URL checks through… | [Desktop source health](../architecture/m3u-playlist-module.md#desktop-source-health) | Existing contract |
| CLAUDE.md:2012–2018 | Desktop Source Health — Desktop Sources also offers library-wide selective cleanup through dialog-scoped SourceCleanupService.… | [Desktop source health](../architecture/m3u-playlist-module.md#desktop-source-health) | Existing contract |
| CLAUDE.md:2020–2022 | Desktop Source Health — Startup source auto-refresh uses SourceActivityService to protect busy IDs from cleanup. Late batch… | [Desktop source health](../architecture/m3u-playlist-module.md#desktop-source-health) | Existing contract |
## Master integration follow-up
During conflict resolution with master on 2026-09-21, the compact root guidance
was retained and the new upstream knowledge was checked against canonical docs:
- Live channel playlist handoff, including Stalker arrival and fallback behavior:
[portal navigation](../architecture/portal-detail-navigation.md).
- Shared locale-independent search folding and SQLite search variants:
[agent workflow](../development/agent-workflow.md#shared-search-text-folding).
- Stalker series resume, watch kind versus routing kind, and dashboard labels:
[portal navigation](../architecture/portal-detail-navigation.md) and
[workspace dashboard](../architecture/workspace-dashboard.md).
- Scoped XMLTV fallback and Xtream programme refresh helpers:
[M3U contracts](../architecture/m3u-playlist-module.md).
- Lazy portal EPG queues, revision handling and dashboard fallback:
[workspace dashboard](../architecture/workspace-dashboard.md).
The original 716-entry inventory above remains tied to its immutable source.
A subsequent master integration on 2026-09-21 also preserved TMDB year-evidence
ranking, its accepted older-season ambiguity, and v4 lookup-cache migration in
the updated [TMDB contract](../architecture/tmdb-metadata-enrichment.md). These
upstream additions stay in that canonical document rather than CLAUDE.md.