Merge remote-tracking branch 'origin/master' into claude/reverent-thompson-163619

# Conflicts:
#	CLAUDE.md
#	package.json
This commit is contained in:
4gray committed 2026-07-04 11:55:34 +02:00
commit 0285cf6c40
127 files changed
+3651 -4869

No files matched your search

+8 -9
View File
@@ -28,10 +28,10 @@ Use it when changing existing views or introducing new list-based UI in the work
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html`
- Channel row styles:
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss`
- Shared EPG pane:
`libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html`
- Shared EPG pane styles:
`libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss`
- Shared EPG timeline:
`libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.html`
- Shared EPG timeline styles:
`libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.scss`
- Shared list selection style:
`apps/web/src/nav-list.scss`
- Theme tokens:
@@ -141,12 +141,11 @@ The shared row should be reused instead of rebuilding channel markup per view.
### Collapsible Live EPG
- Live TV layouts with an internal player use `app-live-epg-panel` around the
- Live TV layouts with an internal player render `app-epg-timeline` as the
EPG content, including playlist-specific live pages and the global
favorites/recent live tabs.
- The live panel toolbar owns the current-program summary and live date
navigation together, so `app-epg-list` hides its internal date navigator when
projected inside that panel.
- The timeline's own panel bar owns the current-program summary and live date
navigation together; there is no separate wrapper component around it.
- Collapsed state is shared across M3U, Xtream, and Stalker with
`live-epg-panel-state`; missing or invalid values restore to expanded.
- The collapsed panel is a slim current-program strip with a trailing progress
@@ -289,7 +288,7 @@ Settings use the same system but are flatter than content-heavy views.
Before creating new markup or CSS:
1. Check whether `app-channel-list-item` can be reused.
2. Check whether `app-epg-view` already provides the correct structure.
2. Check whether `app-epg-timeline` already provides the correct structure.
3. Check whether `nav-list.scss` already solves the list-selection problem.
4. Extend tokens first, duplicate styles last.
+45 -11
View File
@@ -266,16 +266,33 @@ per-tab EPG logic:
favorites). Callers read their `progressTick()` signal first so the computed
re-runs on the ~30s tick.
### EPG Timeline Panel
### EPG Panel (Timeline & List views)
The programme guide under the player is a horizontal **timeline ribbon**
(`app-epg-timeline`, `libs/ui/epg/src/lib/epg-timeline/`) shared by all four live
surfaces: the M3U video player, the unified live tab, and the Xtream and Stalker
live-stream layouts. It replaces the former vertical `app-epg-list` /
`app-epg-view`.
The programme guide under the player renders in one of **two interchangeable
views**, chosen by the **`epgViewMode`** setting (`'timeline'` default, or
`'list'`; Settings → EPG → *Guide view*):
The component stays presentation-focused; the reusable, view-agnostic pieces
(so a future list view can share them) are split out and re-exported from
- **Timeline** — a horizontal **ribbon** (`app-epg-timeline`,
`libs/ui/epg/src/lib/epg-timeline/`).
- **List** — a vertical, single-day **programme list** (`app-epg-list-view`,
`libs/ui/epg/src/lib/epg-list-view/`) with a prev/today/next stepper.
Both are shared by all four live surfaces: the M3U video player, the unified live
tab, and the Xtream and Stalker live-stream layouts (replacing the former
vertical `app-epg-list` / `app-epg-view`). `EpgListViewComponent` mirrors
`EpgTimelineComponent`'s input/output contract **1:1**, so each host swaps them
with a plain `@if (epgViewMode() === 'list') { <app-epg-list-view … /> } @else {
<app-epg-timeline … /> }` — identical bindings in both branches. Hosts read
`epgViewMode` from `SettingsStore` (a signal), so flipping the setting swaps the
panel live. The setting flows end-to-end (`Settings.epgViewMode` →
`DEFAULT_SETTINGS` → `SettingsStore`/`StorageMap` → the segmented control in
`settings-epg-section`) and needs no backend change. The control is
**Electron-only in practice** — the EPG settings section (and the form control)
is gated behind `supportsEpg`, which is false in PWA; there the stored value
simply stays at the `'timeline'` default.
Both components stay presentation-focused; the reusable, view-agnostic pieces
(shared by the timeline and the list) are split out and re-exported from
`@iptvnator/ui/epg`:
- `epg-timeline.utils.ts` (axis/blocks/date helpers) + `epg-timeline-render.util.ts`
@@ -290,6 +307,19 @@ The component stays presentation-focused; the reusable, view-agnostic pieces
scrolling + channel-select auto-focus); timeline-specific, kept out of the
component so it stays under the line ceiling.
The **list view** (`epg-list-view/`) composes those same shared modules — it does
**not** duplicate classification or gating logic. It reuses `classifyTimelineWhen`
/ `hasProgramsForDateKey` / `nearestDateKeyWithPrograms`, `epg-archive.util`,
`epg-summary.util`, the `epg-date` helpers, `EpgProgrammeDialogService`, and the
shared `app-epg-timeline-empty-state` — and drops all ribbon geometry, zoom, and
horizontal scroll. It filters the loaded window to the selected day (overlap-based,
matching `hasProgramsForDateKey`), sorts, and deduplicates via a pure
`buildEpgListRows` (`epg-list-view.utils.ts`); renders each row through the dumb
`app-epg-list-view-row`; and delegates its own vertical auto-focus + sticky
"now" strip to `EpgListScrollController` (`epg-list-scroll.controller.ts`). Render
states, the collapsed inline summary, the date stepper, catch-up/timeshift
activation, and the details dialog behave identically to the timeline.
- **One channel, preloaded window.** The panel always shows a single channel.
Each provider returns a multi-day window in roughly one call (M3U
`GET_CHANNEL_PROGRAMS`; Stalker `get_epg_info`; Xtream `get_simple_data_table`),
@@ -370,7 +400,11 @@ The component stays presentation-focused; the reusable, view-agnostic pieces
tag / "Watch") stays pinned at the bottom. With an inline player the guide is
a compact panel (`.epg.epg--inline` → `flex: 0 0 clamp(180px, 36vh, 264px)`
in `_portal-layout.scss`) so the player stays dominant; with an external
player the guide keeps `flex: 1` and fills the whole content area.
player the guide keeps `flex: 1` and fills the whole content area. In **list
mode** the hosts also set `.epg--list`, which raises only the inline clamp
(`--epg-inline-height: clamp(280px, 46vh, 430px)`) — vertical rows need more
height than the ribbon; the timeline height is unchanged, and the collapsed
56px clamp still wins because the modifier sets just the CSS variable.
- **Wide-tier description preview.** When a block is the `wide` tier (rendered
width ≥ `132px`, i.e. long programmes and/or zoomed in) **and** the programme
has a `desc`, a dimmed (`--text-secondary`) preview of the description renders
@@ -564,8 +598,8 @@ class EpgService {
| Component | Purpose |
| ----------------------------- | ------------------------------------ |
| `EpgListComponent` | Timeline view for single channel |
| `EpgListItemComponent` | Individual program in timeline |
| `EpgTimelineComponent` | Horizontal timeline for one channel |
| `EpgListViewComponent` | Vertical single-day list alternative |
| `EpgItemDescriptionComponent` | Program details dialog |
| `MultiEpgContainerComponent` | Grid view of all channels' schedules |
@@ -82,3 +82,16 @@ playback, or EPG data belongs in `libs/portal/shared/data-access`, not
`libs/portal/shared/util`. That keeps pure collection helpers importable by
Xtream/Stalker data-access libraries while allowing shared UI to use
provider-specific collection services without creating cycles.
Note: `workspace-shell-util` (`libs/workspace/shell/util`) is tagged
`type:data-access` despite its path. It exports injectable services such as
`WorkspaceStartupPreferencesService` that depend on `@iptvnator/services`, and
it must stay eagerly importable from `apps/web/src/app/app.routes.ts` without
pulling the lazy-loaded workspace shell feature bundle into the initial chunk.
## CI Enforcement
The `Lint` job in `.github/workflows/ci.yml` runs
`pnpm nx run-many --target=lint --all` on every PR, so
`@nx/enforce-module-boundaries` violations, legacy bare-alias imports, and
`max-lines` violations fail CI. Run `pnpm run lint` locally before pushing.
+7 -8
View File
@@ -14,7 +14,7 @@ Stalker now uses two EPG paths with different purposes:
- The active channel EPG panel uses `get_epg_info` as a bulk endpoint, fetches a
7-day window once per playlist session, caches programs by channel id, and
renders the selected channel through the shared `app-epg-list` component.
renders the selected channel through the shared `app-epg-timeline` component.
- Channel rows no longer send preview EPG requests during initial category load.
They stay empty until bulk EPG has been fetched once, then derive their
current program and progress bar from the cached bulk map.
@@ -39,7 +39,7 @@ date-navigator UI used in the M3U/Xtream flows.
│ │ ensureBulkItvEpg(168) │
│ │ selectedItvEpgPrograms() │
│ ▼ │ │
│ current program preview ├── bulk hit → app-epg-list │
│ current program preview ├── bulk hit → app-epg-timeline │
│ after first bulk load └── empty/unsupported → short fallback │
└────────────────────────────────────────────────────────────────────────────┘
│ │
@@ -166,7 +166,7 @@ Key mapped fields:
### Active panel data (`get_epg_info` / fallback) → `EpgProgram`
The active panel uses controlled `EpgProgram[]` because `app-epg-list` filters
The active panel uses controlled `EpgProgram[]` because `app-epg-timeline` filters
and groups by day.
Normalization rules:
@@ -183,10 +183,9 @@ Normalization rules:
| File | Purpose |
| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-epg.feature.ts` | bulk cache and fallback handling |
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | active-channel EPG loading and controlled `app-epg-list` wiring |
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | active-channel EPG loading and controlled `app-epg-timeline` wiring |
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | active panel template |
| `libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts` | row preview loading |
| `libs/ui/shared-portals/src/lib/epg-list/epg-list.component.ts` | shared controlled EPG list with date navigator |
| `libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts` | shared controlled EPG timeline with date navigator |
### Store API
@@ -210,7 +209,7 @@ playlists.
1. User activates a live channel
2. The component ensures playback link resolution as before
3. The component calls `ensureBulkItvEpg(168)` on first use for the playlist
4. `selectedItvEpgPrograms()` feeds `app-epg-list`
4. `selectedItvEpgPrograms()` feeds `app-epg-timeline`
5. If the selected channel has no bulk programs, the component falls back to
`get_short_epg`
@@ -267,5 +266,5 @@ is available.
## Future Enhancements
- add cache refresh / invalidation for long-running live sessions
- add Stalker catch-up support to `app-epg-list` once the playback flow exists
- add Stalker catch-up support to `app-epg-timeline` once the playback flow exists
- optionally add category-level prefetch timing metrics for bulk EPG
+24 -2
View File
@@ -17,16 +17,38 @@ pnpm nx show projects --withTarget e2e
| ---------------------------------- | ----------------------------------- |
| Angular renderer entry points | `pnpm run typecheck:web` |
| Electron main process entry points | `pnpm run typecheck:backend` |
| Curated CI unit suite | `pnpm run test:unit:ci` |
| Full unit suite (all projects) | `pnpm run test:unit:ci` |
| EPG data access | `pnpm nx test epg-data-access` |
| Workspace shell utilities | `pnpm nx test workspace-shell-util` |
| Shared SQLite schema/connection | `pnpm nx test database` |
| Packaging metadata | `pnpm nx test packaging` |
## Lint
```bash
pnpm run lint # nx run-many --target=lint --all
pnpm nx lint <project> # single project
```
The CI workflow (`.github/workflows/ci.yml`) runs lint for every project on
each PR. This enforces `@nx/enforce-module-boundaries` (scope/domain/type tag
constraints), the legacy bare-alias ban, and the `max-lines` file-size rule
(hard maximum 400 lines per TypeScript file). Files that predate the
`max-lines` rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after
splitting a baselined file below the limit, regenerate the list with
`node tools/eslint/generate-max-lines-baseline.mjs`. Never add new files to
the baseline.
## Coverage Tiers
Use `tools/coverage/coverage-policy.json` as the source of truth for coverage
ownership.
ownership. Every project with a `test` target must be classified in a tier;
`pnpm run coverage:policy:check` (part of `coverage:ci`) fails CI when a new
project is missing from the policy, a listed project no longer exists, or a
Tier A entry has no test target. CI runs Tier A with coverage (uploaded to
Codecov) and each Tier B/C project's `validationCommand` (falling back to
`nx test`) without coverage; projects with an `e2e` target are skipped there
because the E2E workflow already runs them on every PR.
| Tier | Rule | Validation |
| --- | --- | --- |