mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
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:
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <proposed_plan>, 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 || ^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@<target> --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/<project>//.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 <area>-<short-slug>.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: <short headline> (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 > General > 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 <video>… | [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 <audio> 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/<plugin>/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 — > 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 <proposed_plan>, 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 <area>-<short-slug>.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: <short headline> (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 || ^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@<target> --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/<project>//.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) — | Capability | Strategy | | --------------------------------- |… | [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<MatMenu>('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 <!-- ✅ Correct - Unwrap the signal --> <button [matMenuTriggerFor]="menu()">Open Menu</button> <!-- ❌… | [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<string>(); 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()) { <p>Welcome!</p> } @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=<x11-window> 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 <delta>… | [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 <button>, 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 > 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 <patch>-nightly.<commit date>.<run number> 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 <project>:<target> # 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/<plugin>/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 > General > 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.
|
||||
Reference in new issue
Block a user