The timeline declared a `selectedDate` input but never read it: `viewDayKey`
seeded to today and only local `commitDay` navigation mutated it, so a
programmatic host change was ignored and a preserved non-today date reset to
today on (re)mount — the panel showed today/empty-day while the host pointed at
another day (the old EpgList honored the controlled input). Flagged by Codex.
Seed `viewDayKey` from `selectedDate` via `linkedSignal` so it initializes from
and follows the host, while local day navigation still overrides via `.set()`.
Steady state is unchanged: the host echoes back the emitted key, so the signal
recomputes to the same value (no loop, no flicker).
Regression coverage: two specs assert a controlled non-today `selectedDate`
renders the ribbon on that day and follows a programmatic change. ui-epg 80,
portal-shared-ui + playlist-m3u green; lint clean; component 399 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the file-size review finding and prepare for a future EPG list view by
extracting the view-agnostic pieces out of the 538-line timeline component and
411-line utils file; both are now well under the line ceiling and the shared
building blocks are re-exported from `@iptvnator/ui/epg`.
- `epg-timeline.utils.ts` (227) keeps axis/blocks/date helpers; the
short-programme render strategy (tiers, grouping, zoom bounds) moves to
`epg-timeline-render.util.ts` (re-exported for API compatibility).
- `epg-archive.util.ts` — pure catch-up gating (`isWithinArchiveWindow`,
`canCatchUpProgramme`, `epgDialogActionFor`) off `when`/`startMs` primitives.
- `epg-summary.util.ts` — `EpgTimelineSummary` + collapsed-summary progress maths.
- `epg-programme-dialog.service.ts` — `EpgProgrammeDialogService`, opens the
shared details dialog and returns the chosen action.
- `epg-timeline-scroll.controller.ts` — `TimelineScrollController` owns ribbon
scrolling + channel-select auto-focus (with its migrated spec).
Behaviour unchanged. epg-timeline.component.ts 538→393, epg-timeline.utils.ts
411→227. Tests: ui-epg 78 (10 suites), portal-shared-ui 47, playlist-m3u 30;
lint + web build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`epgArchiveDays` used `tvg.rec ?? timeshift ?? catchup.days`, but `??` does not
skip an empty string — the common `tvg-rec=""` default stopped the chain and
yielded 0, which the timeline treats as an unbounded catch-up window, so a
channel with a real `timeshift`/`catchup-days` could offer "Watch" on
programmes older than its actual archive. Use the shared `getM3uArchiveDays`
(non-blank + correct precedence), matching the unified live tab. (Codex P2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auto-focus no longer forces the view back to today on every ribbon
(re)mount: it skips without clearing `lastFocusKey`, and only commits today
when today actually has a programme. Fixes being trapped on the empty-day
state when navigating to a day that has data (Codex P2).
- Pass `archiveDays` to the timeline from the unified live tab (via the shared
`getM3uArchiveDays`), so M3U catch-up "Watch" is gated to the channel's real
window instead of the unlimited `0` default (Codex P2).
- Never fold the on-air programme into a "N short" group chip, so its
`when: 'now'` highlight (and the auto-focus target) survive when zoomed out
(Greptile P2).
- Chunk the channel candidate lookup (500 keys/query) so the bound-parameter
count stays under SQLite's limit on large playlists — the case-insensitive
raw+lower match binds each key up to 4× (Greptile P2).
Tests: ui-epg 78, electron-backend 433, portal-shared-ui 47; lint + web build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the vertical EPG list/panel with a single horizontal timeline ribbon
(`app-epg-timeline`) shared across all live surfaces — the M3U player, the
unified live tab, and the Xtream and Stalker layouts. It preloads a multi-day
window and supports scroll, zoom, day navigation, short-programme tiers and
per-state empty/error views; the programme-details dialog is restyled to match
and now shows the channel logo.
Timeline / UI
- Auto-focus the currently-airing programme on channel select — instant
(`behavior: 'auto'`), centred, and deduped by programme-set identity so the
30s tick / zoom / re-emits never cause flaky re-jumps.
- Wide-tier cards show a dimmed description preview under the title; title and
preview clip on whole lines (`flex-shrink: 0` + `-webkit-line-clamp`), never
mid-line.
- Dynamic bar subtitle: the now-playing / archive programme title instead of a
static "Timeline" label.
- State-aware toolbar: hide Now + zoom when there is no ribbon, and the date
stepper when the channel has no EPG on any day (hidden, not disabled).
- Empty-state fills and centres within the panel area instead of a fixed
min-height that overflowed the compact inline panel and hid the buttons.
EPG current-programme lookup (electron-backend)
- Timezone-aware "airing now" comparison via SQLite `datetime()` (was a lexical
string compare that broke for offset vs UTC `Z` timestamps).
- Unscoped fallback for resolved candidates whose programmes are tagged with an
out-of-scope source (multi-source imports sharing XMLTV ids), so the sidebar
matches the timeline.
- Match channel candidates by the raw (case-sensitive) key as well as `LOWER()`,
since SQLite `LOWER()`/`COLLATE NOCASE` fold only ASCII — fixes Cyrillic /
Greek channel names showing "No program information available".
- Overlap-based day membership (`hasProgramsForDateKey`) so a film spanning
midnight keeps the ribbon on "today" while it airs.
Refactor
- Extract the duplicated channel-list EPG enrichment (progress + side-car map)
into `channel-list-container/epg-enrichment.util.ts`, shared by the
all-channels / groups / favorites / recent views; also fixes recent's and
favorites' unguarded, NaN-prone progress maths.
Tests: ui-epg 75, components 77, electron-backend 432; lint + web build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(m3u): highlight active archive epg program
* Store the active archive EPG program with the resolved playback URL so the clicked M3U catch-up row stays highlighted during archive playback.
* Show archive playback state and return-to-live controls in the inline player panel.
* Hide duplicate EPG program rows that share the same time slot while keeping the richer provider entry.
Register an "Switch player to Embedded MPV" command in the Cmd+K command
palette so the embedded MPV player can be activated like the other players.
Visibility is gated on an async getEmbeddedMpvSupport() check, mirroring the
Settings dropdown so the command only appears when embedded MPV is usable.
Generalizes the per-command visibility flag from desktopOnly to a `requires`
discriminator ('none' | 'managed-external' | 'embedded-mpv').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix Stalker route initialization so full portal sessions load auth-capable playlist data before category/content requests. Add regression coverage for full playlist lookup, empty lookup fallback, fast-path explicit portal metadata, and authenticated request routing.
Adds IMDb rating sort and minimum-rating filtering for Xtream VOD/series catalogs, consolidates refinement controls, and guards rating controls away from Live TV.
- save Xtream playlist details through browser-safe metadata persistence in PWA\n- keep PWA Xtream data source cache in sync with current playlist metadata\n- cover dialog close timing, stale cache, and PWA data-source bootstrap regression
* fix(security): harden Electron IPC against MITM, SSRF, path and injection risks
S1 TLS: validate certs by default on playlist/EPG fetches (opt-out via IPTVNATOR_ALLOW_INSECURE_TLS); new util/secure-https.ts.
S2: write-file IPC restricted to save-dialog-authorized paths.
S3: XTREAM_PROBE_URL guarded by assertRemoteUrlAllowed + maxRedirects:0; new events/url-safety.ts (+19 tests).
S4: EPG titles rendered via interpolation, not [innerHTML].
S5: downloads reveal/play limited to recorded download paths.
S6: Stalker cmd encoded (slash-preserving) to block query injection.
EPG-worker and Stalker fetches reject file://-style/credentialed URLs; LAN/self-hosted targets remain allowed.
* perf(player): lazy-load web video players via @defer
Wrap Video.js/HTML5/ArtPlayer in @defer (on immediate) so video.js, hls.js,
artplayer and mpegts.js split into a deferred chunk loaded on first playback
instead of eagerly on the player route. Embedded MPV (native) stays eager.
Spec uses DeferBlockBehavior.Playthrough.
* fix(player): remove leaked HTML video listeners on destroy
volumechange used a mismatched removeEventListener reference, while
loadedmetadata and timeupdate were never removed at all. Bind all three to
stable handler fields used for both add and remove, and add a teardown
regression test asserting each listener is detached on destroy.
* refactor(dashboard): extract pure navigation helpers from DashboardDataService
Move the 8 stateless link/navigation-state/type-kind helpers into a new
dashboard-navigation.util.ts so the routing logic is independently testable and
the 1260-line god-service shrinks. DashboardDataService keeps the public methods
as thin delegators (facade) so the public API and the single consumer
(workspace-dashboard-rails) are unchanged. First slice of the DashboardDataService
decomposition; verified by the existing service spec (33/33) and the app typecheck.
* fix(review): address PR feedback (IPv6 link-local, write-path cap, @defer placeholder)
- url-safety: broaden IPv6 link-local detection to the full fe80::/10 range
(fe80:: through febf::), not just the fe80:: prefix (+ regression tests).
- playlist.events: cap authorizedWritePaths (evict oldest past 32) so a save
dialog opened without a following write cannot accumulate entries until restart.
- web-player-view: add a @placeholder to each @defer (on immediate) player block
to avoid the one-frame blank/layout-shift before the chunk resolves.
* fix(security): close Electron network and download gaps
* test(downloads): cover cancellation and restart cleanup
* fix(downloads): address Greptile review gaps
* test(security): reproduce remaining Greptile findings
* fix(security): close remaining Greptile findings
* test(downloads): reproduce early database queue stall
* fix(downloads): release queue after setup failures
* test(downloads): reproduce completion queue stall
* fix(downloads): release queue after completion failures
* feat(ui): add custom title bar window controls for Windows and Linux
Hide the native title bar on win32/linux (titleBarStyle: 'hidden', frame
untouched so native resize borders and snapping keep working) and render
minimize / maximize-restore / close buttons in the renderer, mirroring the
existing macOS traffic-light setup.
- New WINDOW:* IPC contract (minimize, toggle-maximize, close, get-state)
handled in window.events.ts, resolved from the sender WebContents;
close goes through win.close() so window-bounds persistence still runs.
- WINDOW:STATE_CHANGED pushed on maximize/unmaximize/fullscreen so the
maximize/restore glyph stays correct for OS-triggered changes; controls
hide while fullscreen.
- WindowControlsComponent mounts once in app-root as a manual popover so
it stays in the browser top layer above CDK overlays (dialogs,
multi-EPG) - same behavior as macOS traffic lights.
- Theme-aware via CSS vars (--app-on-surface, --app-hover-overlay);
Windows-red close hover. Drag regions get right padding through a
body-level frameless-platform class.
- Gated by RuntimeCapabilitiesService.usesCustomWindowControls; PWA and
macOS never mount the controls.
Includes unit specs for the component and IPC handlers, an Electron E2E
suite (window-controls.e2e.ts), and a window-chrome section in
docs/architecture/workspace-shell.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(linux): upgrade Electron to 41 for frameless window decorations on Wayland
With the native title bar hidden, Linux windows lost the WM-drawn shadow
and rounded corners. Electron draws client-side decorations only on
native Wayland, and frameless-window CSD (GTK drop shadow + extended
resize boundaries) landed in Electron 41 - before that, frameless
windows render as plain rectangles.
- electron ^39.8.5 -> ^41.7.2 (Wayland auto-detected since 38.2; X11
sessions remain undecorated, matching other frameless Electron apps;
Windows keeps its DWM shadow and rounded corners).
- better-sqlite3 pinned to exactly 12.9.0: the last release shipping
prebuilt binaries for both Node 20 (ABI 115, Jest) and Electron 41
(ABI 145, runtime). 12.10.0 dropped the Node 20 prebuilds, forcing a
from-source build that fails without a C++ toolchain.
- pnpm override node-abi 3.85.0 -> 3.92.0 so electron-builder
install-app-deps can map Electron 41 to ABI 145.
Reviewed Electron 40/41 breaking changes: only the renderer clipboard
deprecation, which this app does not use.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(e2e): address review feedback and window-managerless Linux CI
- Skip the three window-manager-dependent E2E assertions (maximize
toggle, main-process state sync, minimize) on Linux CI: GitHub's
ubuntu runners drive Electron under xvfb without a window manager, so
maximize/minimize state never materializes there. Windows CI and
local Linux/macOS runs keep the coverage.
- WINDOW:TOGGLE_MAXIMIZE now returns the requested state instead of
re-reading isMaximized() right after the call, which races on Linux
window managers where maximize()/unmaximize() complete
asynchronously; the WINDOW:STATE_CHANGED push stays authoritative.
- Skip attaching window-state push listeners on macOS, where the
custom controls never mount and the IPC traffic had no subscriber.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): gate custom window controls on the full bridge surface
Include getWindowState and onWindowStateChange in the
usesCustomWindowControls capability check — the controls rely on both
for initial state and for keeping the maximize/restore glyph in sync
with OS-triggered changes, so a partial bridge should not mount them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(xtream): open correct channel and category from Ctrl+F live search
Three bugs prevented a live channel clicked from global search (Ctrl+F)
from playing and highlighting correctly in the sidebar:
1. Component reuse on same-route navigation: when the user was already
on the /live route, Angular reused LiveStreamLayoutComponent without
re-running the constructor, so openXtreamLiveItemId was never read
from history state. Fixed by subscribing to NavigationEnd and calling
checkPendingAutoOpenFromState() on every navigation.
2. Early effect firing with wrong content type: the auto-open effect
could fire before syncRouteState set selectedContentType to 'live',
causing the channel to be searched in VOD streams, not found, and
the pending ID cleared. Fixed by guarding on selectedContentType()
=== 'live' before processing.
3. Category filter blocking channel lookup: getVisibleChannels() only
returned channels in the currently selected category, so a channel
from a different category was never found. Fixed by switching to
getAllLiveStreams() (raw liveStreams signal) for the auto-open lookup.
Also sets selectedCategoryId to the opened channel's category so the
sidebar scrolls to the right category and highlights the channel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(xtream): address PR review feedback on live search auto-open
- Remove redundant constructor call: rely solely on NavigationEnd
subscription to read openXtreamLiveItemId from history state, which
fires after component creation for both initial and re-navigation
- Add explicit setSelectedItem call after auto-open so EPG loading and
remote-control status reflect the playing channel independently of
the constructStreamUrl side-effect
- Add 6 regression tests covering: initial NavigationEnd, category
highlighting, content-type guard, lazy liveStreams loading, missing
channel, and component-reuse re-navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>