docs: refactor and document architecture for Stalker and Workspace components

- Moved Stalker-related components and services from apps/web to libs/portal for better modularity.
- Introduced SQLite DB Worker to handle non-EPG database operations, improving UI responsiveness.
- Updated documentation for the Workspace Dashboard and Shell, detailing current implementation and routing structure.
- Added new EPG fixture scenarios to the Xtream mock server for testing purposes.
- Enhanced the overall architecture documentation to reflect recent changes and improvements.
This commit is contained in:
4gray committed 2026-04-05 19:05:07 +02:00
1 parent c42ddeaef2
commit d366672506
16 files changed
+1493 -200

No files matched your search

+25 -10
View File
@@ -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
+1 -1
View File
@@ -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**
@@ -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.
+97
View File
@@ -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.
@@ -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.
+2 -2
View File
@@ -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
+119
View File
@@ -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`
@@ -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
+4 -4
View File
@@ -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:
+479
View File
@@ -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 `<base href="./">` 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.
+5 -5
View File
@@ -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` | `<app-epg-view>` 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` | `<app-epg-view>` 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<EpgItem[]>
+26 -18
View File
@@ -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:
@@ -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.
+102 -154
View File
@@ -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<string, unknown>;
}
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.
+128
View File
@@ -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.
+42
View File
@@ -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 |
| `<any other>` | 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`