mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
9455e0db6550ecbf2c46da346e762167d848df29
123
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5b2eb515d1 |
feat(downloads): track live-TV recordings in the download manager (#1452)
* feat(downloads): track live-TV recordings in the download manager Embedded MPV recordings were written to disk and forgotten: no list, no reveal/play, no missing-file handling, and the channel/EPG context was lost the moment the recording stopped. Recordings now live beside downloads: - New `recordings` table (no unique index, no playlist FK — recordings survive source deletion; playlist name stored via playlistDisplayLabel). - EmbeddedMpvRecordingTracker persists the lifecycle: start/stop hooks plus a session-snapshot observer for implicit stops (stream-replacement auto-stop, frame-copy helper crash, session error/close); startup repair turns rows a hard kill left behind into playable `interrupted` partials. - Channel/EPG metadata is captured at recording START in all four live hosts (M3U, Xtream, Stalker ITV, unified live tab); a clean stop triggers renderer-side enrichment with every program overlapping the recorded window, keyed by target path — covering recordings that span a program boundary. Provider EPG never reaches SQLite, so post-hoc lookup is impossible by design. - Own RECORDINGS_* IPC surface + RECORDINGS_UPDATE_EVENT ping and a separate supportsRecordings capability gate (the supportsDownloads allowlist is all-or-nothing and stays untouched). Reveal/play shell IPCs are gated on the recordings table, so the renderer-supplied recording directory stays a write-location preference, not a shell-access grant. - Manager UI: `recording` filter chip, "Recording now" queue section (REC pulse, elapsed, live file size — no percentage, the length is unknown), 16:9 channel-logo Recordings library, Needs attention with Remove only, focused detail at /workspace/downloads/recording/:recordingId. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): close the stop-enrichment race and repair player stubs Greptile spotted a real ordering bug: the stop IPC returns as soon as mpv acknowledges, while the recording row's terminal-state update is still queued in the tracker. The renderer answers that snapshot with stop enrichment, whose handler only accepts a terminal row — so the covered-program metadata could be silently dropped with "Recording not found". - EmbeddedMpvRecordingTracker.whenSettled() exposes the serialized write chain; RECORDINGS_UPDATE_PROGRAMS awaits it before the terminal-row lookup. Regression covered from both sides: the handler must not touch the database until the barrier resolves, and the barrier must imply a committed row. CI also caught spec stubs that had not learned the new player inputs (my local run-many had been an Nx cache hit, so the failures only surfaced in CI): - Teach the `app-web-player-view` and `app-embedded-mpv-player` stubs the `recordingMetadata` input and `recordingStopped` output across the m3u, Xtream, Stalker, unified-live-tab and web-player-view specs. - The races spec now asserts the metadata argument explicitly instead of matching a two-argument call. - Extract the Stalker and unified-live-tab spec stubs into sibling `*.spec-stubs.ts` files (the pattern ui/playback already uses) so both specs stay under the 1200-line test limit without shaving assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(downloads): make the recordings events spec a module The spec deliberately has no static imports — every dependency is swapped through jest.doMock before the harness's dynamic import — which also made it a TS script rather than a module, so its top-level `registeredHandlers` landed in the global scope and collided with the same-named const in stream-probe.spec.ts (TS2451). Local per-project runs compile the specs separately and stayed green; only the Tier A coverage suite builds them into one program, so CI caught it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): address Codex review on recording lifecycle Four findings from the Codex review, all real: - P1: `addon.stopRecording()` only dispatches — native-view uses `mpv_set_property_async`, frame-copy writes a helper command — so finalizing inside the stop hook could stat a file mpv had not flushed and even unlink bytes still being written. The tracker now treats the hook as a request and finalizes on the acknowledged inactive snapshot, with a 10 s bound so a lost acknowledgement cannot strand the row. Only a recording that never went active has its empty reservation removed. Stop enrichment follows through `whenFinalized(targetPath)` (bounded) instead of merely draining the write queue. - Live file size: `file_size_bytes` is written at finalization only, so the manager's 15 s refresh reported nothing while recording. Active rows are now decorated with a current `fs.stat` size. - Manager-initiated Stop bypassed both player stop paths, so recordings spanning program boundaries kept only the start-time program. `EmbeddedMpvPlayerComponent` now owns the active→inactive edge and emits `recordingStopped` for every trigger; the adapter and legacy toggle no longer emit it themselves. - Startup recovery could terminate a row another live instance was still writing under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES. Rows carry `owner_pid` and recovery skips those whose owner process is alive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): derive the enrichment wait from the stop fallback Greptile caught the seam my previous fix left: the enrichment barrier waited 5 s while the tracker's acknowledgement fallback only finalizes at 10 s, so a stop mpv never confirms let the terminal-row lookup expire early and drop the covered programs with no retry — precisely the case the fallback exists for. The wait is now derived from the acknowledgement bound (fallback + 1 s), with a regression test that fails if the two ever drift apart again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): address the second Codex pass on recordings Four more findings, all real: - P1 (macOS native-view): `StopRecording` clears `recordingActive` *before* dispatching the async property set and restores it if the request is rejected, so the first inactive snapshot is optimistic, not an acknowledgement — the tracker could finalize (and stat) a file mpv was still writing, and a rejected stop would leave the row `completed` while recording continued. An inactive snapshot now has to survive a 1.5 s settle window (three poll cycles); a revived recording cancels the pending finalization. - Removing a failed row unlinked its path unconditionally, which takes the file of a newer recording that reused the freed name within the same timestamp second. The cleanup now runs only while no other row claims it. - The All chip and the header's active badge ignored recordings, so a manager holding only recordings read "All 0" and an active recording never showed up in the badge. - Switching channels auto-stops the recording, but by the time the host handled the stop its `activeChannel`/EPG already described the NEW channel, so the old recording was enriched with the wrong schedule (and an unrelated program could be promoted to its title). The stop event now carries the EPG key captured while the recording was active and every host compares it before enriching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): close the persistence race and two recording UX gaps - Greptile P1: the enrichment deadline (fallback + 1 s) still raced the terminal write — if the tracker queue or the UPDATE took longer than the remaining margin, `whenFinalized` returned while the row was still `recording` and the one-shot enrichment was dropped. The deadline now bounds only the wait for mpv; `finalize()` removes the entry synchronously, so once it has started the wait follows the write itself. - Codex: `RECORDINGS_STOP` ignored `owner_pid`. Session ids restart per process, so under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES stopping another instance's row could stop an unrelated local recording. Foreign rows are now refused. - Codex: the In progress chip counted active recordings while its filter deliberately hid them, so clicking it showed "no matches". Active recordings now belong to that filter — a chip whose count disagrees with its page is a lie. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(downloads): drop the enrichment barrier instead of tuning it Three review rounds circled the same class: synchronizing mpv's asynchronous stop acknowledgement with a one-shot program enrichment. Each fix moved the deadline (5 s → fallback+1 s → wait-on-the-write) without removing the reason a deadline existed at all — the handler insisted on a *terminal* row. It never needed one. `openSync('wx')` makes the reserved path exclusive while a recording owns it, so the newest row for that path IS the recording that was stopped, and `finalize()` writes only status/end time/size and never `programs_json`. Enrichment and finalization are therefore order-independent: - `RECORDINGS_UPDATE_PROGRAMS` matches the newest row for the path in any status and awaits only the tracker's write queue, which exists solely to guarantee the INSERT committed (a recording stopped milliseconds after it started). - `whenFinalized`, its deadline constant, and the per-entry finalized promise are gone; the tracker keeps only the settle window and fallback that make *finalization* itself correct. No behavior is lost and the whole timing class disappears with the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): bind recording finalization to its entry and shield live rows from startup repair Two races from the Codex review: - Tracker timers finalized by reusable session id, so a stop followed by an immediate restart on the same session let the old settle timer finalize the NEW row (marked completed while mpv kept writing) and strand the old row in 'recording'. Finalization is now bound to the exact open entry, and replacing a session's entry arms the old entry's settle timer so an unobserved stop still finalizes it. - reconcileStaleRecordings() runs after the renderer is interactive; a recording started during bootstrap has ownerPid === process.pid and was repaired to interrupted/failed mid-write. Recovery now skips rows the tracker reports as actively tracked (activeRowIds()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): harden recording startup repair against recycled pids and stale renderer lists Second Codex pass on the recovery path: - A live ownerPid alone no longer shields a row: after a crash the OS can recycle the pid for an unrelated process, which would park the row in 'recording' with no instance able to finalize it. Recovery now also checks (best-effort, ps/tasklist) that the process looks like an IPTVnator/Electron instance; an unreadable name stays conservative and keeps the skip. - The renderer loads before the repair pass runs and may already hold the pre-repair list with a stale Stop affordance; recovery now broadcasts one RECORDINGS_UPDATE_EVENT after changing any rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): defer teardown finalization behind the flush window and bound the live-size stat Third Codex pass: - A synthetic error/closed snapshot from disposeSession() arrives while the frame-copy helper may still be flushing (0.5 s quit grace + 2 s SIGTERM grace before SIGKILL). Finalizing there statted a file mid-write — short captures became terminal 'failed', longer rows persisted a truncated size, and startup recovery could repair neither. The tracker now defers that finalization behind a 2.5 s flush window; the row stays 'recording' (repairable) meanwhile, and an already-acknowledged stop's settle timer keeps its 'completed' verdict instead of being relabelled 'interrupted'. - The active row's live file size used a bare await stat(): one stat hanging on a dead network filesystem wedged every RECORDINGS_GET_LIST. The probe now mirrors the availability probe's contract — in-flight coalescing plus a 1 s deadline degrading to no size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): unmask recycled recording owners, guard the PWA recording route, and unblock file probes Fourth Codex pass: - Recycled-pid discrimination no longer stops at the process-name family check (any Electron app could shield the row): a live holder must also not provably have started after the recording did (ps -o etime= / PowerShell StartTime). A pid frees only when its previous owner dies, so a recycled pid's holder is always younger than the recording; unreadable evidence stays conservative. - /workspace/downloads/recording/:recordingId gets a supportsRecordings capability guard redirecting the PWA to the manager — RecordingsService never becomes authoritative there, so the detail rendered a permanently blank workspace. - Finalization and startup repair stat through a bounded async probe (3 s deadline, ENOENT/ENOTDIR as the only proof of absence) instead of main-thread statSync: a dead network mount no longer freezes the main thread or the tracker queue, repair leaves unjudgeable rows recoverable, and finalization keeps the requested status with an unknown size rather than branding a likely-good file failed. The 0-byte reservation unlink is fire-and-forget for the same reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep inconclusive recording probes out of Needs attention and bound repair batches Fifth Codex pass: - Recording list decoration now uses the bounded availability variant that preserves 'unknown': a timed-out or permission-errored probe is not proof of absence, so a good recording on a slow mount no longer lands in Needs attention with its Play/Reveal hidden. ElectronRecordingItem.fileAvailability widens accordingly; consumers already gate on === 'missing'. - Startup repair probes its whole batch concurrently, so main.ts awaits roughly one 3 s deadline instead of one per stale row. Cross-process ping propagation under IPTVNATOR_ALLOW_MULTIPLE_INSTANCES stays out of scope (debug-only flag, same single-window design as DOWNLOADS_UPDATE_EVENT) — rationale left on the review thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): fix duration rounding at hour boundaries and bound owner-process probes Sixth Codex pass: - The recording duration formatter rounded minutes after flooring hours, so 59:45 read '60 min' and 1:59:45 read '1 h 60 min'. One shared recordingDurationLabel() now rounds the total minutes before splitting (both the detail page and the library card used a duplicated copy). - Startup repair's synchronous ps/tasklist/PowerShell ownership probes get a 2 s spawn timeout and are memoized per unique pid, so a batch of rows from one crashed instance costs at most one name query and one start-time query, and a hung process query degrades to the conservative fallback instead of blocking the main thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): return to the manager through history from the recording detail Seventh Codex pass (single finding): with a validated returnUrl the manager is already the previous history entry, so Back now uses Location.back() instead of pushing a third entry that made the browser Back button reopen the detail; router navigation remains the fallback for direct links — matching the offline-detail navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): bound removal cleanup and shell gates, date interrupted rows by file mtime Eighth Codex pass: - RECORDINGS_REMOVE no longer awaits an unbounded unlink of a failed row's leftover reservation: cleanup is raced against the 1 s deadline, so a hung network unlink cannot keep the Remove action busy — the row deletion is what matters. - Reveal/Play swap the synchronous lstat gate for the bounded async availability probe: a dead mount no longer blocks the main process, and only PROVEN absence refuses the action — an inconclusive probe lets the shell try and answer honestly. - Startup repair dates an interrupted row's endedAt from the captured file's mtime (mpv's last write) instead of the repair time, so an overnight shutdown no longer inflates a five-minute capture into an hours-long recording; the repair-time fallback remains when mtime is unreadable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep recording-start program metadata fresh across EPG boundaries Ninth Codex pass (single finding): the unified live tab's recordingMetadata computed cached its Date.now() verdict — starting a recording after an EPG boundary snapshotted the previous show. It now tracks the existing 30 s progress tick. The Stalker live layout's currentProgram had the same memoization (feeding recording metadata, the EPG panel summary, and external-player metadata); it gains a 30 s clock tick with interval cleanup in ngOnDestroy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): re-select the Xtream current program against the 30 s tick at recording start Tenth Codex pass (single finding): the Xtream live layout's recording snapshot read withEpg().currentEpgItem, a computed whose Date.now() verdict stays cached until epgItems changes — a recording started after an EPG boundary snapshotted the previous show. The selection logic is extracted as the pure findCurrentEpgItem(items, nowMs), the store computed delegates to it unchanged, and recordingMetadata re-selects with the layout's existing 30 s currentTimeMs tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): scope stop enrichment to the exact recorded list item Eleventh Codex pass (single finding): the stop-enrichment guard compared only the EPG key, which is not unique for M3U items — two list entries sharing a tvgId (or the display-name fallback) could hand the first item's recording the second item's schedule after a switch-triggered auto-stop. RecordingStartMetadata/RecordingStoppedEvent gain an opaque sourceItemKey (unified tab: item.uid; M3U player: channel.id), captured while the recording is active exactly like the EPG key, carried through the player's stop edge, and compared by the hosts before enriching. Xtream/Stalker keys are already playlist+id-scoped and need no extra key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): derive the M3U start-snapshot program from the active channel's schedule Twelfth Codex pass (single finding): the M3U recording snapshot read the NgRx currentEpgProgram, which retains its last value across a channel switch and through EPG gaps (the mirror effect only dispatches when a program exists) — a recording started on a channel with no airing program could persist the previous channel's title, which stop enrichment deliberately never overwrites. The snapshot now derives the program from the active channel's own schedule against the existing 30 s clock, and an EPG gap snapshots no program. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): keep finalizing rows in the recovery ledger and guard the repair update Thirteenth Codex pass (single finding): finalize() removes an entry from the open map before its queued terminal update commits, so activeRowIds() briefly omitted a row still persisted as 'recording' — startup recovery overlapping a clean stop could relabel it interrupted, after which the tracker's status-guarded update could not restore 'completed'. Finalizing entries now stay in a dedicated ledger until the update settles, and the repair UPDATE itself is guarded on status='recording' as a second belt against a finalization that commits between recovery's SELECT and its write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(downloads): register update listeners before the initial list load Fourteenth Codex pass (single finding): RecordingsService awaited its initial RECORDINGS_GET_LIST before subscribing to the update ping — a recording transition during that request pinged into the void while the response still reflected the pre-transition state, and recording pings are rare enough that nothing self-healed until the 15 s poll (armed only once an active row is visible). The listener now registers first so the load-state coalescing queues the trailing refresh. DownloadsService had the same latent window and gets the same reorder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: 4gray <fourgray@proton.me> |
||
|
|
bee7df1e02 |
feat(playlist): auto-detect import method that parses pasted provider messages (#1445)
Adds an "Auto-detect" method to the Add playlist dialog: paste the message a provider sent — links, Xtream credentials, a MAC address with device identity — and a deterministic parser recognizes the source(s) and prefills the matching import form. - detectProviderImportCandidates (libs/shared/interfaces) extracts URLs, MAC addresses and labeled fields, classifies each finding as Xtream, Stalker or an M3U link/body, and returns ranked candidates. Pure and synchronous. - Built against a corpus of 19 real reseller handouts kept verbatim in the spec: Unicode "font" labels, arrow/dingbat separators, separator-less hex serials, dual device IDs, multi-MAC lists, bare three-line handouts, and a guard so a parental PIN is never read as the account password. - Detection only proposes: the target form's own validation and behavioral probes remain the sole path into the store, and no pasted text leaves the app. Passwords are masked on candidate cards, including query and HTTP Basic userinfo forms. - Covered by parser, component and dialog unit tests plus two web E2E specs for the paste → pick → prefilled form workflow; i18n for all 19 languages. |
||
|
|
365cf35317 |
refactor(portals): extract the destructive Xtream refresh into one flow (#1431)
The header action and the Workspace sources page each had their own ~60-line implementation of the destructive Xtream refresh, which is why #1421 had to fix the same connectivity-guard bug twice. `XtreamRefreshFlowService` now owns the sequence once; the entry points supply only an `XtreamRefreshProgressReporter`, so neither can reach the guard reset and skip it. Extracting it surfaced a pre-existing race that neither entry point could have fixed alone: the two guards were independent, so the same playlist could be refreshed from both at once and the second run parked an already-emptied catalog over the first run's snapshot, losing favorites, history, hidden categories and playback positions. The shared flow now serializes runs per playlist, refusing a second one before the guard reset. Found by Greptile; the ordering is pinned by a test that fails when the check moves below the reset. |
||
|
|
f7bb3a13db |
feat(playlist): open recognized M3U movies in the VOD detail view (#1420)
M3U entries recognized as movie files now open in the portals' two-state VOD detail view, fed by TMDB metadata instead of the empty EPG zone. Watch-first: activation still plays immediately, with plot, cast, rating and artwork below the player; Escape reveals the Browse hero. Recognition is a synchronous URL-shape heuristic (movie container extension or an Xtream-style /movie/ path; radio, DASH, /series/ paths and episode-marker names keep today's live layout), gated on TMDB enrichment plus the new default-on Settings.m3uVodDetails toggle. Works in Electron and the PWA. Review follow-ups included: the playback payload no longer carries TMDB fields (its identity is the player's source-application key), the persisted volume reaches the player and survives Browse → Play, the enrichment guard keys on the full lookup identity, and the saved engine mounts first time instead of briefly falling back to Video.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e3f72f7dce | perf(portals): fast-fail requests to portal hosts that stopped answering (#1421) | ||
|
|
5b211faf73 |
feat(remote-control): cover live collections, honest volume, status resets (#1399)
* feat(remote-control): cover live collections, honest volume, status resets Remote control previously worked only on the three routed live layouts (M3U player, Xtream live, Stalker ITV); playing live TV from favorites, recently viewed, or the global collections left the mobile remote inert. - Wire channel up/down, number select, and status publishing into the unified live tab, covering per-portal and global favorites/recent for M3U, Xtream, and Stalker; navigation follows the search-filtered, sorted list exactly as rendered (shared deriveVisibleFavoriteChannels) - Treat non-live status updates as snapshots in the main process so stale now-playing fields are cleared instead of merged forever - Publish a reset snapshot from every integration on destroy, so leaving a live view clears the remote instead of freezing it - Report M3U supportsVolume only for built-in inline playback and no-op volume commands while MPV/VLC/Embedded MPV owns the audio - Publish live status for Stalker radio (same layout, same handlers) and fix its channel-number lookup for non-numeric radio ids Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH * fix(remote-control): review-loop hardening for status honesty - Make the non-live status update an authoritative reset in the main process: only portal survives, supportsVolume is forced false, stray now-playing fields from callers are dropped (Copilot review) - Stop Stalker radio status from leaking an unrelated TV channel's EPG: the ITV-keyed bulk cache survives itv->radio navigation and Ministra ids collide across the two lists, so EPG fields publish for itv only - Publish the reset snapshot when the M3U active channel clears in place (e.g. quitting external MPV), not only on route destroy - Consider a live external session in the M3U volume gate: a diagnostic-recovery MPV/VLC launch owns the audio even while a web player is configured; republish capability on session start/end - Share one REMOTE_CONTROL_RESET_STATUS constant across all four integrations instead of four hand-copied literals Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH * fix(remote-control): external session outranks DASH in M3U volume gate The managed clear-DASH MPV/VLC fallback (Shaka browser-support preflight failure) leaves activeChannelIsDash() true while the external session owns the audio, so the DASH shortcut bypassed the session check and kept advertising remote volume support. The live-session check now precedes the DASH branch; radio stays first because its inline audio element is always mounted and remains audible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmMT33wgK52QL6JAz468eH --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
9aeb83e515 |
fix(m3u): forward playlist-level custom headers to external players (#1397)
* fix(m3u): forward playlist-level custom headers to external players The custom User-Agent/Referer/Origin stored on an M3U playlist only reached the built-in web players (via the Electron webRequest override). MPV/VLC and the embedded MPV player make their own HTTP requests and received only the per-channel #EXTVLCOPT values, so a playlist-wide custom User-Agent was silently dropped for UA-locked providers (#1221). External launch payloads now resolve each header independently: the channel-level #EXTVLCOPT value wins, the playlist-level value is the fallback, blank values count as absent — matching the semantics the unified favorites/recent stream resolver already had. Covers the auto-launch and catch-up effects in m3u-state, the manual MPV/VLC fallback and the embedded MPV payload in VideoPlayerComponent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL * fix(playback): send Origin as a real VLC header and cover header IPC in E2E Review follow-ups: VLC only used the Origin value as an :http-referrer fallback while MPV already sent it via --http-header-fields; both VLC paths (fresh spawn and RC enqueue) now emit the same buildHttpHeaderFields list, so a real `Origin: ...` header reaches the provider, deduplicated against an explicit headers-map Origin. The legacy origin-as-Referer fallback stays. The dash-clearkey Electron E2E now asserts the new IPC contract (blank channel-level headers arrive as undefined, not empty strings) and gains a scenario that sets a playlist-level User-Agent through the source editor and verifies the captured MPV fallback launch carries it across the renderer/main IPC boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6ad9f3ff8a |
feat(stalker): discover portal endpoints on add and edit (#1391)
* feat(stalker): discover portal connection on edit * docs(stalker): document smart endpoint discovery * fix(stalker): make edited connection persistence atomic * fix(stalker): serialize edit discovery * fix(stalker): fence all edit authentication * fix(stalker): serialize overlapping edits * fix(stalker): hydrate playlist identity before edit * test(stalker): await edit hydration * fix(stalker): release abandoned edit fences * fix(stalker): reject stale repairs before discovery * fix(stalker): fence stale portal modes * test(stalker): align simple portal session guard * fix(stalker): reject superseded portal responses * test(stalker): await settled append failure * fix(stalker): retain abandoned auth fences * fix(stalker): fence abandoned discovery retries * fix(stalker): retire restored repair overrides * fix(stalker): verify repair override retirement * fix(stalker): preserve edit-owned repair tokens * fix(stalker): defer repair retirement during edits * fix(stalker): fence repair history reads * fix(stalker): fingerprint portal URL credentials * fix(stalker): persist submitted identity after navigation * fix(stalker): merge late connection saves * fix(stalker): keep edits off Xtream save path * fix(stalker): preserve concurrent edit state * fix(stalker): reject replaced late edit targets * fix(stalker): guard every resolved edit write * fix(stalker): make pwa edit guard transactional * fix(stalker): migrate pwa flags transactionally * fix(stalker): reserve pwa edits across tabs * fix(stalker): coordinate playlist replacements with edit * fix(stalker): reserve lazy repairs across tabs * fix(stalker): drain local repair before edit lock * fix(stalker): block queued repairs during edit drain |
||
|
|
d73acd6bfc | fix(playback): clarify external player launch feedback (#1388) | ||
|
|
1a6af75761 |
feat(settings): per-section pages with unsaved-changes bar (#1384)
* feat(settings): split settings into per-section pages with an unsaved-changes bar Replace the single scrolling settings page with routed section pages (/workspace/settings/:section): the context-panel rail links each section, only the active section renders, and unknown or capability-gated sections redirect to General. The shared form lives on the parent component, so staged edits survive section switches; a floating unsaved-changes bar (Save/Discard) replaces the always-visible footer Save button. Rail links navigate with replaceUrl so Back still leaves settings in one step. Along the way: - delete the unreachable settings dialog mode and the dead AppPortalNavigationActionsService with both of its never-injected DI tokens (PORTAL_NAVIGATION_ACTIONS, PLAYLIST_PLAYER_ACTIONS) - delete the scroll-spy directive and pendingScrollTarget plumbing - revive the EPG panel's "Open EPG settings" empty-state button as a deep link to /workspace/settings/epg; the M3U player now reports m3u-needs-setup only when the channel has no programmes and no EPG source exists in settings or on the playlist itself - load TMDB cache stats when the Metadata page opens (the section component now only exists while its page is open) - add SETTINGS.UNSAVED_CHANGES / SETTINGS.DISCARD_CHANGES to all 19 locales Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(settings): confirm before leaving with unsaved changes Add settingsUnsavedChangesGuard (canDeactivate on the :section route) with a three-action dialog: save and leave, leave without saving, keep editing. The guard only intercepts leaving the settings AREA — section switches share the one settings form and pass unconditionally, so the dialog can never nag while moving between pages. A failed save cancels the navigation instead of silently dropping the edits it promised to keep; leaving without saving also reverts the live theme preview. Save-and-leave is disabled while the form is invalid, with a hint explaining why. New SETTINGS.UNSAVED_DIALOG_* keys in all 19 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): stage cover size and EPG view mode; adapt e2e to section pages Cover size and EPG view mode were the only two controls that persisted eagerly on click, which made Discard (and leave-without-saving) unable to revert them: hydrateFromStore() faithfully reloaded the just-persisted edit. They now stage in the form like every other setting and reach the store on Save. Review finding by Greptile (P1) and Codex. E2E suites that walk through settings are updated for one-section-page rendering (epg, backup-roundtrip, xtream-epg, remote-control) and for the staged cover size (downloads asserts the dataset after Save); the EPG icon fallback test saves before leaving settings so the new unsaved-changes dialog does not block its navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd96b85c19 |
feat(playback): recommend recovery actions (#1374)
* docs(playback): design recovery recommendations * docs(playback): plan recovery recommendations * refactor(playback): extract diagnostic utilities * feat(playback): define recovery recommendation contracts * feat(playback): rank recovery recommendations * feat(playback): track session recovery attempts * feat(playback): identify content recovery sessions * feat(ui): add ranked playback diagnostic panel * feat(playback): switch temporarily to recommended players * test(playback): cover temporary player recommendation * test(playback): verify recommendation capability guards * docs(playback): document recovery recommendations * fix(playback): keep recovery keys credential-free * fix(playback): remove derived tracking ownership * fix(playback): preserve distinct recovery fallbacks * fix(playback): reset resume for new sources * fix(playback): preserve desktop recovery guidance * docs(playback): clarify recovery policy exceptions * fix(playback): reject stale progress updates * fix(playback): keep protected recovery guidance neutral * test(playback): cover stale progress output * fix(playback): neutralize protected diagnostic copy * fix(playback): harden runtime guidance ownership * fix(playback): stabilize recovery application ownership * fix(ci): classify playback util coverage * fix(e2e): preserve playback fixture bytes |
||
|
|
9ff1c6ae01 |
feat(stalker): identity hardening (#1370)
MAC addresses are canonicalized to the uppercase colon form a real STB
sends and validated at the input boundary, with a hint when they fall
outside Infomir's OUI — which the stock server's default filter refuses
with a bare {status: 1} no user could diagnose. Normalization applies
only to a value the user actually edits: rewriting stored bytes would
move the session fingerprint for every existing playlist with no user
action, and the MAC is the account key.
Device IDs can optionally be derived from the MAC the way StbEmu and
stalker-to-m3u do — SHA256(MAC) and SHA256(MAC + "stalker"), which a
real box never reports as equal. The portal pins the first non-empty
device_id/device_id2 it sees to the MAC permanently, refuses a different
one, and treats a later empty value as an unrecoverable lockout, so
derived values are written into the visible fields and persisted as
literal strings, never recomputed at request time. The option is offered
at import only; the edit dialog warns instead once an ID has actually
reached the portal.
get_profile now reports one coherent MAG250 (ver, stb_type — previously
empty —, hw_version, image_version, client_type), and a device conflict
gets its own StalkerPortalError kind so the UI can explain it instead of
relaying the portal's "Your STB is damaged".
Closes the identity-fields cluster: #927, #860.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d2a83164ec | feat(stalker): protocol-correct auth lifecycle (#1354) | ||
|
|
c741815b97 |
fix(build): include shared UI stylesheets in Nx cache inputs (#1360)
* fix(build): include shared UI stylesheets in Nx cache inputs `libs/ui/styles` held shared SCSS partials but had no `project.json`, so its files belonged to no Nx project and were absent from every task hash. Editing a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and shipped the previous CSS — a silent wrong build rather than a failure. Nx derives its project graph from TypeScript imports only, so a relative Sass `@use` that crosses a project root creates no edge. Verified directly: after adding the project but before declaring anything, `ui-styles` still had zero dependents in the graph. Make it the `ui-styles` project (no targets — it exists to be hashed) and declare `implicitDependencies` on the 8 consumers. Chosen over adding the path to `sharedGlobals`, which would put shared styles into every project's hash and make a one-line SCSS tweak mark the whole workspace affected. A styles edit now marks 15 projects affected and leaves electron-backend, website, the mock servers and the shared libs alone. `libs/ui/styles` was the only projectless directory holding files under `libs/` or `apps/`. Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every relative stylesheet import against Nx's real project graph and fails when one escapes the input closure of a build that compiles it, naming the project to declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of `apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes that file, and a lib -> app edge would make the graph cyclic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(build): spawn git without a shell in the stylesheet check `execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where single quotes are literal characters rather than quoting. Git received the pathspec with the quotes intact, matched nothing and exited 0, so `styles:inputs:validate` reported success after checking zero stylesheets — silently disabling the check for Windows developers while staying green. Spawn with `execFileSync` so no shell is involved and git expands its own pathspec; verified to return the identical 133 files. Both this and the eslint glob trap next to it in the docs report success while covering nothing, so also make an empty scan fail rather than pass: the workspace always contains SCSS, and a listing that returns none means the scan broke. Reported by Codex review on #1360. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(styles): move nav-list partial into ui-styles (#1361) * fix(build): count every target of a comma-separated Sass @import `@import` is the only rule that takes a list, and the scan read just its first target. A later entry crossing an Nx project boundary escaped the cache key while the check still reported success — the same silent-pass failure the tool exists to prevent. Parse every target of an `@import` list. The obvious "read all quoted strings" fix trades one silent gap for a phantom one, so the rule decides: `@use`/`@forward` load exactly one module and a quoted string after it is `with (...)` configuration, and `url(...)` stays a plain CSS import the browser resolves at runtime. Neither is a module Sass compiles. The workspace has no relative `@import` at all today, so the scan still finds the same 42 imports across 133 files; this closes the gap before someone writes one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b92503feae |
feat(stalker): endpoint probing + behavior-based portal mode with lazy repair (#1344)
* feat(stalker): endpoint probing + behavior-based portal mode with lazy repair Replace the URL-shape guess behind isFullStalkerPortal with real endpoint discovery: at import, probe portal.php -> server/load.php -> stalker_portal/server/load.php (the pasted .php endpoint first) and classify the portal by observed behavior — a token-less itv/get_genres answering data proves a token-free panel, the middleware's plain-text auth failure proves the endpoint enforces the token, confirmed by the real handshake + get_profile. The proven endpoint and mode are persisted. The three diverging portal-mode predicates (import, session service, legacy migration) collapse into one shared helper in @iptvnator/shared/interfaces; executeStalkerRequest becomes the single request choke point (search and the collection stream resolver fold in), and the production-dead makeStalkerRequest copy is removed. Existing misclassified playlists repair themselves lazily: only after a request actually fails with the plain-text auth bodies, HTTP 404, or a terminal handshake error, at most once per playlist per session, and only a configuration discovery proved to answer is persisted — via a minimal portalUrl/isFullStalkerPortal patch, so favorites, recents and playback positions survive. Working reseller panels are never probed or rewritten; there is deliberately no eager one-shot migration, because tolerant portal.php panels cannot be told apart from misclassified canonical portals without probing. The Electron handler now embeds the HTTP status code in the error message (ipcRenderer.invoke strips custom properties from rejections), and probe requests carry silent:true so expected 404s do not toast error snackbars. The stalker mock gains a portal.php-less /ministra host so e2e can prove the 404 fallthrough end to end. Fixes #850, #686, #755. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): sync watchdog, PWA proxy errors and cmd resolution with lazy repair Review round 1 (Greptile P1, Codex P1/P2): - A successful repair now re-syncs the ACTIVE watchdog playlist via the new StalkerSessionService.refreshActiveWatchdogPlaylist(): a simple-to-full repair starts the required keepalive mid-session, full-to-simple stops it, and an endpoint change repoints the pings instead of leaving them on the activation-time snapshot. - PwaService.forwardStalkerRequest surfaces the web-backend proxy's normalized { message, status } no-payload envelope as an HTTP error carrying the status, so endpoint discovery and the lazy repair can classify upstream 404s in the PWA too (previously payload unwrapping returned undefined and dead endpoints were unrepairable there). Probe requests pass silent:true and skip the error snackbar. - fetchStalkerPlaybackLink and the collection StreamResolverService re-apply the repair override AFTER the request, so a relative create_link reply resolves against the endpoint that actually answered (the resolver keeps the /stalker_portal path segment as base, so this matters beyond origin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): parse candidate URLs and tie repair overrides to their source config Review round 2 (Codex P2 x2): - Endpoint candidates are now derived from the parsed origin + pathname: a pasted URL carrying a query or fragment (host/c?key=value) no longer gets /portal.php bolted onto the query, which made every probe hit /c and persisted the non-API URL. - A repair override is tied to the failing configuration it replaced. Playlists carrying anything else (the user edited the portal URL or mode through the playlist dialog) drop the override and re-arm the once-per-session probe latch, so edited metadata is used verbatim and may repair again if it fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): auth-gated probes, normalized offline fallback, mock docs sync Review round 3 (Codex P1 x2, P2): - A probe answered with HTTP 401/403 now classifies the endpoint as auth-required and attempts the real handshake instead of skipping the candidate: non-standard middlewares answer 401 where the stock server sends HTTP 200 + plain text, and such portals authenticated fine before discovery existed. - The unreachable-host import fallback normalizes the pasted URL (origin + pathname) before the legacy /c -> portal.php rewrite, so a query or fragment can no longer make it persist the browser page URL - a 200 HTML answer from /c is not a repair trigger, which would have left the playlist empty for good. - The stalker mock-server README and architecture doc now describe behavior-based discovery and the /ministra host instead of the retired URL-shape rule and its "known inconsistency" note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): recognize JSON auth failures and guard repairs against mid-probe edits Review round 4 (Codex P1 + P2): - isStalkerAuthFailureResponse() recognizes the JSON envelope some panels answer instead of the plain-text body ({js:{error:"Authorization failed"}} / {js:{msg:...}}). Probe classification treats it as auth-required instead of token-free data, and the lazy-repair trigger fires on it at runtime — previously such a portal was persisted simple with no repair path at all. - A repair is committed only after re-reading the persisted row and verifying it still carries the configuration that failed: a user who edits the portal URL (or deletes the playlist) during the multi-second probe now wins over the in-flight repair result for the old URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): probe past endpoint 5xx, sibling fallbacks, identity-aware repair guard Review round 5 (Codex P2 x3): - A probe that fails with a RESOLVABLE HTTP status keeps discovery going: a broken /portal.php handler answering 500 must not hide a healthy sibling endpoint. Only status-less failures (true network level) stop the loop. The Electron handler now gives real HTTP 5xx responses the same parseable "HTTP Error <code>" message shape as 4xx, so the renderer can tell them apart from ECONNREFUSED/timeouts after ipcRenderer strips the object shape. - Standard fallback candidates for a nonstandard pasted endpoint (.../cp/api.php) derive from its DIRECTORY, so recovery probes hit /cp/portal.php instead of /cp/api.php/portal.php. - The repair's row re-verification also compares the MAC and all Stalker identity fields: a probe authenticated as the old identity must not install its token/watchdog or persist onto a row whose credentials were edited mid-probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): reactivation-safe watchdog, wider JSON auth phrases, per-config probe latch Review round 6 (Greptile 4/5 concern + Codex P1/P2): - setCurrentPlaylist applies the repair override before feeding the watchdog and store state: re-activating the portal route with the stale NgRx meta no longer stops or repoints the repaired keepalive back to the broken configuration. - The structured js.error/js.msg fields accept the full phrase set the session service recognizes (Invalid token, Auth failed, bare unauthorized/authorization) — panels answering those envelopes were still classified token-free. Plain-text body matching stays narrow on purpose (HTML false positives). - The once-per-session probe latch is keyed by the SOURCE configuration fingerprint (endpoint, mode, MAC, identity) instead of the playlist id: a repair discarded because of a mid-probe edit no longer blocks the edited configuration from repairing, while stale snapshots of an already-probed configuration still cannot loop the probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): identity-aware override invalidation and timeout-tolerant probing Review round 7 (Greptile P1 + Codex P2): - The repair override records the identity fingerprint the probe authenticated as. Editing the MAC or any Stalker identity field afterwards drops the override, the per-config probe latch AND the cached token, so requests and watchdog pings never pair the edited identity with a session negotiated for the previous one. - A status-less probe failure that is a TIMEOUT (renderer budget, axios request timeout, ETIMEDOUT) continues to the next candidate — one hanging handler must not hide healthy siblings; connection-level failures (refused, unresolvable host) still stop discovery, so dead hosts keep failing fast. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): watchdog pings authenticate as the persisted row Review round 8 (Greptile 4/5 concern): The watchdog held its activation-time playlist snapshot for the whole session, so portal metadata edited (or repaired) mid-session kept the keepalive authenticating as the previous identity/endpoint — its pings could keep the old session alive and repopulate the playlist-scoped token cache with a token for the pre-edit identity. Each ping now resolves the playlist from the persisted row first (the single source of truth), falling back to the snapshot only when the store cannot be read, and refreshes the snapshot on every successful read. Any edit — identity, endpoint or mode — reaches the keepalive within one ping cycle; a row now marked simple (or deleted) stops the watchdog. The in-flight guard is claimed before the row read so overlapping pings cannot double-fire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): identity-tagged tokens, watchdog override overlay, retire-on-failure Review round 9 (Greptile 4/5 concern + Codex P2): - The session token cache is tagged with the identity fingerprint (MAC + all Stalker identity fields) the session was negotiated for; ensureToken re-authenticates instead of handing an edited identity the previous token. The fingerprint helper is shared (stalker-identity.utils) with the repair layer's override/latch checks. - Watchdog pings overlay the repair layer's in-session override on the resolved row (registered decorator, no import cycle): a simple-to-full repair whose persistence is pending or failed no longer reads the stale row and stops the freshly started keepalive. - makeAuthenticatedRequest retires a failed token even on the no-retry path (watchdog pings), so a dead session is never handed to the next caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): pending authentications are identity-scoped Review round 10 (Greptile 4/5 concern): pendingAuth entries carry the identity fingerprint they authenticate as. A request for an edited identity no longer adopts an in-flight result negotiated for the previous identity: it waits the old authentication out (a competing handshake would strand it with a dead token on strict portals) and then negotiates its own session. This was the last id-only-keyed session structure — override, probe latch, token cache, watchdog snapshot and pending auth are now all identity-aware. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): atomic repair persistence, full probe history, normalized offline classify Review round 11 (Codex P2 x3 + P1 docs): - The repair's row verification and patch now run ATOMICALLY inside the per-playlist write queue via the new PlaylistsService.transformPlaylistMeta(): a user edit that is queued but not yet committed wins over the repair — the transform sees the edited row and aborts instead of overwriting it. Write failures after a successful verification keep the session-only override, read failures discard the repair. - The per-playlist probe latch keeps EVERY attempted source fingerprint, so alternating edits (A -> B -> A) cannot evict a fingerprint and let stale snapshots re-run discovery. - The unreachable-host import fallback classifies the normalized origin+pathname, so a query merely mentioning /server/load.php cannot make a panel URL look canonical and abort the offline import. - docs/architecture/stalker-portal.md documents the actual probe sequencing: any resolvable HTTP status (incl. 5xx) and timeouts continue, 401/403 classify as auth-required, only connection-level failures abort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): collision-proof session fingerprints Review round 12 (Greptile P1): identity values are unrestricted strings, so the delimiter-joined fingerprint could alias distinct identity tuples (serial "a|b" + empty device vs serial "a" + device "b") and bypass the identity invalidation. Both the identity fingerprint and the repair source fingerprint are JSON-encoded now; regression test pins the exact aliasing pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): preserve URL authority in normalization; document per-config latch Review round 13 (Codex P1 docs + P2): - normalizeStalkerPortalInputUrl mutates the parsed URL (clear query/ fragment, trim pathname) instead of rebuilding from origin, and the candidate builder swaps only the path — file: URLs (origin "null") no longer make the builder throw, and basic-auth credentials are not silently dropped before probing. - The canonical docs and the repair service JSDoc now describe the actual loop guard: at most one probe per SOURCE CONFIGURATION (endpoint, mode, MAC, identity) per playlist per session, not once per playlist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): HTTP 401/403 failures trigger the lazy repair Review round 14 (Codex P1): discovery classifies 401/403 endpoints as auth-required, but the repair trigger accepted only 404 — a legacy playlist misclassified token-free against an HTTP-auth-gated middleware could never reach discovery and stayed unusable. 401/403 now qualify; endpoint-specific 5xx still do not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): re-enter repair for edited configurations after a pending probe Review round 15 (Codex P2): a request carrying an edited configuration that raced an in-flight probe only awaited it and inherited its outcome — the edited fingerprint stayed unattempted and the first request failed without triggering its own discovery. repairPortal now re-enters after awaiting the pending probe, so the per-config latch decides: already attempted -> reapply, never attempted -> own probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): probe history remembers outcomes so restored configs repair again Review round 16 (Greptile P1): the per-config latch kept A's fingerprint after an edit to B dropped A's override, so restoring A left it latched with nothing to reapply — broken until restart. The history now stores each probe's OUTCOME (override or null): a restored configuration reinstalls its remembered repair without a second discovery, and the anti-ping-pong property (A<->B alternation never re-runs discovery from stale snapshots) is preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlist): serialize deletion behind the per-playlist write queue Review round 17 (Codex P2): deletePlaylist bypassed serializePlaylistWrite, so a queued mutation (e.g. the Stalker portal repair's conditional transform) finishing after an unserialized delete could upsert the row back and resurrect the playlist. Deletion now runs through the same queue: queued writes commit first, the delete lands last, and a transform enqueued after the delete reads a missing row and aborts. Regression test pins the write-then-delete ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): reinstalled repairs re-sync the watchdog like fresh ones Review round 18 (Greptile P1): the restored-configuration branch reinstalled the remembered override without the watchdog refresh the fresh-repair path performs — if the intermediate edit stopped the keepalive, the restored full-portal session recovered requests but never its pings. The reinstall now calls refreshActiveWatchdogPlaylist with the override applied, symmetric with a fresh repair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): discarded probes retry once their configuration is restored Review round 19 (Greptile P1): the pre-probe history reservation survived the row-mismatch discard, so restoring the original configuration hit the latch with nothing to reinstall — lazy repair stayed disabled for the session. Probe records are now explicit (override / no-change / discarded): a discarded configuration probes again once one cheap row read confirms the row was RESTORED to it, while stale snapshots of it stay declined without a discovery run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): IPC-safe transport errors, repairable profile path, nested base paths Review round 20 (Codex P2 x4): - The Electron handler throws a real Error for axios failures without a response: Electron serializes rejections via toString(), so a plain object arrived as "[object Object]" and discovery could not tell a timeout (keep probing) from a dead host (stop). - isAuthorizationError parses HTTP 401/403 out of the IPC-wrapped message, so an expired-token 403 retires the token and re-authenticates instead of surfacing as a plain failure. - The account-info full-profile path (which bypasses executeStalkerRequest) routes repair-trigger failures through StalkerPortalRepairService and retries with the repaired playlist, so opening the dialog can fix a stale endpoint. - resolveStalkerPlaybackUrl derives the installation base from the endpoint's API suffix instead of a fixed stalker_portal|c|portal allowlist: relative create_link replies now resolve correctly under arbitrary discovered installations such as /cp/server/load.php. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): strict probe data shape, mode-aware profile retry, docs API name Review round 21 (Codex P1 docs + P2 x2): - Probe classification requires the real get_genres shape (array, or a {data: []} envelope without an error) instead of a bare `js` key: a 200 error envelope ({js:{error:"Unknown action"}}, {js:false}) no longer ends discovery on a broken candidate and persists an empty catalog. - After a repair that flips the portal to simple mode, the account-info retry re-enters the mode routing and uses get_main_info instead of handshaking against a token-free panel again. - docs/architecture/stalker-portal.md names transformPlaylistMeta and its atomic source-check invariant (plus the serialized deletion) rather than the race-prone updatePlaylistMeta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): account dialog re-routes after a simple-to-full repair Review round 22 (Codex P2): fetchViaMainInfo runs through executeStalkerRequest, whose lazy repair retries the SAME action, so a repair proving the portal is actually full left the dialog calling get_main_info — canonical installations publish subscription details only through handshake + get_profile, leaving the dialog empty. The routing is now symmetric with the full-to-simple case: an empty main-info result whose repair flipped the mode re-enters the profile flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): row-gate override reinstall; document mode-based account routing Review round 23 (Codex P2 + P1 docs): - Reinstalling a remembered override now requires the persisted row to actually carry that configuration again. A stale request for A while the row holds an unrelated C no longer resurrects A's override, which would retry against B and repoint the active watchdog away from C. (The edit-back-to-A case stays as documented: there the row IS A.) - docs/architecture/stalker-portal.md and CLAUDE.md describe account-info routing by the observed portal MODE instead of the endpoint shape — a token-enforcing portal.php is a full portal now — and note the mode-change re-routing in both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): share the auth-failure predicate; prefer profile over partial main-info Review round 24 (Codex P1 + P2): - isAuthorizationError now reuses isStalkerAuthFailureResponse, so the phrases discovery and the lazy repair already classify as auth failures (Access denied., Unauthorized request., and their JSON envelopes) also retire the session token. Previously a full portal expiring with either phrase kept its dead token: the repair rediscovered the same endpoint/mode, recorded no-change, and every later request stayed broken. - After a simple-to-full repair, even a PARTIAL get_main_info answer no longer wins over the profile flow — expiry and tariff live only behind handshake + get_profile. The partial facts are kept only if the profile path itself publishes nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): keep a literal c installation directory in candidate derivation Review round 25 (Codex P2): the /c landing-page rewrite ran after the endpoint file was stripped, so `/tenant/c/portal.php` collapsed to `/tenant` and the sibling probes went one level too high, rejecting a valid portal whose installation directory is literally named `c`. The rewrite now applies only when the pathname itself ends in `/c` (no endpoint file); pasted endpoints strip only the file part. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): route rejected post-repair main-info retries to the profile flow Review round 26 (Codex P2): a simple-to-full repair during fetchViaMainInfo makes executeStalkerRequest retry the same action against the repaired full portal, and installations that do not implement get_main_info answer 404 — the rejection escaped before the repaired-mode check, so the dialog failed instead of switching to get_profile. The rejection is captured and reaches the same check; without a mode change it is rethrown unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): full predicate for wrapped denials; record the removed store prop Review round 27 (Codex P2 + P1 docs): - The repair trigger applies the shared auth-failure predicate to the error MESSAGE too, so authentication's wrapped structured denials (Error('Profile error: Access denied.')) reach the repair instead of bypassing it and leaving a healthy sibling endpoint unprobed. - docs/architecture/stalker-store-api-baseline.md records makeStalkerRequest as removed, with the reason it gets no facade alias: it was production-dead and held a fourth private copy of the portal-mode branch that the shared predicate exists to prevent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): complete auth predicate for wrapped error messages Review round 28 (Codex P2): the plain-text BODY matcher deliberately knows only the three middleware phrases, so passing an error message through it let authenticate()'s wrapped denials — Error('Profile error: Invalid token') / 'Auth failed' — bypass both the repair trigger and the session auth predicate. A dedicated isStalkerAuthFailureMessage() applies the wide phrase set to controlled error strings, while arbitrary portal bodies keep the narrow matcher that cannot false-positive on HTML pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): reject denied profiles during confirmation; document all repair triggers Review round 29 (Codex P2 + P1 docs): - Full-portal confirmation validates the get_profile envelope with the shared structured predicate: a handshake can hand out a token whose profile still answers {js:{error:"Invalid token"}}, and authenticate() inspects only msg/block_msg — discovery would have persisted an unusable endpoint and stopped before the healthy sibling. authenticate() now returns the raw profile response for that check. - The canonical lazy-repair contract lists the complete trigger set: the plain-text bodies AND their JSON envelopes, HTTP 404, HTTP 401/403, and terminal handshake/profile errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6c065124ed |
feat(stalker): add account info dialog for Stalker portals (#1330)
* feat(stalker): add account info dialog for Stalker portals Xtream playlists have had an account-info dialog for a while; Stalker portals stored the same facts (login, expiry, tariff, status captured at import) as dead weight in the database and showed them nowhere. Add StalkerAccountInfoComponent mirroring the Xtream dialog's visual language: status pill, days-left/tariff/MAC hero stats, account and portal panels. Data is cached-first — the import-time snapshot renders instantly with a "Saved data" badge, then StalkerAccountInfoService refreshes it: full /stalker_portal/ installations re-run handshake+get_profile, portal.php panels are queried best-effort via account_info/get_main_info. A failed refresh keeps the cached snapshot; no data at all shows a retry-able error state. Entry points are unified behind shared portal-account predicates (isXtreamAccountPlaylist / isStalkerAccountPlaylist in shared/interfaces) so both portal types get the same set: header playlist switcher (bottom section + new per-row ⋮ Account info item), dashboard source card ⋮ menu, and the command palette (now visible on stalker routes with its own description). The header service picks the dialog by playlist type; the per-row path works for non-active playlists and skips the session-scoped stream counts. Also adds the missing top-level LOADING/RETRY i18n keys the Xtream dialog already referenced (they rendered as raw keys), a get_main_info handler in the stalker mock server, and STALKER.ACCOUNT_INFO translations for all 19 locales. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): unwrap nested js.account_info envelope in get_main_info Ministra-style portals nest the account block — fetchStalkerExpireDate() in stalker-player-request.utils already consumes exactly that shape, so the flat-only mapper silently discarded valid responses and legacy imports (which have no cached snapshot) got an empty account panel. Merge nested fields over flat aliases, send the JsHttpRequest parameter the existing get_main_info caller sends, switch the mock server to the nested envelope so the E2E covers the realistic shape, and document the account-info feature in CLAUDE.md (review feedback from Greptile and Codex on #1330). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(stalker): pin account-info expiry fixture below the day boundary Math.round on the epoch could round up half a second, putting the fixture's expiry just past the 30-day mark so daysLeft ceil'd to 31 on CI. Floor keeps the interval strictly inside 30 days regardless of when within the second the spec runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(stalker): address account-info review round two Three P2s from Codex on #1330: - Normalize the cached stalkerAccountInfo snapshot before rendering: the import path persists portal values verbatim, so expireDate can be a date string or milliseconds at runtime despite the declared number type. normalizeStoredStalkerAccountInfo() runs the same parsers as the fresh path. - Publish the re-auth token into StalkerSessionService's cache: strict portals invalidate the previous token per handshake, so the dialog's authenticate() would otherwise strand an active portal session on a dead token. - Extract the duplicated ~460-line account-dialog stylesheet into libs/ui/styles/_account-dialog.scss, shared by both dialogs with the provider accent injected via --account-dialog-accent; each consumer keeps only its accent and layout overrides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): serialize account-profile refresh with session auth The dialog's direct authenticate() call bypassed the pendingAuth map ensureToken() uses, so a refresh could run a second handshake while a catalog or watchdog request was still authenticating. On strict portals each handshake invalidates the other's token, and the later setCachedToken() could publish an already-dead one. Move the refresh into StalkerSessionService.refreshAccountProfile(): it waits for any in-flight authentication, registers its own so later callers wait for it, and republishes the resulting token. A failed pending auth no longer aborts the refresh, and the pendingAuth entry is only cleared when it is still this call's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): move pendingAuth cleanup out of the promise initializer TS2454 under the Angular compiler: the finally block referenced authPromise inside its own initializer, so every Electron/web production build failed even though jest and lint accepted it. Await the promise at the call site and retire the map entry there instead — same only-clear-our-own-entry semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): harden account-info portal detection and expiry math Review round four (Codex P2s on #1330): - Fall back to the URL rule when isFullStalkerPortal is undefined: a playlist restored from an older backup carries no flag once the one-shot metadata migration has run, and it would then be sent down the unauthenticated legacy path and labelled a legacy panel. - Parse a bare YYYY-MM-DD expiry as a local calendar date. Date.parse reads it as UTC midnight, which renders as the previous day west of UTC and shifts the days-left boundary; timestamps carrying a time or offset keep standard parsing. - Decide expiry from the raw timestamp, not the rounded counter: an expiry that passed less than a day ago ceil's to 0/-0, so the hero stat claimed "0 days left" on a dead subscription. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): make account-profile refresh own the auth slot Review round five (Codex P2s on #1330): - Claim the pendingAuth slot in a loop and publish it before the first await. One settled promise releases every waiter at once, so a single pre-check let two queued refreshes both start handshakes that invalidate each other on strict portals. - Retire the cached token before the handshake: ensureToken() reads tokenCache before pendingAuth, so catalog and watchdog requests starting mid-handshake were handed a token this refresh was about to kill instead of queueing on the slot. - Render the portal type from the same resolver the fetch path uses, so a restored backup without an explicit flag is no longer labelled a legacy panel while authenticating as a full portal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): retire only the token that actually failed auth A request dispatched with the previous token can see its authorization failure arrive after a profile refresh has already cached a fresh one. The retry path deleted the cache blindly, killing the fresh token and kicking off another handshake that in turn invalidated tokens of newer requests — cascading retries on strict portals. makeAuthenticatedRequest() now retires the cached token only while it still equals the token that failed; a late failure of a stale token leaves the refreshed token in place and the retry reuses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(stalker): distinguish the two no-data outcomes of the account dialog A portal that answers but publishes no account facts renders the ready-state "No account details" panel; only an unreachable portal without a cached snapshot enters the error state with retry. The doc conflated both as "error with retry" (review feedback on #1330). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): reject negative expiry sentinels before date parsing Portals encode unlimited/missing expiry as "-1" or "0"; the unsigned-digit check let "-1" fall through to Date.parse, which V8 reads as January 1, 2001 — an unlimited account rendered as expired. Signed numeric strings now take the numeric branch, whose non-positive guard already discards them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): reject out-of-range calendar components in expiry dates The multi-argument Date constructor normalizes invalid components ('2026-00-00' becomes Nov 30, 2025), fabricating an expiry and countdown from a placeholder. Round-trip the parsed year/month/day and reject any date that does not survive unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e86e988e72 |
feat(ui): turn the phone context panel into an off-canvas drawer (#1332)
* feat(ui): turn the phone context panel into an off-canvas drawer On ≤640px viewports the workspace context panel (categories, filters, settings sections, collection filters) no longer stacks above the route content capped at 30vh — it is a hidden-by-default drawer that slides in from the left over a backdrop, opened via a new header toggle (phone-only, CSS-gated) and closed by selection, backdrop tap, Escape, or any navigation. State lives in the new WorkspaceShellContextDrawerService provided by the shell component; panels close it explicitly after selections that do not navigate (Stalker ITV/radio categories, settings sections, sources filters, collection filters), since NavigationEnd alone cannot cover those. Desktop behavior is untouched, including the ResizableDirective inline width. Closes the drawer follow-up deferred from #1100 / PR #1326. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): make the phone context drawer modal for keyboard users Addresses Greptile P1 and Codex P2 review feedback on #1332: - CdkTrapFocus on the sidebar captures focus into the drawer on open and contains it while the drawer is modal; the shell restores focus to the header toggle on close, since the closed drawer is visibility: hidden and focus left inside it would silently drop to <body>. - The drawer service closes the drawer when the viewport leaves the phone breakpoint (matchMedia), so the trap can never hold the in-flow desktop sidebar after a resize. - The toggle's tooltip and aria-label are now variant-aware — categories on portal routes, filters on sources/collection routes, settings sections on the settings route — instead of a fixed 'Categories & filters' that misdescribed two of the three; the two generic i18n keys are replaced by six variant keys across all 19 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): remove background content from the a11y tree while the drawer is open Round-2 review feedback on #1332 (Greptile P1, Codex P2): - The rail, header, route content and playback footer are marked inert while the phone drawer is open — CdkTrapFocus constrains Tab focus, but a screen reader's virtual cursor could still reach and activate the visually obscured controls behind the backdrop. - The drawer panel itself is the trap's initial focus target (tabindex=-1 + cdkFocusInitial), so focus capture still works when a category list is loading, empty, or failed and renders no focusable rows. - Focus restore on close is deferred one tick: the toggle lives in the inert header, and focus() on a still-inert element is silently ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): gate global shortcuts and Escape behind the open phone drawer Round-3 review feedback on #1332 (Codex P2s): - The shell consumes Escape while the drawer is open: downstream Escape consumers (the portal detail shell's inline player close, the shared controls shortcuts) check defaultPrevented, so one keypress no longer closes both the drawer and the obscured playback surface. - inert does not silence document-level keydown listeners, so players opt out themselves while inside an inert region: ControlsShortcuts gains an optional hostElement handler and ignores every shortcut (including Escape) when that host has an inert ancestor, and the radio audio player applies the same check to its volume/mute keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): close control, Cmd+F gate, and Embedded MPV inert guard for the drawer Round-4 review feedback on #1332 (Greptile P1, Codex P2s): - The drawer carries its own phone-only close button: touch screen-reader users have no hardware Escape and cannot reach the inert header toggle or the aria-hidden backdrop, so the trapped surface must offer dismissal itself — even when a category list is loading or empty and renders no actionable entries. - Ctrl/Cmd+F no longer opens global search while the drawer is modal; the shortcut would have navigated and focused an input inside the inert header. - EmbeddedMpvShortcuts (native-view legacy dock) gains the same hostElement/inert-ancestor guard as the shared controls shortcuts, so the obscured player cannot react to Space/arrows/M/Escape behind the drawer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-5 drawer feedback + update phone-layout e2e for the drawer Merges master (#1333 landed mobile-layout.e2e.ts pinning the #1326 stacked-panel behavior this PR replaces) and updates that spec to pin the drawer contract instead: panel hidden by default with full-width content, header toggle opens it over a backdrop, category selection and backdrop tap close it. Verified locally on Chromium, Firefox and WebKit (12/12). The spec's getByTestId calls needed plain [data-test-id=...] locators — the web-e2e Playwright config never mapped testIdAttribute. Also addresses Codex round-5 P2s: - Focus restore now reports whether the toggle received focus; when a drawer selection navigated to a route without a context panel (toggle gone), focus falls back to the route content instead of dropping to <body>. - The Xtream and Stalker live layouts' Ctrl/Cmd+B sidebar shortcut opts out while their host sits inside an inert region, matching the other document-level listeners. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): suppress command palette and shortcuts dialog behind the open drawer Greptile round-6 finding on #1332: the document-level Ctrl/Cmd+K handler in WorkspaceShellFacade and the '?' help-key handler in WorkspaceKeyboardShortcutsService still opened their dialogs while the phone context drawer was modal, stacking a second focus-trapped surface on top of it. Both now check the drawer service (injected optionally, same shell-component providers) and stay quiet while it is open, like the Ctrl/Cmd+F global-search gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-7 drawer feedback — Cmd+R gate and native MPV surface hiding Addresses the two Codex round-7 P2s on #1332: - WorkspaceShellContextDrawerService moves to @iptvnator/workspace/shell/util and becomes root-provided, so AppComponent's document-level Ctrl/Cmd+R global-recent shortcut can observe the modal drawer without pulling the lazy shell chunk into the eager bundle. Cmd+R is now suppressed while the drawer is open, like Cmd+F/Cmd+K/'?'. - The shell registers the open drawer with a new EmbeddedMpvOverlayVisibilityService.acquireExternalModalSurface() API: the native-view video surface is composited outside DOM stacking and would paint straight over the drawer regardless of z-index. The service treats registered external modal surfaces exactly like open Material dialogs. - The service's recompute no longer reads overlayActive back before setting it: signals already skip notification on equal values, and that hidden read registered overlayActive as a dependency of any reactive context calling into the service — the shell's acquire/release effect looped forever on exactly that (caught by a live browser probe; the unit suite mocked the service). The effect also wraps the acquire in untracked() for caller-side hygiene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): expose the phone drawer as a named modal dialog Round-8 review feedback on #1332 (Codex P2s): - While open, the drawer carries role=dialog, aria-modal=true, and a variant-appropriate accessible name (categories / filters / settings sections) — assistive technology now hears that a named modal surface opened instead of an unnamed complementary landmark. Closed (and the always-visible desktop sidebar) stays a plain landmark. - The UI-guidelines drawer section no longer claims the drawer service is component-provided; it is root-provided from workspace/shell/util since the round-7 move, and the stale claim could have led a future change to re-scope it and silently break the AppComponent shortcut gate and the Embedded MPV overlay observer. Matching code comments updated everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): round-9 — gate M3U player keys behind the drawer, raise drawer stacking Greptile round-9 P1 + Codex round-9 P2 on #1332: - The M3U video player's document-level digit-key channel switching and Ctrl/Cmd+B sidebar toggle now apply the same inert-ancestor guard as every other routed-content key listener. A codebase sweep confirms this closes the class: every document-level key listener on routed content is now either gated by the shell (Escape, Cmd+F/K/R, '?') or opts out via closest('[inert]'); the guidelines now require the guard for any new listener. - The drawer moves from z-index 99/98 to 951/950: above the settings action bar (100) and the root EPG/update panels (900/901), which inert removes from interaction but not from paint order — below the CDK overlay container (1000), since dialogs opened from inside the drawer (Manage categories) must stack on top of it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d44045de31 |
test(playlist): fix flaky refresh-preparation worker-event spec (#1331)
The spec raced a fixed 160ms sleep against the service's internal rAF + 120ms paint delay that runs before deleteXtreamPlaylistContent is called. Under parallel jest load the sleep could win, asserting before the mocked worker event was ever delivered. Await a deferred resolved by the mock right after it fires onEvent instead, so the assertion is causally ordered after the signal update. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
94efd7d379 |
fix(workspace): restore playlist info entry in header playlist dropdown (#1328)
The context-actions section of the playlist switcher lost its "Playlist
info" button when playlist actions moved into the per-row menu
(
|
||
|
|
8f861a3a1b |
fix(ui): make the workspace usable on phone-sized screens (#1326)
* fix(ui): make the workspace usable on phone-sized screens
The shell was half-adapted below 640px: the rail flipped to a horizontal
bar but the link lists inside it kept stacking downwards, so the navigation
was drawn outside the bar and over the header (#1100).
Three resizable rails — the shell context panel, the live-layout channel
sidebar and the M3U channel drawer — kept their persisted desktop width,
which left the content around 50px on a 375px screen. They now span the
full width and stack above the content. The inline width written by
ResizableDirective is why these rules need `!important`.
Found while walking the rest of the UI at 375px and 768px:
- The detail hero kept poster and details side by side, squeezing the
action row below its own labels until "Play" was clipped to its icon.
- The settings section list did not scroll and painted over the footer,
which also affected short desktop windows.
- Hiding the M3U channel list on a phone was one-way: the restore handle
was hidden and only Cmd/Ctrl+B could bring it back.
- The live header drew the channel count and the paginator on top of each
other up to tablet width, because the paginator does not shrink and the
meta collapsed to zero width and overflowed its box.
- The search scope checkbox was pushed off the right edge.
Live TV states a floor for the player instead of a ceiling for the lists,
so the video keeps a usable share of the screen under the categories panel
and the channel list.
Closes #1100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — keep the palette reachable and the video visible
Two findings from the Codex review on #1326.
Hiding the command-palette trigger on phones removed the only pointer-driven
way to open it: the rail renders route links plus Settings and emits nothing,
so `commandPaletteRequested` had exactly one source. The button stays and its
keyboard-shortcut label is swapped for an icon instead. Doing that exposed a
latent flex trap in the same row — an <input> keeps an intrinsic min-width
from its `size`, and `min-width: auto` honours it, so the field refused to
shrink and pushed the trigger out onto the buttons beside it.
The M3U drawer released the shared player floor, which on a short landscape
phone (600-640px wide) left the content container at half the shell body.
The inline guide inside it is `flex: 0 0 <basis>` and took its full 180px out
of a container that no longer had it, so the video could reach zero height.
The floor is restored and now yields on short viewports, the video states its
own minimum, and the guide is what gives way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the channel list keep its height on a landscape phone
Follow-up to the review: the player floor added in the previous commit was
measured against the viewport, not against what the shell had left. On a
640x360 landscape phone the stacked categories panel already takes 30vh, so
claiming another 50vh here drove the channel sidebar to zero height while it
was still marked expanded — no way to pick another channel — and pushed the
layout past the viewport.
The floor now applies only where the screen can afford it (`min-height:
600px`), the sidebar states a floor of its own so it cannot be squeezed out,
and the collapsed rule clears that floor so hiding the list still works.
Below that height the two panes simply share what is left.
Portrait is unchanged: categories 244px, channel list 220px, player 240px on
a 375x812 screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — settings nav on landscape, poster dead space
Two more findings from the Codex review.
The stacked settings context panel capped itself at 30vh, which on a 360px
screen is 108px — less than the panel's own title and footer, so the seven
section rows collapsed to nothing behind an overlapping footer. On short
screens the caption gives way (the rail already labels the page), the footer
sheds its tall-screen padding, and the settings variant gets a slightly
larger cap: unlike the live routes there is no player below competing for
height, only a scrollable form.
The poster kept a 330px minimum from the skeleton fallback at the bottom of
the file — sized for the 220px desktop poster — while the stacked phone hero
renders it 140px wide with a ~210px aspect-ratio height. Every loaded detail
page carried ~120px of empty space between the poster and the title. The
override sits after that rule because it wins on source order, not
specificity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the playlist switcher yield to the search field on narrow phones
Codex review of
|
||
|
|
46c7713841 |
fix(ui): preserve EPG in narrow channel rows (#1312)
Preserve current-program context and enabled actions in narrow channel rows while aligning loaded rows, skeletons, and virtual-scroll geometry across M3U, Xtream, Stalker, Favorites, and Recent views. |
||
|
|
32ba209b63 |
fix(portals): restore fresh-import pins atomically (#1311)
* fix(portals): restore fresh-import pins atomically * fix(portals): preserve Xtream restore retry state * fix(portals): serialize Xtream restore revisions |
||
|
|
f80eb4d1b9 |
fix(playlist): open playlists handed over by the OS (#1299)
Opening an .m3u/.m3u8 file from the command line or a file association did nothing. The renderer parsed `process.argv` and sent an `OPEN_FILE` IPC event that had no `ipcMain` handler and no preload channel, so `sendIpcEvent` logged it as an unknown type and dropped it. The path now belongs to the main process, which is where the OS actually delivers it: - argv is parsed on first launch (skipping the executable and Chromium switches) and normalized to an absolute path; - macOS gets an `open-file` listener registered before `whenReady`, since Launch Services never puts the path in argv; - the single-instance guard forwards a second launch's argv and working directory instead of discarding them, so opening a playlist against a running app works too. Requests are queued in the main process until the renderer subscribes to the `OPEN_FILE` push and drains the queue, which closes the startup race. The import itself reuses the existing file path, so persistence, playlist-scoped EPG and the navigation to the new playlist behave exactly like a dialog import; a failed open now surfaces a snackbar instead of silence. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a2fafcfc08 |
test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark * feat(xtream-mock-server): add deterministic 100k fixture * style(xtream-mock-server): apply repository formatting * fix(xtream-mock-server): harden performance fixture data * feat(xtream-mock-server): add performance control plane * docs(performance): correct Xtream capture plan * fix(xtream-mock-server): harden performance controls * fix(xtream-mock-server): harden control lifecycle * feat(performance): add Xtream preload markers * feat(performance): trace Xtream main phases * feat(performance): mark Xtream store publications * feat(performance): trace Xtream database phases * feat(performance): trace Xtream delete cancellation * feat(performance): capture Xtream phase attribution * feat(performance): mark Sources Xtream refresh * test(performance): define Xtream benchmark evidence contracts * test(performance): add Xtream benchmark runner * test(performance): surface failure evidence writes * test(performance): align database read clock * test(performance): preserve capture failure contracts |
||
|
|
24f0dee6f0 |
test(performance): add formal M3U import benchmark (#1287)
* test(performance): add formal M3U import benchmark * test(performance): harden formal capture validity * test(performance): address benchmark review feedback |
||
|
|
f9e71a6d8d | fix(electron): isolate refresh performance correlation | ||
|
|
1ab82b04a1 |
refactor(deps): drop uuid for a shared crypto-based id helper (#1266)
Supersedes #1252 and #872. uuid 14 is ESM-only, apps/web/jest.config.ts only kept v9 working by mapping `^uuid$` at a `wrapper.mjs` that v14 no longer ships, and the specifier also has to be synced in libs/shared/m3u-utils/package.json or @nx/dependency-checks fails lint. All four call sites only used `v4()`, so the dependency goes away instead. `createRandomId()` prefers `crypto.randomUUID()` and falls back to building the same v4 shape from `crypto.getRandomValues()` — that fallback is load-bearing, because randomUUID is only exposed in secure contexts and the self-hosted PWA is regularly served over plain http on a LAN address. getRandomValues stays available there, and it is what uuid's own v4 used. `@types/uuid` goes too; it only existed for the untyped v9 package. |
||
|
|
9ae53e4515 |
fix(playback): make the "Show subtitles" setting reach the web players (#1269)
The persisted subtitle preference only ever had an owner behind the default-off shared web-controls flag, so with the shipping controls it did nothing: Video.js never read it, ArtPlayer declared the input but never used it, and the HTML5 player only ran a one-shot pass after play() resolved — before hls.js had added its text tracks. No portal host bound the input at all, so it never reached Xtream or Stalker pages either. Extract the source-local track controllers into the adapter-free WebVideoSourceTracks and have WebVideoSourceControlsBridge wrap it, so both controls modes apply the preference through the same code. The preference-off players bind it directly (VjsLegacyTracks for Video.js), and WebPlayerViewComponent reads the preference from SettingsStore instead of an input so every host inherits it. The preference means different things depending on who renders the caption UI: shared controls stay authoritative for the session, while vendor chrome is source-default — the preference seeds each new source and is released once the media reports playing, so the engine own caption menu keeps working. Mode selection is an optional playbackStarted probe passed to the HLS, native and Shaka helpers; in that mode the HLS helper deselects the track rather than hiding it, since subtitleDisplay would silently override the vendor menu. Closes #1155 |
||
|
|
3032cfa88d | fix(m3u): avoid persisting hydrated favorites (#1232) | ||
|
|
b4c0cce741 |
fix(backup): export and restore hidden Xtream categories by real xtream IDs (#1224)
Category rows crossed the DB-worker IPC boundary with Drizzle's camelCase property names while the renderer contracts declare snake_case, so backup export dropped hidden-category IDs and restore degraded to a type-only match that hid every category. Project category ops to the declared wire shape, normalize restore state from untrusted sources (dropping entries without a numeric xtreamId), reject entries with missing user-state collections, and add full export→import round-trip coverage (unit manifest-equality + Electron e2e) plus regression tests. Closes #1017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bd07e17857 |
feat(m3u): DASH + ClearKey playback via Shaka Player (#1225)
* feat(m3u): extract ClearKey DRM from #KODIPROP playlist lines Adds the typed ChannelDrm model (shared interfaces) and a KODIPROP post-processing step in createPlaylistObject() — the single funnel for all four playlist import paths. Parses inputstream.adaptive.license_type, license_key and drm_legacy; ClearKey keys accepted as kid:key hex pairs, W3C ClearKey license JSON, or a plain kid→key JSON map. Unsupported license types (Widevine/PlayReady/license URLs) are preserved with supported=false so playback can surface a DRM diagnostic instead of failing silently. Also adds isDashStreamUrl/isDashChannel helpers for DASH routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(playback): add Shaka DASH source engine with ClearKey support Introduces ShakaVideoSession (libs/ui/playback/src/lib/shaka-engine/): a lazily imported shaka-player engine (separate lazy chunk, ~217 KB transfer) owning attach/configure/load with an operation queue and generation guard against channel-switch races. Channel ClearKey config maps to drm.clearKeys; channels with an unsupported license type emit a DrmOrEncryption diagnostic without starting an engine. Shaka errors are classified into the existing playback diagnostics (PlaybackDiagnosticSource.Shaka). Wires the engine into both built-in players like hls.js/mpegts.js: - HTML5: extension === 'mpd' branch in playChannel(); hls/mpegts/native glue extracted to helpers to keep the component within the size budget - ArtPlayer: customType 'mpd' in ArtPlayerSourceSession (+ getDrm seam) - Shared controls: WebVideoControlsSource kind 'shaka' + WebVideoShakaControls using the Shaka 5 text model (selectTextTrack(null) hides subtitles; Player.setTextTrackVisibility no longer exists) Adds a CJS shaka-player jest stub (video.js precedent) for web specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(m3u): route DASH channels to the inline Shaka-capable player DASH (.mpd) channels always play in a built-in web engine (radio precedent): external MPV/VLC cannot receive KODIPROP ClearKey configuration (VLC upstream #29465) and Video.js has no DASH bridge yet. - shouldShowInlinePlayer() bypasses the external-player setting for DASH - new shouldAutoLaunchExternalPlayer() guard consolidates the MPV/VLC auto-launch conditions in the m3u-state effects (incl. catch-up path) - the M3U page overrides the player for DASH channels: ArtPlayer stays ArtPlayer, everything else falls back to the HTML5 player - ChannelDrm is passed through ResolvedPortalPlayback into the synthetic player-view channel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): add offline DASH ClearKey fixtures and e2e coverage Fixtures (apps/web-e2e/src/fixtures/dash/): ~4s VP9+Opus DASH, clear and CENC-encrypted variants with fixed synthetic ClearKey credentials. Content synthesized by ffmpeg; encryption done by Shaka Packager because ffmpeg's mp4 muxer writes senc-only metadata (Chromium needs saiz/saio) and cannot produce the subsample encryption the VP9 CENC binding requires. Generation script + README document regeneration. web-e2e (Chromium): import an M3U with KODIPROP ClearKey via raw text, verify encrypted and clear DASH actually play (currentTime advances, no diagnostic banner) and that an unsupported license type (Widevine) surfaces the DRM diagnostic. Fixtures are served through Playwright route interception with HTTP Range support; the Angular service worker is blocked since SW-routed requests bypass interception. electron-backend-e2e: the same happy path + negative against a local Range-aware fixture server — the automated proof that ClearKey EME works in the real Electron runtime (file:// secure context). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: document DASH + ClearKey playback architecture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pwa): extract KODIPROP DRM on the web-backend /parse import path The web-backend keeps its own playlist builder for the PWA URL-import path, so the shared createPlaylistObject() DRM hook never ran there and encrypted DASH channels imported by URL reached Shaka without keys. Apply extractDrmFromRaw() in that builder too and cover the path with a regression test. Addresses Codex review on PR #1225. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): interrupt stalled Shaka loads and destroy failed engines Two review findings on the ShakaVideoSession lifecycle: - stop()/start() now tear the current player down immediately instead of queueing the destroy behind the in-flight operation. Shaka's destroy() interrupts a pending load() (LOAD_INTERRUPTED), so a stalled manifest fetch can no longer wedge the operation chain and block the next channel start (Codex P1). - A rejected attach()/load() now destroys the failed player after emitting the diagnostic, so a non-functional engine never stays attached to the media element or exposed to the shared-controls bridge (Greptile P1). Regression tests cover both paths. The Shaka fakes are consolidated into a shared jest-free test double that mirrors the destroy-interrupts-load semantic, and the ArtPlayer source-session spec is split (fixtures + DASH cases) to stay within the max-lines lint budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): unify DASH URL detection with playback extension normalization isDashStreamUrl() used the simpler getStreamExtensionFromUrl(), so URLs the player engines classify as DASH (stream.MPD, ?ext=mpd, ?format=mpd) were not routed to the Shaka-capable inline player and lost their ClearKey metadata with Video.js or external players configured (Codex P2). The normalized getPlaybackMediaExtensionFromUrl() now lives in @iptvnator/shared/m3u-utils (re-exported unchanged from the playback lib) and both routing and engine selection share it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lint): satisfy CI lint and CodeQL in DASH support files - replace shell-built tar/npm commands with execFileSync arg arrays in the fixture generator (CodeQL: uncontrolled shell command) - give jest stub methods explicit bodies (no-empty-function) - compact the diagnostic label switches in WebPlayerViewComponent to stay under the max-lines budget Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): tear down the Shaka engine on critical error events too A non-recoverable Shaka error emitted after a successful load left the dead engine attached to the media element and exposed to the shared-controls bridge (Greptile P1, round 2). Critical error events now destroy the player right after the diagnostic is emitted, matching the load-failure path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): honor DASH catch-up URLs and drop unusable DRM fallbacks Two Codex round-2 findings: - The inline-playback DASH gate only examined the channel URL, while the external-player guard checks the resolved catch-up URL — a replay that resolves to an .mpd manifest with MPV/VLC configured ended up with no player at all. The gate now uses the effective playback URL (activePlaybackUrl ?? channel.url). - The unsupported-DRM diagnostic advertised MPV/VLC fallback actions, but external players cannot receive the KODIPROP license config either — the diagnostic no longer recommends them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): suppress unusable external fallback for ClearKey DRM failures Runtime DRM errors on channels that carry KODIPROP ClearKey config (wrong or rotated keys) advertised MPV/VLC fallback actions, but external players never receive the license config — the fallback could only fail differently. DRM-classified diagnostics from such channels no longer recommend external players; clear channels keep the hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): symmetric DASH inline gate and lazy DRM for pre-upgrade playlists - The inline DASH gate is now true when either the channel or the resolved catch-up URL is DASH, mirroring the external-player guard — a .mpd channel whose catch-up resolves to .m3u8 no longer ends up with no player at all. - Playlists imported before the DRM feature carry no drm field, but the raw KODIPROP block survived in the stored items; the M3U page now falls back to extractDrmFromRaw(channel.raw) at playback time, so encrypted channels work without a re-import (Channel gains raw?). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync the DASH/Shaka contract across agent docs Mirrors the DASH/Shaka source-engine contract into AGENTS.md and adds Shaka to the shared web-video bridge descriptions in CLAUDE.md and the player-controls contract; documents the lazy raw-KODIPROP DRM fallback for pre-upgrade playlists in the M3U architecture doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): reset the media element for rejected DRM and widen ClearKey fallback suppression - Switching from a playing stream to an unsupported-DRM DASH channel loads no new source, but play() still ran and the un-loaded element could resume the previous stream underneath the diagnostic banner. The HTML5 player now resets the element instead of playing. - Any inline failure on a KODIPROP ClearKey channel (manifest, codec, media, network — not just DRM-category errors) is unsolvable in MPV/VLC, which never receive the license config; the external fallback hint is now suppressed for all diagnostics of such channels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): restore suppressed DASH captions when the preference re-enables The Shaka bridge dropped the auto-selected text track with selectTextTrack(null) when showCaptions was off, but did not remember it — re-enabling the preference mid-session left captions permanently off (HLS/native bridges already restore). The session now remembers the suppressed track id and reselects it via the bridge's caption-state pass; suppression is also skipped when no track is active. Covered by session and new WebVideoShakaControls specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: retrigger CI GitHub Actions created no check suites for the last three pushes to this branch (third-party apps received the webhooks); an empty commit re-fires the push and pull_request events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(playback): split oversized Shaka session and HTML5 spec files CI lint enforces max-lines 400: extract ShakaTextTrackSuppression and the shaka-error helpers out of ShakaVideoSession, and move the DASH-specific HTML5 player test into its own spec. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: allow manual dispatch of the cross-platform E2E workflow GitHub stopped delivering push/pull_request events for this branch; workflow_dispatch provides a manual escape hatch (CI and build-and-make already have one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d61fd5db19 |
feat: add strip country prefix setting (#1162)
* feat: add strip country prefix setting
* feat: scope country-prefix stripping to live content and cover missing surfaces
- narrow the heuristic: pipes always strip, dash/colon separators only
when the prefix is a short uppercase tag ("UK - BBC One" strips,
"Sky - Sports F1" and "Mission: Impossible - Fallout" stay intact)
- fall back to the original name when stripping would leave nothing
- scope stripping to live content only: grid type (live/itv/radio),
playback isLive, external sessions without contentInfo, dashboard
cards with contentType 'live'
- cover previously missed surfaces: M3U player EPG timeline header,
M3U inline player title, radio player, dashboard live rails
- replace hardcoded settings strings with translate keys and add
SETTINGS.STRIP_COUNTRY_PREFIX(_DESCRIPTION) to all 18 locales
- add unit specs for the utility plus regression specs for
channel-list-item and external-playback-dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover strip-country-prefix call sites for codecov
- dashboard-rail: new spec for cardTitle live/movie/series scoping
- grid-list: strip enabled/disabled, VOD passthrough, 'No name' fallback
- portal-inline-player: live strip vs VOD passthrough
- unified-live-tab: timeline channel name strip + M3U name precedence
- video-player: timeline/radio/inline titles with the setting on and off
- settings-store: default false + persisted true round-trip
- settings-form.utils: new spec for form default and ?? false fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1c710d69ef |
chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps (#1116)
* chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps Removes verified-dead components (0 class/selector references outside their own files): EpgListComponent (+ epg-list-item), EpgViewComponent, LiveEpgPanelComponent, StalkerCollectionChannelsListComponent, NavigationComponent, FilterSortMenuComponent, video-player ToolbarComponent, PortalCollectionShellComponent and LoadingOverlayComponent, together with their barrel exports. Alive code extracted from the deleted trees: - LiveEpgPanelSummary -> libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts - EpgProgramActivationEvent -> libs/ui/epg/src/lib/epg-program-activation-event.ts - epg-list.utils.ts trimmed to the three timeline-used helpers and moved to libs/ui/epg/src/lib/epg-program.utils.ts - epg-item-description/ moved up out of the deleted epg-list/ folder Also removes 18 i18n keys now unused (from all 18 locales), dead CSS selectors targeting the deleted elements, and unused dependencies: lodash (+ @types/lodash), semver, @ngrx/component-store and @videojs/http-streaming (videojs-quality-selector-hls declares no peer dependency on it; video.js 8 bundles VHS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(coverage): move shared-portals to Tier C, fix stale doc references The Tier A gate failed in CI because deleting the dead epg-view and live-epg-panel components removed the only specs in libs/ui/shared-portals. The lib now contains a single type-only interface (LiveEpgPanelSummary), so there is no runtime code to unit test; reclassify it to Tier C with a documented reason, matching the gate's own guidance. Also update remaining doc references to the deleted components in docs/architecture/stalker-epg.md, iptvnator-ui-guidelines.md and CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0b526b119a |
feat(epg): add vertical list view for the live EPG panel (#1115)
* feat(epg): add vertical list view for the live EPG panel
Add an EPG list view — a vertical, single-day programme list — as an
alternative rendering of the live EPG panel, selectable via a new
Settings → EPG → "Guide view" toggle (epgViewMode: 'timeline' | 'list',
default 'timeline' so existing users see no change).
- New EpgListViewComponent (app-epg-list-view) mirrors
EpgTimelineComponent's input/output contract 1:1, so all four live
hosts (M3U player, unified live tab, Xtream, Stalker) swap the panel
with a plain @if and identical bindings.
- Reuses the shared view-agnostic EPG modules (classifyTimelineWhen,
hasProgramsForDateKey, epg-archive.util catch-up gating,
epg-summary.util collapsed-summary maths, epg-date helpers,
EpgProgrammeDialogService, app-epg-timeline-empty-state) — no
duplicated logic.
- Rows show time range, title, optional description, live progress on
the on-air row, catch-up "Watch" on past rows when archive playback
is available, and a details dialog; keyboard activation guards
nested buttons (target === currentTarget).
- Auto-focuses the on-air row on channel select, restores it across
collapse/expand remounts, and shows a sticky in-flow "On now" strip
(never overlaying rows) when the current programme is scrolled away;
all scroll maths is rect-based relative to the scroller.
- List mode raises only the inline panel height via an epg--list
modifier (--epg-inline-height clamp); timeline and collapsed heights
are unchanged.
- Setting flows end-to-end (Settings interface → DEFAULT_SETTINGS →
SettingsStore/StorageMap → segmented control in the EPG section);
Electron-only UI, PWA stays on the timeline default. i18n keys added
to all 18 locales.
- Tests: new component/row/utils/scroll-controller specs, settings
persistence spec, swap tests in all four host specs, and Electron
E2E for the settings round-trip and the rendered list view. Docs
updated (m3u-playlist-module.md).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(epg): address list-view review findings from Codex and Greptile
- Reset the list view to today when a new channel's programme set
arrives while the user is parked on another day (timeline parity):
the scroll controller now keys by the full programme-set identity
(programsFocusKey) and commits today before focusing, instead of
silently stranding the new channel on the stale day. (Codex P2)
- Centralise the 'timeline' fallback as a resolvedEpgViewMode computed
on SettingsStore; the four live hosts consume the derived signal
instead of duplicating the `?? 'timeline'` expression. (Greptile P2)
- Extract the component's reactive plumbing into
registerEpgListViewEffects(), bringing the component back under the
300-line guideline (290). (Greptile P2)
- Controller spec rewritten around programme-set fixtures with new
coverage: return-to-today on channel switch, day navigation left
alone, no-takeover when today has no data, empty-set no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(epg): drop malformed programmes from the list-view day filter
Reject programmes whose stop is not after their start in
buildEpgListRows — same as the timeline's buildTimelineBlocks. Bad
provider data would otherwise render impossible time ranges and could
even be offered as catch-up playable. (Codex P2 on
|
||
|
|
401012eac4 |
feat(epg): rework live EPG panel into a horizontal timeline (#1102)
Replaces the vertical EPG list with a shared horizontal `app-epg-timeline` ribbon across all live surfaces (M3U player, unified live tab, Xtream, Stalker): zoom, day navigation, short-programme grouping, catch-up/timeshift, and per-state empty views. Backend gains timezone-aware `datetime()` comparisons, unscoped source fallback, non-ASCII candidate matching, and chunked candidate queries. Timeline split into reusable, view-agnostic modules (archive/summary/dialog service/render util/scroll controller) for the future EPG list view. Fixes landed during review: - honor the controlled `selectedDate` input (seed via linkedSignal) - restore ribbon position across collapse/expand - keep the ribbon mounted when scrolling across a gap day - don't trigger block playback on Enter from nested watch/info buttons - don't reset timeshift playback on the 30s now-tick during EPG gaps Greptile 5/5 (safe to merge); Codex clean; CI green. |
||
|
|
3cb228d931 |
fix(m3u): highlight active archive epg program (#1097)
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. |
||
|
|
e801028005 |
feat(epg): support playlist-scoped sources
Add playlist-scoped EPG source support for M3U playlists. |
||
|
|
89916af9d8 |
feat(search): add workspace global search with M3U support
Moves global search into a routed workspace view, adds M3U live/radio results, lazy pagination, and DB-backed matching improvements. |
||
|
|
20d0f01428 | fix(xtream): address portal review feedback | ||
|
|
254879f92b | fix(xtream): improve portal compatibility | ||
|
|
bc4a5958dc |
fix(remote-control): apply volume to built-in M3U players
Closes #1061 |
||
|
|
dfdb5bb8ad |
fix(playlist): save xtream details in pwa
- 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 |
||
|
|
e8aa7c3a34 |
[codex] Add scoped EPG security trust controls (#1054)
* Add scoped EPG security trust controls * fix: address scoped trust review feedback --------- Co-authored-by: 4gray <fourgray@proton.me> |
||
|
|
701cf23299 | chore(frontend): clean logging and lint suppressions | ||
|
|
003a8774a1 |
fix(electron): tighten navigation review feedback
Restrict packaged file navigation to the app renderer, guard redirects, and handle fire-and-forget header override IPC failures. |
||
|
|
fc449ca73e |
feat(electron): harden renderer security
Scope playback request header overrides to active stream origins and document the Electron runtime security contract. |
||
|
|
ed8680116c | fix(runtime): address consolidated review feedback | ||
|
|
4603bd83d5 | refactor(runtime): gate playlist refresh by capability | ||
|
|
3e398f1d1a | refactor(remote-control): gate live bridge by capability | ||
|
|
61cfe64951 | refactor(playlist): use runtime capability for xtream sections |