diff --git a/docs/architecture/category-management.md b/docs/architecture/category-management.md index 0269f5747..177378954 100644 --- a/docs/architecture/category-management.md +++ b/docs/architecture/category-management.md @@ -56,7 +56,7 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0 **Category Management Dialog** -- Path: `apps/web/src/app/xtream-electron/category-management-dialog/` +- Path: `libs/portal/xtream/feature/src/lib/category-management-dialog/` - Features: - Checkbox list of all categories - Select All / Deselect All buttons @@ -77,7 +77,7 @@ Both components: ### Store -**File**: `apps/web/src/app/xtream-electron/xtream.store.ts` +**File**: `libs/portal/xtream/data-access/src/lib/stores/xtream.store.ts` Added `reloadCategories()` method to refresh categories from database after visibility changes, ensuring the sidebar updates immediately. @@ -96,9 +96,23 @@ When a user refreshes an Xtream playlist, hidden category preferences are preser 2. **Temporary storage**: The hidden categories are stored in `localStorage` under key `xtream-restore-{playlistId}` along with favorites and recently viewed data 3. **During re-import**: When categories are saved via `DB_SAVE_CATEGORIES`, the data source checks `localStorage` for saved hidden category xtreamIds 4. **Restoration**: Categories matching the saved xtreamIds are inserted with `hidden = true`, preserving the user's visibility preferences +5. **ID normalization**: Xtream category IDs arrive from the API as strings, while SQLite stores `categories.xtream_id` as an integer. Restoration must normalize incoming `category_id` values before matching them against saved hidden-category xtreamIds. This ensures that users don't lose their category visibility customizations when refreshing playlists to get updated content. +### Debugging Note + +Hidden-category restoration runs through the Electron DB worker. When debugging +or validating a fix in a live Electron app: + +1. rebuild the worker-backed Electron runtime +2. restart the running Electron process +3. reconnect `agent-browser --cdp 9222` + +Otherwise the app may still be using an older +`dist/apps/electron-backend/workers/database.worker.js` bundle even though the +TypeScript source has already been updated. + ## Files Changed ``` @@ -117,20 +131,21 @@ libs/services/src/lib/ libs/ui/components/src/lib/recent-playlists/ └── recent-playlists.component.ts # Stores hidden categories to localStorage on refresh -apps/web/src/app/xtream-electron/ -├── category-management-dialog/ # Dialog component +libs/portal/xtream/feature/src/lib/ +├── category-management-dialog/ # Dialog component │ ├── category-management-dialog.component.ts │ ├── category-management-dialog.component.html │ └── category-management-dialog.component.scss -├── data-sources/ -│ └── electron-xtream-data-source.ts # Reads/passes hidden categories on save -├── xtream-main-container.component.ts # Added button & dialog +├── xtream-main-container.component.ts # Added button & dialog ├── xtream-main-container.component.html ├── live-stream-layout/ -│ ├── live-stream-layout.component.ts # Added button & dialog +│ ├── live-stream-layout.component.ts # Added button & dialog │ └── live-stream-layout.component.html -├── sidebar.scss # Updated header styles -└── xtream.store.ts # Added reloadCategories method + +libs/portal/xtream/data-access/src/lib/ +├── data-sources/ +│ └── electron-xtream-data-source.ts # Reads/passes hidden categories on save +└── stores/xtream.store.ts # Added reloadCategories method apps/web/src/assets/i18n/ └── en.json # Added translation keys diff --git a/docs/architecture/download-manager.md b/docs/architecture/download-manager.md index 997cc3b8b..20dd78b94 100644 --- a/docs/architecture/download-manager.md +++ b/docs/architecture/download-manager.md @@ -15,7 +15,7 @@ The download manager is a desktop-only feature that layers a curated queue, prog - **Downloads service** (`apps/web/src/app/services/downloads.service.ts`) Signals back the current download list while `hasDownloads` and `isAvailable` gates UI rendering. Before each download the service resolves a download folder (stored in `SettingsStore` or fetched via `downloadsGetDefaultFolder`) and calls `downloadsStart`. The backend extracts the file extension from the URL or falls back to `mp4`. `onDownloadsUpdate` updates the signal, while helper methods `retryDownload`, `removeDownload`, `cancelDownload`, and `playDownload` talk to the corresponding IPC commands so retries reuse existing rows and completed items can open the recorded path. -- **Downloads view** (`apps/web/src/app/xtream-electron/downloads`) +- **Downloads view** (`libs/portal/downloads/feature`) A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons. `downloads.component.html` now wraps the list inside a scrollable panel (`downloads__list-wrapper`) so long queues stay reachable, and `downloads.component.scss` drives a bold two-tone aesthetic inspired by the frontend-design mandate—gradient cards, floating avatars, and theme-aware variables triggered via `body.dark-theme`. Failed/canceled cards now show retry/delete controls, queued/downloading cards show a cancel icon, and completed cards render inline play/open buttons with `mat-icon` cues. The header also shows the resolved download folder and a `CHANGE FOLDER` action. - **Theme fixes** diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md new file mode 100644 index 000000000..92c0cdc09 --- /dev/null +++ b/docs/architecture/embedded-inline-playback.md @@ -0,0 +1,124 @@ +# Embedded Inline Playback + +This document records the current contract for embedded playback in portal detail views. + +## Summary + +- Embedded web players are `videojs`, `html5`, and `artplayer`. +- External players are `mpv` and `vlc`. +- Live playback stays inline in dedicated live layouts. +- VOD and series detail playback now also stays inline on canonical detail surfaces. +- Material dialog playback remains only as a fallback for older non-detail callers. + +## Scope + +The first pass is intentionally limited: + +- Xtream VOD detail route +- Xtream series detail route +- Stalker VOD detail view +- Stalker series detail view + +Not migrated in this pass: + +- Generic non-detail playback entry points that still call `PlayerService.openPlayer(...)` +- Any collection/search surface that does not host a canonical detail surface of its own + +## Components + +Shared inline player shell: + +- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/portal-inline-player/portal-inline-player.component.ts` + +Xtream detail hosts: + +- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts` + +Stalker detail hosts: + +- `/Users/4gray/Code/iptvnator/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` +- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-series-view/stalker-series-view.component.ts` + +Fallback dialog path: + +- `/Users/4gray/Code/iptvnator/apps/web/src/app/services/player.service.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/player-dialog/player-dialog.component.ts` + +## Playback Decision Rule + +When a detail view starts playback: + +1. Resolve or construct a typed playback payload. +2. Check the active player setting. +3. If the player is embedded, render the inline player inside the current detail view. +4. If the player is external, hand the same payload to `PlayerService` for MPV/VLC playback. + +The detail host owns inline state. `PlayerService` is no longer the primary owner of UI playback state for canonical VOD/series detail screens. + +## Typed Playback Payload + +Shared playback payloads live in: + +- `/Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/portal-playback.interface.ts` + +Types introduced: + +- `PlayerContentInfo` +- `ResolvedPortalPlayback` + +These provide a single shape for: + +- `streamUrl` +- `title` +- optional thumbnail and resume start time +- playback-position metadata +- optional external-player headers and request metadata + +## Xtream Behavior + +Xtream detail views already own canonical routes, so they construct playback locally and decide inline vs external locally. + +Behavior to preserve: + +- resume/playback position continues saving from `timeUpdate` +- back navigation clears inline playback with the route +- favorites, recent, and search still route into canonical Xtream detail screens before playback + +## Stalker Behavior + +Stalker previously resolved playback and opened UI in the same method. + +Current contract: + +- `resolveVodPlayback(...)` returns a `ResolvedPortalPlayback` +- `createLinkToPlayVod(...)` remains as a compatibility wrapper for untouched callers +- canonical Stalker detail views use the resolver directly and decide inline vs external locally + +This keeps: + +- inline/store-state detail navigation intact +- series and VOD-as-series support intact +- non-detail callers working until they are migrated + +## Playback Position Saving + +The old dialog path saved playback positions from inside `PlayerDialogComponent`. + +The new contract is: + +- inline detail hosts listen to `timeUpdate` +- each host throttles saves +- each host persists via existing playback-position infrastructure + +This avoids coupling inline UI state to a global dialog. + +## Future Migration Rule + +If a non-detail surface is converted away from dialog playback: + +- give that surface a canonical inline host +- switch it to `ResolvedPortalPlayback` +- do not move portal-specific navigation into `PlayerService` + +The preferred direction is view-owned inline playback, not a larger dialog manager. diff --git a/docs/architecture/external-wiki-sync.md b/docs/architecture/external-wiki-sync.md new file mode 100644 index 000000000..ffbf00400 --- /dev/null +++ b/docs/architecture/external-wiki-sync.md @@ -0,0 +1,97 @@ +# External Wiki Sync + +Related: + +- [Workspace Shell](./workspace-shell.md) +- [SQLite DB Worker](./sqlite-db-worker.md) + +## Summary + +- Repo docs are canonical, even when they were originally drafted by an LLM. +- The external Obsidian wiki imports canonical repo docs read-only into `_repo-context/`. +- Higher-level synthesis pages stay outside `_repo-context/` in the wiki's own folders. +- Sync is one-way by default: repo docs -> wiki context. + +## Ownership Model + +Canonical repo docs include: + +1. `docs/architecture/**/*.md` +2. top-level workflow docs such as `README.md`, `GETTING-STARTED.md`, `AGENTS.md`, and `CLAUDE.md` +3. selected module `README.md` files when they describe current code behavior or workflows + +The external wiki can add cross-links, feature pages, decision notes, and synthesis pages, but it must not become a second source of truth for the same implementation details. + +## Export Scope + +The repo-owned exporter writes only to `_repo-context/` inside the external vault. + +Current default export scope: + +1. `docs/architecture/**/*.md` +2. `README.md` +3. `GETTING-STARTED.md` +4. `AGENTS.md` +5. `CLAUDE.md` +6. selected module `README.md` files + +The exporter also generates: + +1. `_repo-context/index.md` +2. `_repo-context/repo-map.md` +3. `_repo-context/recent-changes.md` +4. `_repo-context/manifest.json` +5. `_repo-context/state.json` + +## Running The Exporter + +Set the external vault path in the shell environment: + +```bash +export IPTVNATOR_WIKI_VAULT=/absolute/path/to/your/obsidian-vault +``` + +Then run: + +```bash +pnpm wiki:export --mode full +pnpm wiki:export --mode changed +``` + +You can also override the vault path per command: + +```bash +pnpm wiki:export --mode changed --vault /absolute/path/to/your/obsidian-vault +``` + +If the vault path is missing, the exporter skips cleanly and reports why it did not run. + +## Agent Workflow After Changes + +After a meaningful implementation change, agents must assess whether canonical repo docs need updates. + +Documentation-worthy changes include: + +1. new or changed user-visible behavior +2. architecture or data-flow changes +3. non-obvious maintenance workflows +4. new setup, debugging, or operational steps +5. new subsystem contracts or boundaries + +Prefer updating an existing authoritative doc before creating a new one: + +1. `README.md` for top-level developer or user workflows +2. `docs/architecture/` for architecture, ownership, and behavior contracts +3. the nearest module `README.md` for local usage or behavior + +If docs changed and `IPTVNATOR_WIKI_VAULT` is configured, agents should run `pnpm wiki:export --mode changed` before considering the task complete. + +## Promotion Workflow + +If a wiki page becomes stable enough to be canonical: + +1. promote that content back into a repo doc +2. treat the repo doc as the source of truth +3. export again so `_repo-context/` reflects the promoted canonical doc + +The wiki page can then either link to the repo-backed generated page or remain as a smaller synthesis page that references the canonical doc. diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md new file mode 100644 index 000000000..1ead04c24 --- /dev/null +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -0,0 +1,252 @@ +# IPTVnator UI Guidelines + +This document captures the current UI language used across IPTVnator, with emphasis on channel lists, EPG views, settings surfaces, and shared selection patterns. + +Use it when changing existing views or introducing new list-based UI in the workspace, Xtream, or Stalker flows. + +## Core Principles + +1. Prefer shared components over duplicated markup. + The canonical channel row is `app-channel-list-item`. + +2. Drive emphasis through selection state, not through constant decoration. + Neutral rows should stay quiet. Only active or current items should pick up strong color. + +3. Use the same selection language everywhere. + Selected nav items, channels, and current EPG cards should feel like the same system. + +4. Keep dark and light themes intentionally different. + Dark theme can carry more density and tinted surfaces. + Light theme should be flatter and cleaner, with white or near-white cards. + +5. Scroll ownership must be explicit. + Headers stay visible. Lists scroll. Do not let nested panes compete for scroll. + +## Canonical References + +- Channel row: + `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 list selection style: + `apps/web/src/nav-list.scss` +- Theme tokens: + `apps/web/src/m3-theme.scss` +- Settings surfaces: + `apps/web/src/app/settings/settings.component.scss` + +## Shared Tokens + +These tokens are the base for interactive emphasis: + +- `--app-selection-color` +- `--app-selection-surface` +- `--app-selection-surface-strong` +- `--app-selection-border` +- `--app-selection-glow` + +Use Material surface tokens for neutral surfaces: + +- `--mat-sys-surface` +- `--mat-sys-surface-container-low` +- `--mat-sys-surface-container` +- `--mat-sys-surface-container-high` +- `--mat-sys-outline-variant` +- `--mat-sys-on-surface` +- `--mat-sys-on-surface-variant` + +Do not hardcode unrelated accent colors for selected state when these tokens already exist. + +## Selection Pattern + +Apply the same visual recipe to selected list items, active channels, and current EPG items: + +- Background: + `linear-gradient(135deg, var(--app-selection-surface-strong), var(--app-selection-surface))` +- Border: + `var(--app-selection-border)` +- Glow: + outer shadow using `var(--app-selection-glow)` +- Lift: + `transform: translateY(-1px)` for selected list items only +- Text: + selected text should inherit `var(--app-selection-color)` + +Use this pattern for: + +- `.nav-item.selected` / `.nav-item.active` +- `.channel-list-item.active` +- `.epg-item.current-program` + +Do not add extra badges, left rails, or second selection systems unless there is a strong reason. + +## Channel List Item + +The shared row should be reused instead of rebuilding channel markup per view. + +### Structure + +- Min height: + `68px` +- Horizontal gap: + `12px` +- Padding: + `8px 10px 8px 12px` +- Radius: + `12px` +- Logo shell: + `44x44`, rounded, subtle inset treatment +- Compact variant: + `52px` min height with slightly tighter padding + +### Content Layout + +- Title is one line, medium-bold, slightly condensed +- Program title is a secondary line with lower emphasis +- Timeline uses three columns: + start time, progress bar, end time +- Action buttons sit on the trailing edge and inherit row color + +### Logo Rules + +- Show fallback icon only when no image is available or image loading fails +- Do not render placeholder and real logo at the same time +- Keep logos contained with `object-fit: contain` + +## EPG Views + +### Shared EPG Pane + +- Header title stays sticky +- Program list is the only scrolling region +- Add bottom padding so the last program is not clipped +- Current program card uses the same selection treatment as selected channels + +### EPG Card + +- Radius: + `14px` +- Neutral cards use low-contrast surface treatment +- Current card uses selection surface and selection border +- Description should clamp rather than overflow + +### Sticky Header + +- Keep the title readable above content +- Use a solid or near-solid backing surface +- Do not let it overlap or cover player controls + +## Progress Bars + +Channel preview progress and EPG current-program progress should stay visually aligned. + +### Track + +- Height: + `6px` +- Shape: + full pill radius +- Neutral background: + medium gray or neutral surface tint +- Include a slight inset edge so the remaining duration is visible + +### Fill + +- Use `--app-selection-color` +- Add a subtle sheen, not a heavy gradient +- Add a restrained glow, not a neon effect + +The progress bar should clearly communicate: + +- completed duration +- remaining duration + +Avoid making the track too faint, especially in dark theme. + +## Navigation Lists + +Use the shared `nav-list.scss` treatment for sidebar and context-panel list items. + +### Rules + +- Keep labels one line with ellipsis +- Keep icon area clear from the selection border and any decorative rail +- Hover is neutral surface, not the selected color +- Selected state uses the shared selection recipe + +If the label is too long for the rail, shorten the label key instead of shrinking the component until it becomes inconsistent. + +## Settings Surfaces + +Settings use the same system but are flatter than content-heavy views. + +### Light Theme + +- Prefer white or near-white cards +- Use neutral borders from `--mat-sys-outline-variant` +- Keep active sections mostly defined by outline and subtle tint +- Avoid dark translucent backgrounds + +### Dark Theme + +- Denser tinted surfaces are acceptable +- Neutral rows can use low-opacity dark overlays +- Keep strong blue tint reserved for active sections and selected items + +## Theme Guidance + +### Light Theme + +- Flat beats glossy +- White and surface-container layers should separate content +- Selection should read as a blue outline plus soft tint, not a solid slab + +### Dark Theme + +- Slight translucency is acceptable +- Background layers can be deeper and more cinematic +- Keep contrast readable without going pure white everywhere + +## Reuse Strategy + +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. +3. Check whether `nav-list.scss` already solves the list-selection problem. +4. Extend tokens first, duplicate styles last. + +## Implementation Workflow + +When updating IPTVnator UI: + +1. Inspect the current shared component first. +2. Reuse the shared structure where possible. +3. Keep selection, progress, and spacing in sync across Xtream, Stalker, and shared portal views. +4. Verify in both light and dark themes. +5. Verify in the running Electron app when the change is visual or layout-sensitive. + +## Anti-Patterns + +Avoid these: + +- introducing a new selected-state color unrelated to the theme tokens +- duplicating channel row markup in portal-specific views +- showing placeholder logos behind real logos +- making entire panes scroll when only the list should scroll +- using dark translucent fills unchanged in light theme +- solving cramped sidebars with smaller fonts instead of shorter labels + +## Definition Of Done For UI Changes + +A visual change is not done until: + +1. Shared component reuse was considered first. +2. Light theme and dark theme both look intentional. +3. Selection and progress states match existing IPTVnator patterns. +4. Scroll behavior is correct. +5. The result was checked in the running app for layout-sensitive work. diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index d677f704d..eece9b227 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -16,7 +16,7 @@ The M3U playlist module provides: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ VIDEO PLAYER PAGE │ -│ apps/web/src/app/home/video-player/ │ +│ libs/playlist/m3u/feature-player/src/lib/video-player/ │ ├─────────────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌──────────────────────┐ ┌────────────────────┐ │ │ │ Sidebar │ │ Video Player │ │ EPG List │ │ @@ -237,7 +237,7 @@ class EpgService { ## Video Player -**Location**: `apps/web/src/app/home/video-player/` +**Location**: `libs/playlist/m3u/feature-player/src/lib/video-player/` ### Supported Players - **ArtPlayer** (default) - Modern player with plugins diff --git a/docs/architecture/navigation-ux-analysis.md b/docs/architecture/navigation-ux-analysis.md new file mode 100644 index 000000000..4b9910cad --- /dev/null +++ b/docs/architecture/navigation-ux-analysis.md @@ -0,0 +1,119 @@ +# UX/UI Analysis: Header vs Rail Navigation + +Date: 2026-03-22 + +## Overview + +Evaluation of IPTVnator's navigation architecture — specifically the separation between **global actions in the top header** and **playlist-local actions in the left rail sidebar**, assessed from a user understanding perspective. + +## Current Architecture + +| Region | Intended Scope | Actual Contents | +|--------|---------------|-----------------| +| **Header** (top) | Global / app-wide | Playlist switcher, search, add playlist, global favorites, downloads, **context menu with local actions** | +| **Rail** (left) | Local / playlist-specific | Dashboard (global), Sources (global), **dynamic provider links** (local), Settings (global) | + +Neither region is purely global or purely local. Both mix scopes, which muddies the mental model. + +## Strengths + +- **Playlist switcher in the header** is excellent placement. Acts like a "workspace context selector" — similar to Slack's workspace switcher or VS Code's project selector. +- **Command palette** nails the global-vs-local distinction with explicit "GLOBAL ACTIONS" and "THIS PLAYLIST" section headers. Clearest articulation of scope in the entire UI. +- **Rail dividers** between static workspace links (Dashboard, Sources) and dynamic provider links provide a subtle visual boundary hinting at the scope change. +- **Search bar adapting its placeholder text** per route is good contextual affordance. +- **Settings at the rail bottom** follows a well-established pattern (Slack, Discord, VS Code). + +## Confusion Points + +### A. Rail Mixes Global and Local Without Explaining Why + +When a user selects an Xtream playlist, the rail shows: +``` +Dashboard <- global +Sources <- global +----------------- +Movies <- local (Xtream) +Live TV <- local (Xtream) +Series <- local (Xtream) +----------------- +Search <- local (Xtream) +Recently viewed <- local (Xtream) +Favorites <- local (Xtream) +----------------- +Settings <- global +``` + +When switching to M3U: +``` +Dashboard <- global +Sources <- global +----------------- +All channels <- local (M3U) +Groups <- local (M3U) +Recently viewed <- local (M3U) +Favorites <- local (M3U) +----------------- +Settings <- global +``` + +**Issue:** The dynamic links change silently. There's no label like "rucolor.tv" or "clean.m3u" above the provider links to indicate *which* playlist these links belong to. Users who switch playlists via the header dropdown may not immediately notice the rail updated. + +**Severity:** Medium. + +### B. Header's Three-Dot Menu Breaks the "Global Header" Mental Model + +The context actions menu in the header contains: +- **Playlist Info** — local to the current playlist +- **Account Info** — local to the current Xtream portal +- **Clear Recently Viewed** — local bulk action + +These are playlist-scoped actions living in what should be the "global" header area. + +**Severity:** Low-Medium. + +### C. "Favorites" Appears in Both Global and Local Contexts + +- **Header:** Global Favorites star icon (cross-playlist) +- **Rail:** Favorites link (playlist-specific) + +A user clicking the star in the header vs the heart in the rail gets *different* favorites views with *no* clear labeling of "global" vs "this playlist." + +**Severity:** Medium-High. Most likely source of user confusion. + +### D. Search Bar Scope Is Invisible + +The search bar disables itself on some routes and changes behavior on others. The placeholder text changes, but "Search in this section..." doesn't clarify *which* section. + +**Severity:** Low. + +## Recommendations + +### Quick Wins (Low Effort, High Impact) + +1. **Add a playlist name label above the dynamic rail links.** Small, muted text showing "clean.m3u" or "rucolor.tv" above the provider-specific navigation. +2. **Differentiate the Global Favorites icon from Local Favorites.** Use different icons, tooltip distinction, or a small globe badge on the global favorites star. +3. **Add a scope label to the search bar** when active: "Searching in Live TV" or "Searching in clean.m3u" instead of generic "Search in this section..." + +### Medium Effort + +4. **Consider moving the three-dot context menu into the context panel** rather than the header, keeping the header purely global. +5. **Animate the rail transition** when switching playlists — a subtle slide or fade to signal that links changed. + +## Overall Assessment + +**Score: 7/10 — Good, with clear improvement opportunities.** + +The architecture follows patterns users will recognize from Slack, VS Code, and Spotify. The main risks are the **silent dynamic rail** and the **favorites scope ambiguity**. Fixing those two issues would bring this to a 9/10 for navigational clarity. + +### Design Principle + +The command palette already has the right model: **explicit scope labels**. Apply this same principle to the rail and header. Anywhere an action's scope isn't obvious from its placement, label it. + +## Key Files + +- `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html` +- `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts` +- `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts` +- `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts` +- `libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts` +- `libs/workspace/shell/feature/src/lib/workspace-command-palette/workspace-command-palette.component.ts` diff --git a/docs/architecture/portal-detail-navigation.md b/docs/architecture/portal-detail-navigation.md new file mode 100644 index 000000000..c3537adfc --- /dev/null +++ b/docs/architecture/portal-detail-navigation.md @@ -0,0 +1,81 @@ +# Portal Detail Navigation + +This document records the current navigation contract for Xtream and Stalker detail flows, especially for favorites, recently viewed, search, and category content. + +Related: + +- [Embedded Inline Playback](./embedded-inline-playback.md) + +## Summary + +- Xtream uses a route-first detail model. +- Stalker uses an inline/store-state detail model. +- Keep favorites/recent/search behavior aligned with the canonical detail model of the same portal. +- Do not force both portals into the same behavior unless the full portal detail architecture is being changed. + +## Xtream + +Xtream details are represented by canonical routes. + +Examples: +- `/xtreams/:id/vod/:categoryId/:vodId` +- `/xtreams/:id/series/:categoryId/:serialId` + +Implication: +- Favorites, recently viewed, and search should redirect to the original Xtream content route and item route. +- This keeps the URL, browser history, and detail rendering model aligned with normal Xtream browsing. + +Current code paths: +- `libs/portal/xtream/feature/src/lib/favorites/favorites.component.ts` +- `libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts` +- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` + +Behavior to preserve: +- Selecting an Xtream item from favorites/recent/search should navigate to the Xtream content type/category/item route when the item is not a live stream. +- Live streams can still open through the player path rather than a detail route. + +## Stalker + +Stalker details are represented by store state and inline detail rendering on the current screen. + +Examples: +- Category content sets `selectedItem` and renders details inline. +- Search sets `selectedItem` and stays on the search view. +- Favorites and recently viewed stay on their current collection screen and open inline detail. + +Implication: +- Favorites, recently viewed, and search should remain in the current Stalker view when opening VOD/series details. +- This keeps Stalker behavior aligned with its normal category-content and search flow. + +Current code paths: +- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` +- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` +- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` (Stalker branch) + +Behavior to preserve: +- Favorites/recent/search should not navigate away to a canonical Stalker detail route because Stalker does not currently use one. +- ITV/live items can still trigger playback immediately. + +## Decision Rule For Future Changes + +When deciding how a favorites/recent/search click should behave: + +1. Follow the portal's canonical detail model. +2. Prefer local consistency within the portal over cross-portal sameness. +3. Only unify Xtream and Stalker behavior if the full detail architecture is being unified as well. + +That means: +- Xtream: navigate to the canonical route. +- Stalker: stay in the current screen and open inline detail. + +## Refactor Guidance + +If a future change proposes that Stalker favorites/recent should deep-link into category routes: +- also update Stalker category-content and search behavior +- define a canonical Stalker detail route model first +- update architecture docs and portal skills together + +If a future change proposes that Xtream favorites/recent should stay inline: +- also replace Xtream route-based detail pages with a portal-local inline detail model +- verify history/back behavior and deep links still make sense diff --git a/docs/architecture/remote-control.md b/docs/architecture/remote-control.md index 83c77b94d..28f647c90 100644 --- a/docs/architecture/remote-control.md +++ b/docs/architecture/remote-control.md @@ -99,7 +99,7 @@ Type definitions: ## Shared helpers -- File: `apps/web/src/app/shared/services/remote-channel-navigation.util.ts` +- File: `libs/portal/shared/util/src/lib/remote-channel-navigation.ts` Functions: @@ -110,7 +110,7 @@ Used by M3U, Xtream, and Stalker live integrations. ## M3U integration -- File: `apps/web/src/app/home/video-player/video-player.component.ts` +- File: `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` Implemented behavior: @@ -133,7 +133,7 @@ Implemented behavior: ## Xtream integration (live view) -- File: `apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.ts` +- File: `libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts` Implemented behavior: @@ -156,7 +156,7 @@ Implemented behavior: ## Stalker integration (ITV live view) -- File: `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` +- File: `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` Implemented behavior: diff --git a/docs/architecture/sqlite-db-worker.md b/docs/architecture/sqlite-db-worker.md new file mode 100644 index 000000000..325ce946d --- /dev/null +++ b/docs/architecture/sqlite-db-worker.md @@ -0,0 +1,479 @@ +# SQLite DB Worker + +This document records the current non-EPG SQLite worker implementation in the +Electron app. + +Related: + +- [Category Management](./category-management.md) +- [Workspace Shell](./workspace-shell.md) + +## Summary + +- Heavy non-EPG SQLite work no longer runs on Electron's main thread. +- A dedicated long-lived database worker now handles the slow Xtream and + playlist database operations that were freezing the UI. +- Renderer APIs stay stable. The main change is that progress and long-running + state now flow through a request-scoped `DB_OPERATION_EVENT` contract instead + of a single global progress event. + +## Goals + +The worker cutover addresses three concrete problems: + +1. Main-process UI stalls during large SQLite operations. +2. Xtream import progress events were global and unsafe for concurrent jobs. +3. EPG and non-EPG writers needed shared SQLite concurrency settings so they + can coexist without `SQLITE_BUSY` regressions. + +## Current Ownership + +### Main-process runtime wiring + +These files own worker lifecycle and IPC bridging: + +1. `apps/electron-backend/src/app/services/database-worker-client.ts` +2. `apps/electron-backend/src/app/events/database/category.events.ts` +3. `apps/electron-backend/src/app/events/database/content.events.ts` +4. `apps/electron-backend/src/app/events/database/playlist.events.ts` +5. `apps/electron-backend/src/app/events/database/xtream.events.ts` +6. `apps/electron-backend/src/main.ts` + +### Worker runtime + +These files own the worker protocol and the SQLite work itself: + +1. `apps/electron-backend/src/app/workers/database-worker.types.ts` +2. `apps/electron-backend/src/app/workers/database.worker.ts` +3. `apps/electron-backend/src/app/workers/database.worker-connection.ts` +4. `apps/electron-backend/src/app/workers/worker-runtime-paths.ts` + +### Pure database operation modules + +Keep SQL-heavy logic here so the worker entry remains a thin dispatcher: + +1. `apps/electron-backend/src/app/database/operations/category.operations.ts` +2. `apps/electron-backend/src/app/database/operations/content.operations.ts` +3. `apps/electron-backend/src/app/database/operations/playlist.operations.ts` +4. `apps/electron-backend/src/app/database/operations/xtream.operations.ts` + +## Worker Architecture + +### Request flow + +1. Renderer calls the existing preload API such as `window.electron.dbSaveContent`. +2. `ipcMain.handle(...)` in the Electron backend builds a payload and delegates + to `DatabaseWorkerClient`. +3. `DatabaseWorkerClient` lazily starts one long-lived `worker_threads` worker + and correlates requests with a generated `requestId`. +4. The worker executes SQLite work and sends back either: + 1. `ready` + 2. `event` + 3. `response` +5. The main process resolves the IPC request and forwards worker events back to + the originating renderer process. + +### Why one long-lived worker + +- It avoids worker startup cost on every search/delete/import. +- It centralizes failure handling and restart behavior. +- It mirrors the existing EPG worker approach without multiplying writable + SQLite owners. + +### Packaged worker bootstrap + +Packaged Electron builds do not load worker scripts and native modules from the +same place: + +1. worker scripts live under `Resources/dist/apps/electron-backend/workers` +2. unpacked native modules live under one of the approved + `app.asar.unpacked/.../node_modules` locations + +Both the EPG worker and the DB worker now share the same runtime helper: + +1. `resolveWorkerRuntimeBootstrap(...)` for main-process worker launch +2. `loadNativeModuleFromSearchPaths(...)` for worker-side native module loading + +The helper uses `process.resourcesPath` as the primary packaged base and keeps +`path.dirname(app.getAppPath())` only as a fallback. + +## Worker Message Contract + +The worker contract lives in +`apps/electron-backend/src/app/workers/database-worker.types.ts`. + +### Core message types + +1. `DbWorkerRequestMessage` +2. `DbWorkerResponseMessage` +3. `DbWorkerEventMessage` +4. `DbOperationEvent` + +### Progress event contract + +The worker now emits request-scoped events with: + +- `operationId` +- `operation` +- `playlistId` +- `status` +- optional `phase` +- optional `current` +- optional `total` +- optional `increment` + +Current shipped operation names: + +1. `save-content` +2. `delete-xtream-content` +3. `restore-xtream-user-data` +4. `delete-playlist` +5. `delete-all-playlists` + +The event is forwarded to the renderer as `DB_OPERATION_EVENT`. + +### Cancellation contract + +Long-running Xtream and playlist operations now support best-effort +cancellation. + +Renderer requests cancellation via: + +1. `DB_CANCEL_OPERATION` +2. `window.electron.dbCancelOperation(operationId)` +3. `DatabaseService.cancelOperation(operationId)` + +If a worker operation is canceled: + +1. the worker emits a final `cancelled` event +2. the request rejects with an `AbortError` +3. the UI clears its busy state without treating the operation as success + +Cancellation is cooperative and chunk-based. Already committed SQLite batches +stay committed. + +## Renderer Contract + +The preload bridge keeps the existing database methods but adds scoped worker +events. + +### Important preload APIs + +1. `onDbOperationEvent(callback)` +2. `dbSaveContent(playlistId, streams, type, operationId?)` +3. `dbDeleteXtreamContent(playlistId, operationId?)` +4. `dbRestoreXtreamUserData(..., operationId?)` +5. `dbDeletePlaylist(playlistId, operationId?)` +6. `dbDeleteAllPlaylists(operationId?)` +7. `dbCancelOperation(operationId)` +3. legacy compatibility: + 1. `onDbSaveContentProgress(callback)` + 2. `removeDbSaveContentProgress()` + +`DatabaseService.saveXtreamContent(...)` now generates an `operationId`, +subscribes to `onDbOperationEvent`, filters by that `operationId`, and only +falls back to the legacy progress API if the newer event channel is missing. + +`DatabaseService` also owns: + +1. `createOperationId(...)` +2. `cancelOperation(operationId)` +3. `isDbAbortError(error)` + +## Migrated Operations + +The worker now owns all heavy non-EPG SQLite paths plus the remaining portal +state handlers that still used direct main-thread SQLite access. + +### Categories + +1. `DB_HAS_CATEGORIES` +2. `DB_GET_CATEGORIES` +3. `DB_SAVE_CATEGORIES` +4. `DB_GET_ALL_CATEGORIES` +5. `DB_UPDATE_CATEGORY_VISIBILITY` + +### Content + +1. `DB_HAS_CONTENT` +2. `DB_GET_CONTENT` +3. `DB_SAVE_CONTENT` +4. `DB_GET_CONTENT_BY_XTREAM_ID` +5. `DB_SEARCH_CONTENT` +6. `DB_GLOBAL_SEARCH` +7. `DB_GET_GLOBAL_RECENTLY_ADDED` + +### Playlist metadata + +1. `DB_CREATE_PLAYLIST` +2. `DB_UPSERT_APP_PLAYLIST` +3. `DB_UPSERT_APP_PLAYLISTS` +4. `DB_GET_APP_PLAYLISTS` +5. `DB_GET_APP_PLAYLIST` +6. `DB_GET_PLAYLIST` +7. `DB_UPDATE_PLAYLIST` +8. `DB_DELETE_PLAYLIST` +9. `DB_DELETE_ALL_PLAYLISTS` +10. `DB_GET_APP_STATE` +11. `DB_SET_APP_STATE` + +### Xtream refresh helpers + +1. `DB_DELETE_XTREAM_CONTENT` +2. `DB_RESTORE_XTREAM_USER_DATA` + +### Favorites + +1. `DB_ADD_FAVORITE` +2. `DB_REMOVE_FAVORITE` +3. `DB_IS_FAVORITE` +4. `DB_GET_FAVORITES` +5. `DB_GET_GLOBAL_FAVORITES` +6. `DB_GET_ALL_GLOBAL_FAVORITES` +7. `DB_REORDER_GLOBAL_FAVORITES` + +### Recently viewed + +1. `DB_GET_RECENTLY_VIEWED` +2. `DB_CLEAR_RECENTLY_VIEWED` +3. `DB_GET_RECENT_ITEMS` +4. `DB_ADD_RECENT_ITEM` +5. `DB_CLEAR_PLAYLIST_RECENT_ITEMS` +6. `DB_REMOVE_RECENT_ITEM` + +### Playback positions + +1. `DB_SAVE_PLAYBACK_POSITION` +2. `DB_GET_PLAYBACK_POSITION` +3. `DB_GET_SERIES_PLAYBACK_POSITIONS` +4. `DB_GET_RECENT_PLAYBACK_POSITIONS` +5. `DB_GET_ALL_PLAYBACK_POSITIONS` +6. `DB_CLEAR_PLAYBACK_POSITION` + +## SQLite Concurrency Rules + +EPG remains on its own worker, so both workers must use compatible SQLite +pragmas. + +Applied now in both the shared connection path and worker-owned connections: + +1. `foreign_keys = ON` +2. `journal_mode = WAL` +3. `busy_timeout = 5000` + +Current sources: + +1. `libs/shared/database/src/lib/connection.ts` +2. `apps/electron-backend/src/app/workers/database.worker-connection.ts` +3. `apps/electron-backend/src/app/workers/epg-parser.worker.ts` + +## UI Behavior Changes + +### Search + +Xtream search now guards against stale async responses: + +1. local playlist search uses a monotonically increasing request version +2. global search uses a separate request version in the dialog component +3. clearing search invalidates older pending results + +This prevents an older worker response from repainting over a newer query or a +cleared search state. + +### Busy states + +The UI now has explicit long-running state for destructive operations: + +1. recent playlist rows show row-level refresh/delete spinners +2. Xtream import overlay shows phase text and a cancel action +3. Xtream playlist rows show request-scoped progress and cancel actions +4. busy rows block repeat clicks while an operation is in flight +5. settings "remove all playlists" owns its own spinner/disabled state + +These changes matter because once SQLite work leaves the main thread, the +renderer can actually paint the loading state instead of freezing. + +## Build And Packaging Notes + +### Worker bundling + +`apps/electron-backend/build-worker.js` now bundles both: + +1. `epg-parser.worker.ts` +2. `database.worker.ts` + +The worker build also aliases: + +1. `database-schema` +2. `database-path-utils` + +These aliases avoid importing the shared database barrel from inside the worker, +which would otherwise pull in runtime code that assumes the main Electron +process environment. + +### Worker path resolution + +`DatabaseWorkerClient` resolves: + +1. development path from `__dirname` +2. packaged path from `process.resourcesPath/dist/apps/electron-backend/workers/...` +3. fallback packaged path from `path.dirname(app.getAppPath())` + +### Packaged artifact verification + +`tools/packaging/verify-electron-package-layout.mjs` verifies packaged worker +artifacts for: + +1. Linux unpacked resources +2. macOS app bundles +3. Windows unpacked app resources + +The script checks: + +1. `epg-parser.worker.js` +2. `database.worker.js` +3. `better-sqlite3` in one approved unpacked node_modules location + +### Development rebuild rule + +Worker-backed database logic is executed from the compiled bundle at: + +1. `dist/apps/electron-backend/workers/database.worker.js` + +Do not assume a source edit is active in the live app. If a fix touches: + +1. `apps/electron-backend/src/app/database/operations/` +2. `apps/electron-backend/src/app/workers/` +3. `apps/electron-backend/src/app/events/database/` +4. preload-backed DB methods consumed by the renderer + +then the safe workflow is: + +1. rebuild the worker bundle, or the full `electron-backend` target if preload, + main-process, or web output also changed +2. confirm the new `dist/` artifact exists or has a fresh timestamp +3. restart the Electron process +4. only then rerun CDP/manual checks or Electron E2E + +A running Electron app keeps using the worker bundle it already loaded at +startup. This is a common reason a worker fix appears "not working" in manual +verification even when the source patch is correct. + +## Testing + +### Unit coverage added + +`apps/electron-backend/src/app/services/database-worker-client.spec.ts` +covers: + +1. worker ready -> request -> response flow +2. event forwarding to a pending request +3. serialized worker error propagation +4. `AbortError` propagation for cancelled work +5. cancel message routing to the live worker +6. worker exit recovery and fresh worker startup + +`apps/electron-backend/src/app/events/epg.events.spec.ts` covers: + +1. shared worker bootstrap usage for the EPG worker +2. `nativeModuleSearchPaths` forwarding into workerData +3. actionable worker-path resolution failures + +`apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts` covers: + +1. packaged and development worker path resolution +2. packaged native-module search path ordering +3. aggregated native module resolution errors + +### Electron responsiveness coverage + +`apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts` covers: + +1. large Xtream import shows the overlay promptly +2. DB worker progress events advance during import +3. renderer animation frames continue while import/delete are in progress +4. large Xtream playlist delete shows row-level busy UI and completes cleanly + +`apps/electron-backend-e2e/src/electron-test-fixtures.ts` now also captures: + +1. `DB_OPERATION_EVENT` history in the renderer +2. a requestAnimationFrame counter for repaint assertions + +For deterministic E2E timing, tests may set: + +```bash +IPTVNATOR_DB_WORKER_BATCH_DELAY_MS=20 +``` + +This delay is test-only and disabled by default. + +### Useful verification commands + +```bash +pnpm exec jest --config apps/electron-backend/jest.config.ts --runInBand apps/electron-backend/src/app/services/database-worker-client.spec.ts apps/electron-backend/src/app/events/epg.events.spec.ts apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts +pnpm nx run electron-backend:build-worker +pnpm exec tsc -p apps/electron-backend/tsconfig.app.json --noEmit +pnpm exec tsc -p apps/web/tsconfig.app.json --noEmit +pnpm nx run electron-backend-e2e:e2e -- --project=electron --grep "Electron Xtream Responsiveness" +pnpm run verify:package-layout -- macos arm64 +pnpm run verify:package-layout -- linux +pnpm run verify:package-layout -- windows +``` + +### Electron runtime validation + +```bash +pnpm nx serve electron-backend +agent-browser --cdp 9222 tab list +agent-browser --cdp 9222 tab 1 +agent-browser --cdp 9222 snapshot -i -c -d 3 +pnpm run smoke:packaged -- macos arm64 +``` + +When worker-backed behavior changed, rebuild and restart before reconnecting: + +```bash +pnpm nx run electron-backend:build-worker +CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache +stat -f "%Sm %N" dist/apps/electron-backend/workers/database.worker.js +``` + +Then restart the Electron process and reconnect to `127.0.0.1:9222`. + +### Electron E2E troubleshooting + +If a production-mode Electron or Electron E2E launch shows `ERR_FILE_NOT_FOUND` +for hashed `chunk-*.js`, `main-*.js`, or `styles-*.css` assets: + +1. treat `dist/apps/web` as stale first +2. rerun a deterministic production build, for example: + +```bash +CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache +``` + +3. verify `dist/apps/web/index.html` uses `` before relaunching + Electron in file-backed mode + +## Current Limitations + +These are intentionally still out of scope for this first cut: + +1. request cancellation +2. moving network-heavy Xtream fetches off the current path +3. migrating every remaining small SQLite IPC handler to the worker +4. richer delete progress reporting for bulk destructive operations +5. repo-wide Angular/Jest cleanup for the currently failing web test baseline + +## Extending The Worker + +When adding another heavy SQLite operation: + +1. Put SQL-heavy logic in `apps/electron-backend/src/app/database/operations/`. +2. Add the channel name to `database-worker.types.ts`. +3. Handle it in `database.worker.ts`. +4. Proxy the IPC handler through `DatabaseWorkerClient`. +5. If the renderer needs progress, emit a request-scoped `DbOperationEvent`. +6. Reuse existing preload/service APIs where possible instead of creating a new + renderer-facing contract. +7. Re-run the worker unit test and at least one Electron runtime smoke. diff --git a/docs/architecture/stalker-epg.md b/docs/architecture/stalker-epg.md index bf25532a5..6b64298ee 100644 --- a/docs/architecture/stalker-epg.md +++ b/docs/architecture/stalker-epg.md @@ -16,7 +16,7 @@ The Stalker ITV live stream layout displays EPG data in the right panel when a l ``` ┌───────────────────────────────────────────────────────────────────────────┐ │ StalkerLiveStreamLayoutComponent │ -│ apps/web/src/app/stalker/stalker-live-stream-layout/ │ +│ libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/ │ │ │ │ ┌──────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │ │ │ Sidebar │ │ Video Player │ │ EPG Panel │ │ @@ -129,14 +129,14 @@ interface EpgItem { | File | Purpose | |------|---------| | `libs/shared/interfaces/src/lib/stalker-portal-actions.enum.ts` | `GetShortEpg`, `GetEpgInfo` enum values | -| `apps/web/src/app/stalker/stalker.store.ts` | `fetchChannelEpg()` method | -| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` | -| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `` integration | +| `libs/portal/stalker/data-access/src/lib/stalker.store.ts` | `fetchChannelEpg()` method | +| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` | +| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `` integration | | `libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts` | Shared EPG display component | ### StalkerStore.fetchChannelEpg() -Location: `apps/web/src/app/stalker/stalker.store.ts` (in `withMethods`) +Location: `libs/portal/stalker/data-access/src/lib/stalker.store.ts` (in `withMethods`) ```typescript async fetchChannelEpg(channelId: number | string): Promise diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index 748f9eeb6..a5e0ea1eb 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -5,6 +5,8 @@ This document describes the Stalker portal implementation in IPTVnator and where ## Related Docs - [Stalker Portal EPG Architecture](./stalker-epg.md) +- [Portal Detail Navigation](./portal-detail-navigation.md) +- [Embedded Inline Playback](./embedded-inline-playback.md) - [Remote Control Architecture](./remote-control.md) - [Download Manager](./download-manager.md) - [Category Management](./category-management.md) @@ -25,7 +27,7 @@ Stalker support covers: ## Routing Structure -Primary route tree lives in `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.routes.ts`. +Primary route tree lives in `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`. - `/stalker/:id/vod` - `/stalker/:id/series` @@ -45,23 +47,23 @@ Primary route tree lives in `/Users/4gray/Code/iptvnator/apps/web/src/app/stalke ## Main UI Components -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-main-container.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts` - Category + content layout for `vod` and `series` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` - ITV live playback, channel navigation, EPG panel integration -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts` +- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-series-view/stalker-series-view.component.ts` - Season/episode UI for all Stalker series modes -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` ## Store and Data Flow Stalker store is now feature-composed: -- Facade: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.store.ts` -- Feature slices: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/features/*` -- Shared store utils: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/utils/*` +- Facade: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker.store.ts` +- Feature slices: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stores/features/*` +- Shared helpers: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/*` Important store responsibilities: @@ -93,8 +95,8 @@ Stalker has multiple real-world data shapes. The current implementation supports Core decision logic and normalization are centralized in: -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/models/*.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/models/*.ts` ## Favorites and Recently Viewed @@ -109,16 +111,22 @@ Current implementation is shared via Stalker-specific helpers: Where this is used: -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts` -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` +- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts` + +Navigation rule to preserve: + +- Stalker favorites, recently viewed, and search stay in their current screen and open inline detail state. +- They should not redirect into a canonical content/category/item route because Stalker detail rendering is currently store-state/inline driven, not route driven. +- See [Portal Detail Navigation](./portal-detail-navigation.md). ## Remote Control Integration Stalker live remote control is implemented in: -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` Supported today: @@ -147,7 +155,7 @@ This reduces duplicate UI logic across portal types and keeps compatibility beha Focused regression tests for Stalker VOD mode branching live in: -- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.spec.ts` +- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts` Covered scenarios include: diff --git a/docs/architecture/stalker-store-api-baseline.md b/docs/architecture/stalker-store-api-baseline.md index 9d8af1868..cc7c641d5 100644 --- a/docs/architecture/stalker-store-api-baseline.md +++ b/docs/architecture/stalker-store-api-baseline.md @@ -1,12 +1,12 @@ # Stalker Store API Baseline -This is the compatibility baseline for refactoring `apps/web/src/app/stalker/stalker.store.ts`. +This is the compatibility baseline for refactoring `libs/portal/stalker/data-access/src/lib/stalker.store.ts`. Goal: keep this public surface stable while splitting to feature stores. ## Source of Truth -- Store implementation: `apps/web/src/app/stalker/stalker.store.ts` +- Store implementation: `libs/portal/stalker/data-access/src/lib/stalker.store.ts` - Baseline created on current branch state before feature-store extraction. ## Public State Signals @@ -108,9 +108,10 @@ Top observed store API usage in app code: Consumer directories sampled: -- `apps/web/src/app/stalker/**` -- `apps/web/src/app/xtream-electron/**` -- `apps/web/src/app/shared/**` +- `libs/portal/stalker/**` +- `libs/portal/xtream/feature/**` +- `libs/portal/catalog/feature/**` +- `libs/ui/components/**` ## Invariants to Preserve During Refactor @@ -123,4 +124,3 @@ Consumer directories sampled: - Full-portal auth path continues through `StalkerSessionService`. - Non-auth/simple path continues through `DataService.sendIpcEvent(STALKER_REQUEST, ...)`. - Resource-driven loading signals preserve existing names. - diff --git a/docs/architecture/workspace-dashboard.md b/docs/architecture/workspace-dashboard.md index 7dd98c6dc..0e8fd33e7 100644 --- a/docs/architecture/workspace-dashboard.md +++ b/docs/architecture/workspace-dashboard.md @@ -1,188 +1,136 @@ -# Workspace Dashboard Plan +# Workspace Dashboard -## Context +This document records the current dashboard implementation inside the workspace +shell. It replaces the earlier dashboard plan document. -The workspace shell is now the primary app entrypoint. The dashboard should evolve from a placeholder into an operational home page that helps users: +Related: -1. Quickly continue playback/work. -2. Switch context across M3U, Xtream, and Stalker sources. -3. Monitor relevant content (recent items, EPG, status) in one place. +- [Workspace Shell](./workspace-shell.md) -This document defines the implementation plan before Phase 1 work starts. +## Summary -## Status Snapshot (February 22, 2026) +- The dashboard is the default `/workspace` landing page. +- It is a widget-based surface with persisted layout and widget settings. +- The current implementation favors a constrained, stable layout over a full + freeform grid system. -### Delivered +Core implementation: -1. Dashboard route is active in workspace shell with persisted widget layout. -2. Widget host and configurable widget model are implemented. -3. `Customize` mode supports: - 1. Enable/disable widgets - 2. Reorder widgets (up/down) - 3. Widget scope (provider + playlist selection) -4. Active widgets in production: - 1. Continue Watching - 2. Recent Sources - 3. Source Statistics - 4. Recently Watched (global) - 5. Global Favorites -5. Recently Watched and Global Favorites support: - 1. Content-kind chips (channels/vod/series) - 2. List/grid toggle - 3. Direct deep linking from widget item to target view/detail -6. Xtream live deep links now auto-start playback when opened from dashboard widgets. -7. Dashboard now uses a shared Material-based widget shell for consistent visual structure. -8. Customize mode now supports drag-and-drop ordering and widget size presets (`1/3`, `1/2`, `2/3`, `full`). +1. `libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.ts` +2. `libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html` +3. `libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts` +4. `libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts` +5. `libs/workspace/dashboard/data-access/src/lib/dashboard-layout.service.ts` +6. `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts` -### Partially Delivered / Deviation From Initial Phase 1 List +## Current Widget Set -1. EPG Radar was intentionally removed from the current dashboard scope and is postponed. -2. Recent Activity / Recently Added widget was intentionally removed from current scope. +Registered widget types: -### Not Started +1. `source-stats` +2. `continue-watching` +3. `recently-watched` +4. `global-favorites` -1. Phase 4 external widgets (RSS/scores/news adapters). +Default layout: -### Immediate Next Widget Tasks (Recommended) +1. `continue-watching` + 1. Enabled by default + 2. Size `full` +2. `recently-watched` + 1. Enabled by default + 2. Size `two-thirds` + 3. Scope-aware +3. `global-favorites` + 1. Enabled by default + 2. Size `one-third` + 3. Scope-aware +4. `source-stats` + 1. Present in the registry + 2. Disabled by default + 3. Size `one-third` -1. Expand widget settings UX (scope presets, bulk provider toggles). +The old refactor summary mentioned `Recent Sources`, but that widget is not in +the current registry and should not be documented as shipped behavior. -## Product Direction +## Layout And Persistence Contract -The dashboard should be a configurable widget system, but introduced in stages: +The dashboard persists a versioned layout object in local storage. -1. Start with useful, stable widgets and a constrained layout. -2. Add edit/customization workflows after widget value is proven. -3. Add advanced drag/resize and external integrations later. +Current storage details: -This avoids building heavy layout mechanics before core data widgets are solid. +1. Storage key: `workspace-dashboard-layout-v3` +2. Schema version: `12` +3. Size presets: + 1. `one-third` + 2. `half` + 3. `two-thirds` + 4. `full` +4. Scope settings: + 1. `providers: Array<'m3u' | 'xtream' | 'stalker'>` + 2. `playlistIds: string[]` -## UX Direction +Normalization rules in `DashboardLayoutService`: -Design style: professional operator console (dense, calm, high-signal). +1. Stored widgets are merged against `DEFAULT_DASHBOARD_WIDGETS`. +2. Missing widgets are restored from defaults. +3. Removed or invalid settings fall back to normalized defaults. +4. Titles and descriptions come from the current code-defined defaults, not + stale stored values. +5. Widget order is reindexed after normalization. -Target layout: +## Rendering Flow -1. Top row: continue actions, recent sources, health/status. -2. Middle row: discovery widgets (recently viewed, recently added, favorites). -3. Edit mode: add/remove/reorder widgets and configure source scope. +1. `WorkspaceDashboardComponent` reads `DashboardLayoutService.state()`. +2. Enabled widgets are filtered and rendered in layout order. +3. `DashboardWidgetHostComponent` maps `widget.type` to a concrete widget + component. +4. Widgets receive the normalized config, including size and optional scope. +5. Data comes from `DashboardDataService` and existing provider/state services. +6. Widget actions deep-link back into workspace/provider routes. -## Architecture +## Customize Mode -## Core Components +Customize mode is part of the current product, not future work. -1. `DashboardPageComponent` (container/layout/edit mode orchestration) -2. `DashboardWidgetHostComponent` (widget factory/renderer by type) -3. `DashboardLayoutStore` (signal-based state for layout, settings, edit mode) -4. `DashboardPersistenceService` (save/load layout, version migration) -5. `DashboardDataFacade` (aggregate provider data for widgets) +Supported actions: -## Widget Contract +1. Toggle widget visibility. +2. Drag-and-drop reorder for visible widgets. +3. Change widget size within the fixed preset list. +4. Configure provider scope for scoped widgets. +5. Configure playlist scope for scoped widgets. +6. Reset the layout to defaults. -```ts -type WidgetSize = 'one-third' | 'half' | 'two-thirds' | 'full'; +Scope-aware widgets currently rely on a provider/playlist filter model rather +than per-widget custom query systems. -interface WidgetScope { - providers: Array<'m3u' | 'xtream' | 'stalker'>; - playlistIds?: string[]; -} +## UX Rules -interface DashboardWidget { - id: string; - type: string; - title: string; - size: WidgetSize; - order: number; - enabled: boolean; - scope: WidgetScope; - settings: Record; -} +1. Widgets should represent user tasks, not provider internals. +2. Each widget must own its loading, empty, and error states. +3. Dashboard failures must stay isolated to the widget that failed. +4. Widget navigation should resolve directly into the relevant content context. +5. New widgets should fit the existing constrained layout model unless the + dashboard architecture is explicitly being expanded. -interface DashboardLayout { - version: number; - widgets: DashboardWidget[]; -} -``` +## Adding Or Changing Widgets -## Capability Rules +Current workflow: -Widgets must degrade gracefully per provider: +1. Add or update the widget type in `dashboard-widget.model.ts`. +2. Implement the widget UI under `libs/workspace/dashboard/ui/src/lib/widgets/`. +3. Register the widget in `dashboard-widget-host.component.ts`. +4. Add default state and migration-safe behavior in + `dashboard-layout.service.ts`. +5. Extend `dashboard-data.service.ts` or reuse an existing feature service. +6. Ensure the widget has explicit empty/error/loading states and valid + workspace deep links. -1. If a source/provider does not support required data (for example EPG), show a clear empty/unsupported state. -2. Widgets never hard fail the page; each widget owns loading/error states. -3. Scope defaults to "all supported providers" unless user config overrides it. +## Deferred Work -## Phased Delivery +These items are intentionally not part of the current contract: -## Phase 1 (Now): Production Dashboard V1 - -Scope: - -1. Replace placeholder dashboard with real widget host + predefined layout. -2. Fixed grid slots (no free drag/resize yet). -3. Initial widgets: - 1. Recent Sources - 2. Continue Watching - 3. Source Statistics - 4. Recently Added / Recently Viewed (provider-aware) -4. Persist enabled/disabled and order (simple list reorder if needed). - -Out of scope: - -1. Freeform drag-and-drop grid resizing. -2. External data providers (RSS/sports/news). -3. Full widget marketplace. - -Acceptance criteria: - -1. Dashboard loads with useful content for at least one active source type. -2. Empty states are clear and actionable (links to Sources/settings). -3. No route regressions for existing workspace sections. -4. Layout/settings survive app restart. - -## Phase 2: Edit Mode and Configuration - -Scope: - -1. "Customize dashboard" mode. -2. Enable/disable widgets. -3. Widget-level source scoping (providers + selected playlists). -4. Order management (move up/down or drag reorder within constrained grid). - -## Phase 3: Advanced Layout (Drag/Resize) - -Scope: - -1. True grid layout manager with size presets and drag repositioning. -2. Collision handling and responsive breakpoint behavior. -3. Optional "reset layout" and preset templates. - -## Phase 4: External Widgets - -Scope: - -1. Adapter interface for non-playlist data widgets (RSS, scores, news). -2. Polling/cache strategy with rate limits. -3. User opt-in and failure isolation per external source. - -## Data and Performance Notes - -1. Use memoized/computed selectors for widget inputs. -2. Avoid redundant provider fetches; reuse existing stores/services where possible. -3. Update widgets incrementally and isolate heavy computations in facade/store utilities. - -## File/Module Placement (Proposed) - -1. `libs/workspace/dashboard/feature/` (page + edit mode orchestration) -2. `libs/workspace/dashboard/ui/` (widget host + widget components) -3. `libs/workspace/dashboard/data-access/` (state model + persistence + data facade/service) -4. Route integration remains in `apps/web/src/app/app.routes.ts` and workspace shell. - -## Open Questions - -1. Should dashboard layout be global per app profile, or per active workspace/provider mix? -2. Which widgets are enabled by default for new users vs migrated users? - -## Next Step - -Start Phase 1 implementation using this document as the source of truth and track deviations explicitly in follow-up updates. +1. Freeform drag/resize grid with collision management. +2. External data widgets such as RSS, sports, or news adapters. +3. A widget marketplace or plugin system. diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md new file mode 100644 index 000000000..c0a92f304 --- /dev/null +++ b/docs/architecture/workspace-shell.md @@ -0,0 +1,128 @@ +# Workspace Shell + +This document records the current workspace-first shell contract. It is the +stable replacement for the older UI refactor summary. + +Related: + +- [Workspace Dashboard](./workspace-dashboard.md) + +## Summary + +- `/workspace` is the primary app surface. +- `WorkspaceShellComponent` owns the persistent frame: rail, header, optional + context panel, content outlet, and external playback footer. +- Descendant workspace pages inherit `layout = 'workspace'` from the + `/workspace` root route. +- Provider route trees now bootstrap through route-scoped session providers + instead of nested provider shell components. + +Core implementation: + +1. `apps/web/src/app/app.routes.ts` +2. `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts` +3. `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html` +4. `libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts` +5. `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts` +6. `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts` + +## Route Contract + +Current workspace routes: + +1. `/` -> `/workspace` +2. `/workspace` -> `/workspace/dashboard` +3. `/workspace/dashboard` +4. `/workspace/sources` +5. `/workspace/playlists/:id/:view` +6. `/workspace/global-favorites` +7. `/workspace/downloads` +8. `/workspace/settings` +9. `/workspace/xtreams/:id/...` +10. `/workspace/stalker/:id/...` + +Compatibility redirect: + +1. `/settings` -> `/workspace/settings` + +Provider route integration: + +1. `apps/web/src/app/app.routes.ts` marks the `/workspace` root route with + `data.layout = 'workspace'`. +2. `isWorkspaceLayoutRoute(...)` treats that layout marker as inherited route + state for all descendants. +3. Xtream and Stalker parent routes attach route-scoped session providers that + bootstrap the active playlist, sync provider section state, and clean up + provider-local state when the route is destroyed. +4. Workspace routes no longer rely on nested provider shell components for + hidden local chrome. + +## Shell Structure + +The shell is intentionally split into four persistent regions: + +1. Left rail: + 1. Static workspace links for dashboard and sources. + 2. Provider-aware context links derived from the active or current playlist. +2. Top header: + 1. Playlist switcher. + 2. Route-aware search input and command palette trigger. + 3. Add source action. + 4. Global favorites shortcut. + 5. Downloads shortcut in Electron. + 6. Context actions menu for playlist/account or section-level actions. +3. Main body: + 1. Optional left context panel. + 2. Main router outlet content. +4. Optional footer: + 1. External playback session bar when a docked session is visible. + +## Context Panel Rules + +The shell decides which secondary panel to show from the current route: + +1. `/workspace/sources` + 1. `WorkspaceSourcesFiltersPanelComponent` +2. Xtream category sections (`live`, `vod`, `series`) + 1. `WorkspaceContextPanelComponent` +3. Stalker category sections (`itv`, `vod`, `series`) + 1. `WorkspaceContextPanelComponent` +4. `/workspace/settings` + 1. `WorkspaceSettingsContextPanelComponent` +5. Downloads sections + 1. `WorkspaceCollectionContextPanelComponent` + +The context panel is part of the shell contract. New workspace-level routes +should explicitly decide whether they need one rather than adding local +sidebars inside feature pages. + +## Search And Navigation Rules + +Search is shell-owned and route-aware: + +1. Disabled on settings routes. +2. Enabled on sources routes. +3. Enabled for supported Xtream and Stalker content/search views. +4. Placeholder text and search handling vary by provider and section. +5. Input changes are debounced before route/store updates are applied. + +Rail navigation is also shell-owned: + +1. Workspace-global entries are static. +2. Provider entries come from `buildPortalRailLinks(...)`. +3. On dashboard, sources, settings, and global favorites, the shell falls back + to the currently selected playlist so provider navigation remains available + even outside a provider route. + +## Maintenance Guidance + +Use this document as the source of truth when changing workspace shell behavior. + +1. New top-level user destinations should default to child routes under + `/workspace`. +2. Shared provider navigation logic belongs in portal-shared util/UI libraries, + not duplicated inside the shell. +3. If a provider route changes how playlist/session bootstrap works, update the + route-session provider and shell-facing route contract together. +4. Historical migration notes, cleanup lists, and one-off refactor steps should + stay out of this file; track them in issues or PR notes instead. diff --git a/docs/architecture/xtream-mock-server.md b/docs/architecture/xtream-mock-server.md index 19bade2ff..003e46ee4 100644 --- a/docs/architecture/xtream-mock-server.md +++ b/docs/architecture/xtream-mock-server.md @@ -25,6 +25,7 @@ faker.seed(seed) ←── all faker calls use same seed per credentia generateCategories() ←── live / vod / series categories │ generateLiveStreams() ←── live TV stream list + scenario EPG fixture? ←── optional deterministic per-stream EPG override generateVodStreams() ←── VOD movie list generateSeriesItems() ←── series list generateSeriesInfo() ←── nested seasons + episodes (pre-populated) @@ -57,6 +58,7 @@ apps/xtream-mock-server/ ├── handlers/ │ ├── get-account-info.handler.ts │ ├── get-categories.handler.ts ← live/vod/series categories + │ ├── get-full-epg.handler.ts ← full EPG + legacy typo alias │ ├── get-streams.handler.ts ← live/vod/series stream lists │ ├── get-vod-info.handler.ts │ ├── get-series-info.handler.ts @@ -159,6 +161,31 @@ All redirect to a publicly available HLS test stream Note: `title` and `description` are **base64-encoded**, matching the real Xtream API. +### `get_simple_data_table` / `get_simple_date_table` + +Both actions return the same full per-channel schedule shape: + +```json +{ + "epg_listings": [ + { + "id": "10000-current", + "epg_id": "channel-10000.mock", + "title": "base64encodedTitle", + "description": "base64encodedDescription", + "start": "2026-04-05 04:30:00", + "end": "2026-04-05 05:00:00", + "start_timestamp": "1775363400", + "stop_timestamp": "1775365200", + "channel_id": "channel-10000.mock" + } + ] +} +``` + +The legacy `get_simple_date_table` typo alias exists because real Xtream panels +sometimes only respond to that misspelled action. + ### `get_series_info` (structure) ```json @@ -193,10 +220,24 @@ Note: `title` and `description` are **base64-encoded**, matching the real Xtream | `large:large` | 9999 | 20 each | 200 | active | | `series:series` | 2002 | live:3, vod:4, series:15 | 30 | active | | `minimal:minimal` | 3003 | 2 each | 5 | active | +| `epg:epg` | 6006 | live:2, vod:1, series:1 | 3 | active | | `expired:expired` | 4004 | 4 each | 10 | Expired | | `inactive:inactive` | 5005 | 4 each | 10 | Disabled | | `` | hash | 6 each | 30 | active | +### `epg:epg` fixture details + +This scenario is reserved for Xtream EPG tests: + +- live category `EPG Focus` contains deterministic channels such as `Timezone News` +- `Timezone News` serves a fixed `get_short_epg` window and a full `get_simple_data_table` schedule +- the full schedule includes a program that spans a UTC midnight boundary and another program after midnight +- raw `start` / `end` strings are intentionally offset from `start_timestamp` / `stop_timestamp` + +That deliberate mismatch lets Electron tests verify the renderer uses timestamp +fields for sorting, current-program selection, progress bars, and local clock +labels instead of trusting provider-local strings. + --- ## Playwright Integration @@ -233,5 +274,6 @@ await page.route('**/localhost:3000/xtream**', async (route) => { - **Add new actions**: Implement a handler function and add a `case` in `routes/dispatch.ts` - **Add new scenarios**: Add an entry to `SCENARIOS` in `scenarios.ts` +- **Add deterministic EPG fixtures**: Extend `ScenarioConfig.epgFixture` and populate `epgListingsByStreamId` in `data-store.ts` - **Adjust data volume**: Change `itemsPerCategory`, `seasonsPerSeries`, or `episodesPerSeason` per scenario - **Custom stream URLs**: Edit the HLS stub redirect in `main.ts`