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`