mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
da9f99f3f247a7de0ae0675db9e5bc5ffe4f57d8
613
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
da9f99f3f2 |
fix(server-storage): a restore merged by a conflict stays parked; first-read tests wait for the read
A restore whose write conflicted with an edit another device made during it had the conflict replay merge that edit in, and flushed() still said the lists were taken, so the restore was consumed with lists that were not the backup's. Both restore paths now compare the stored lists with the ones they wrote once the server took them, and a difference keeps the restore parked to run again on a fresh read. The two first-read tests released the mocked read after a fixed number of microtask turns, which could come before the read started; they now wait until it has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c65502a65d |
fix(server-storage): a duplicate leaves a running tab's kept page loads to it
A duplicated tab copies the tab's session storage, so after a reload that kept older page loads its pointer lists them too, and the duplicate took them over at once, although the tab still running owns those edits and may have newer ones queued; its replay could then write older watch progress over newer. When the previous page load still runs, the kept ones now wait with it and `touch` adopts them, oldest first, once it no longer looks live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b3a493fa7f |
fix(server-storage): a reload finds page loads a reload before it kept; restore master's tick baselines
When storage refused a reload's copy of the last page load's saved writes, the old copy stayed but only this page's memory named it: the tab's pointer already named the new page, so a second reload before any copy was saved recovered from the empty new page, and without a registry record no other tab could find the old one. The pointer now also lists the page loads a page keeps (keepPages), and a reload takes them over, oldest first, before its last page load. The pointer helpers move to server-page-pointer.ts. Both merges of master resolved journey-baselines.json by taking this branch's whole file, which dropped #1929's cdTicksToFirstCard, cdTicksIdle30s and cdTicksToFirstPage. The file is master's again with only this branch's initialBytes and domMutationsToFirstCard raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f4d3d66164 |
Merge origin/master into claude/server-storage-pwa
Resolves tools/performance/journey-baselines.json: master's #1929 (zoneless renderer) lowered renderer.initialBytes to 1,278,278; the merged tree measures 1,285,523 locally, so the branch's raise is now 7,245 bytes (0.6%), with its note updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6e44aef3c8 |
fix(services): writes wait for Delete All before they queue; restores replace only settled lists
Delete All, third variant: a write that arrived after Delete All waited for it inside its playlist's queue. A Stalker repair that reserved that playlist meanwhile skipped the barrier, but its write still queued behind that waiting one, while the reset waited for the repair's reservation. While a Delete All runs, writes now wait for it before they join their queue, so no queue holds work blocked on the reset; a write under a reservation held here joins at once. A restore replaced lists while this page's own write for the playlist was still unsent (refused, or lost): that write's edits joined the replacement and a conflict replay kept items the backup did not have. Both restore paths now require the playlist's writes to be on the server first (flushed), and otherwise stay parked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37210ead25 |
perf(web): run the renderer zoneless and drop the zone.js polyfill (#1929)
* perf(web): run the renderer zoneless and drop the zone.js polyfill Plan item C6 step 5. app.config.ts provides provideZonelessChangeDetection() directly and apps/web builds without the zone.js polyfill, so a tick runs only when a signal, markForCheck, an Angular event or a render hook asks for one, not after every macrotask. The flag from #1824 (change-detection.providers*.ts and the *-zoneless configurations) goes away; a build-config test fails if the polyfill or a zone provider comes back. NgZone.run/runOutsideAngular calls that only mattered with zone.js are removed from the dashboard data services, the settings unload guard and the embedded MPV position poll (with the spec case asserting it), and the unused polyfills.ts, polyfills-test.ts and setup-jest.ts are deleted. Unit tests keep zone.js/testing for fakeAsync and waitForAsync; apps/remote-control-web still bootstraps with zone.js. renderer.initialBytes: 1,596,201 -> 1,545,931 bytes (-50,270) on a local production build of this base; the baseline is lowered to the measured value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): cite #1929 as the initial-bytes baseline evidence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): ratchet the change-detection ticks that zoneless made deterministic On the canonical runner (Performance ratchet run 38359088033, three runs of this branch, five iterations each) renderer.cdTicksToFirstCard read 5, renderer.cdTicksIdle30s 3 and renderer.cdTicksToFirstPage 9 in all 15 iterations, against 31, 1 to 4 and 22 with zone.js. They become baselines with zero slack and the Performance journeys job checks them. J3 and J4 tick counts still vary by one and stay unenforced; J3 wall clock is in master's range, so the local rise measured on #1824 was machine load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): allow J1's one-tick lazy-chunk race in the first-card tick baseline After merging #1931 (Angular Material off the startup path) the Performance journeys job read renderer.cdTicksToFirstCard 5, 5, 5, 5, 6: the lazily loaded chunk resolves on either side of a render. All 15 iterations before it read 5. The baseline stays 5 with one tick of slack and a note; a return towards zone.js levels (31) still fails, and the idle and first-page tick baselines stayed exact in the same run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): find a finished programme for the phone dialog after UTC midnight The Xtream mock lays its 30-minute EPG slots out relative to now, and the list view shows the programmes that overlap the selected day. In the first half hour after midnight (the spec pins TZ=UTC) today holds only the programme on air, so 'stacks the programme dialog actions on a phone' found no data-when=past row and failed every retry on #1929's Windows shard (00:18-00:23 UTC). The spec now waits for the list, and steps back a day when today has no finished programme yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(architecture): record the zoneless renderer in the workspace contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dd143c8aca |
fix(server-storage): a re-read keeps writes another tab had unconfirmed when it went out
A re-read checked other tabs' unconfirmed entries only after its answer arrived. A tab whose write was unconfirmed when the read went out (its edit already in the snapshot) and confirmed before the old answer came back matched neither check, so the stale answer replaced its saved value in the shared cache; that tab's next edit, holding the newer tag, then wrote the stale list and deleted the item on the server. The entries unconfirmed when the read is sent are kept too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c4425c4c8d |
fix(services): Delete All drains queued writes before its reset; reserved writes skip its barrier
The previous commit raised Delete All's barrier inside the exclusive authority reset and drained queued writes there, but a queued deletion or import takes the shared side of that lock: the reset waited for it while it waited for the reset. Delete All drains the writes queued before it outside the lock again, then takes the reset, with its barrier up from the start. The deadlock with a Stalker repair (or Edit) that holds a row reservation and writes under it is broken on the other side: such a write does not wait for the barrier (holdsPlaylistAuthority, counted per reservation in this context), and the reset waits for the reservation as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1059224d79 |
fix(server-storage): Delete All waits for repairs holding a reservation; backups read the server again
Delete All raised its write barrier before it held the exclusive authority reset. A Stalker repair already holding its row reservation then waited on that barrier for its conditional write, while the reset waited for the reservation: neither finished, and later playlist operations stalled behind them. The barrier now goes up inside the reset, once repairs in flight have finished; writes queued so far land first and new ones wait for the delete, as before. ready() said yes after any earlier read, so an export could miss what another device added since, and restoring an empty list did nothing when the cache was already empty. Backup export, restore and the restore replay now ask for a fresh read (ready(true)) and fail, the restore staying parked, when it does not succeed. The facade's adoption loop moves to the adopt module to keep the facade under its line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
954a335f31 |
fix(server-storage): the move uploads playlists create-only and merges into a copy moved first
Two tabs or devices moving browser copies with the same playlist id both planned an upload; the browser documents carry no served version, so both PUTs were unconditional and the later one overwrote the first copy (and any change made on it since), with both moves reporting success. The backend's playlist PUT now honours `If-None-Match: *` (412 when the playlist exists; checked and written in one synchronous handler). The move uploads with it through ServerPlaylistDocumentStore .dbCreateAppPlaylist, and an upload that finds its id taken merges its favorites and history into that copy instead, counted as merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f6a216f59a |
Merge origin/master into claude/server-storage-pwa
Resolves tools/performance/journey-baselines.json: master's #1938 lowered renderer.initialBytes to 1,329,249; the merged tree measures 1,338,342 locally, so the branch's raise is now 9,093 bytes (0.7%), with its note updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3b2af73baa |
style(ui): ADR and one global kit layer for menus, select panels and destructive buttons (#1936)
* docs(ui): record the Angular Material role decision in the UI guidelines Angular Material stays the behaviour and accessibility layer; the look comes from the design kit through mat.*-overrides() on --app-* tokens in the shared partials, kit-only components build on CDK, and plain actions may become native buttons. Records the measured Material and CDK share of the initial bytes, why a replacement would not pay, and the v22 behaviour changes to keep covered in E2E. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * style(ui): one global kit layer for menus, select panels and destructive buttons - `<mat-menu class="app-menu">` gets the kit panel from one rule in styles.scss; the two lazy components drop their `::ng-deep` copies. - Select option panels follow the same panel: bordered, 10px radius, 6px padding, 36px rows at 13px, the chosen row on the list-selection tokens. - `.app-destructive-button` is the kit's red tint on the `--app-danger-*` tokens (tinted fill, red text, no ripple, 40% opacity when disabled) instead of a solid error fill, for filled, text, outlined and icon buttons. - The backdrop blur is scoped to the dark backdrop the CDK gives modal overlays; menus, selects and popovers open over a transparent backdrop that no longer matches the rule. - `mat.elevation-classes()` is dropped: nothing uses `.mat-elevation-z*`, and its 25 rules cost 4.4 KB of the initial stylesheet. The stack now measures 1,595,843 initial bytes (was 1,597,764). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(e2e): wait for focus to enter an opened menu before navigating it Material focuses a menu's first item after the next render, which the zoneless scheduler runs on a later task than the keydown that opened it. The panel is visible before that, so End sent as soon as it appeared still reached the trigger: on the Linux runner the Sources row-menu delete test then found "Remove playlist" inactive on every retry. `waitForMenuFocus` polls for the active element inside the open menu; the Sources keyboard and switcher keyboard tests use it before their first navigation key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ced7386dfa |
perf(web): render the radio play control as an icon button, not a fab (#1938)
The audio player's play/pause control was the app's only mat-fab. It is now a mat-icon-button drawn as the same 56px primary circle (icon-button overrides: 56px state layer, 32px white icon, 50% shape, selection-colour fill, hover scale), so the eager button chunk no longer carries the fab directives and the theme no longer emits the fab tokens: renderer.initialBytes 1,349,048 -> 1,329,249 bytes (-19,799: -13,799 in button.mjs, -6,028 in the stylesheet), measured locally against the merge base. The fab also reset the keyboard focus ring with `outline: none`; the icon button keeps the app ring. - tools/nx/check-material-theme-scope.mjs: `button` brings button and icon-button; `fab` is required only while a runtime template renders mat-fab / mat-mini-fab (matFab / matMiniFab), found through the component templates the way check-form-field-defaults.mjs resolves them. That resolution moves to tools/nx/component-templates.mjs, shared by both. Templates know HTML comments only, so the scan strips just those: a `//` in text or an attribute value no longer hides a fab on the same line. - apps/web/src/m3-theme.scss drops `fab` from $material-components. - apps/web-e2e: the Stalker radio test checks the control's size, shape, fill, icon and keyboard focus ring. - journey-baselines.json: renderer.initialBytes 1,345,807 -> 1,329,249. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
5a90fd6c30 |
fix(server-storage): a closed page record stays closed when a late write rewrites it
On pagehide the page flushes with keepalive and then marks its record closed. When writes were held (offline), the keepalive write fails at once and its requeue saved the journal again a microtask later, which rewrote the record without `closed`: the reload took the previous page load for live and left its favorites and progress unsent until the record aged out (ten minutes). markUnconfirmed now keeps an existing record's closed flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37fac60d92 |
fix(server-storage): M3U favorites by URL and history channel ids in the move; restores wait for the server
The move's M3U remap swapped references for server channel ids, but the app looks favorites up by stream URL and history entries carry the URL as their id: legacy favorites became invisible and history duplicated. Legacy id favorites now become their channel's URL on both sides, and history keeps its URL id with the server's channel id as channel_id. A restore in server mode reported success once its lists were queued: a list the server refused (413) left the old data while the parked restore was consumed. Both restore paths now wait for that playlist's writes (UserDataStorage.flushed, a few flush rounds for conflict replays) and stay parked when the server did not take them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
797aeae307 |
fix(server-storage): conflict-safe Stalker metadata migration on the server; newest document views in the move
The Stalker metadata migration wrote server rows back through the unconditional bulk upsert, from the list read: a device that edited a Stalker playlist in between (or a second browser migrating the same copied database) lost its change. Server rows are now read again one by one and written as conditional mutations on that version; a conflict leaves the flag unset, so the next start retries. Moving a document's history kept the server's copy of an item both held, even when the browser had viewed it later, and skipped the write when the length did not change. The combined history is now newest first by added_at, one entry per item, and any content change is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dfe48f59a0 |
fix(server-storage): a saved write without a readable tag counts as saved; unverified online pages keep retrying
A write whose headers said saved but whose body (with the new tag) was cut off was retired without confirmation: a newer queued edit kept its steps and old tag, and a later conflict replayed the saved step over a newer value from another device. It now counts as saved on an unknown version: the landed steps drop out and the next write conflicts, so only edits made after it replay. Writes that were unconditional stay so. The keepalive path does the same. A late answer to a request sent before an outage can set an unverified page online; with nothing to sync the retry then never ran and the token stayed withheld. The retry now also runs for online pages that are not verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
252ef9506f |
fix(server-storage): the immediate offline check runs once per retry interval
A response whose headers arrived but whose body was cut off reports success and then offline, which started the next immediate instance check at once: repeated cut-off answers kept requests going and the connection indicator flickering. The check on going offline now runs at most once per retry interval; later failures wait for the timed retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
769ed4b246 |
fix(server-storage): interceptor and raw fetches report backend outages too
Only the storage API client reported an unreachable backend as offline. When the first sign of an outage was an HttpClient call through the interceptor (provider targets, playlist parsing) or one of PwaService's raw fetches (Stalker, connectivity reset), the instance stayed verified, and the next request after a replacement instance came up at the same address would carry the old token. Both now report no answer (status 0, a failed fetch) and a gateway's 502/503/504 as offline, so the session checks /instance first. The backend's own JSON 502 for a failed provider is an answer and changes nothing; neither does a fetch this page aborted on its own timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
614b0d3500 |
Merge origin/master into claude/server-storage-pwa
Resolves app-startup-status.component.ts: master replaced the progress spinner with static markup (its import goes), the branch's router import for the sign-in route stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e286a62b2e |
fix(server-storage): a superseded page sends and saves nothing; re-reads keep other tabs' newer saves
A suspended tab's retry timer could run before it was shown again: touch() recreated the record another tab had removed when it adopted the page's writes, so the page sent them again over newer edits. The facade now treats a page whose record is gone as superseded: the queue sends nothing (maySend hook), its journal and record are not written again, and the retry timer reloads it like a page shown again. A re-read answered after another tab saved an entry no longer writes the older answer over the shared cache: entries changed while the read travelled are kept, with an unknown tag, so the next write here conflicts and replays instead of removing what the other tab saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7550a22a41 |
test(workspace): cover the atomic route swap and document it (#1922)
* fix(workspace): swap routed pages in one task when returning to the dashboard Leaving an Xtream portal section for the dashboard inserted the dashboard below the still-present portal page: the @angular/animations renderer queues every DOM removal until the end of the next change detection, while the router inserts the new page at once. With event coalescing that tick runs in the next requestAnimationFrame, so Chromium laid the document out with both pages (the dashboard at y=720 in a 720 px window) and scored the swap as one layout shift: 0.337 at 1280 px, 0.394 at 900 px, 0.723 at 420 px, sources lib-workspace-dashboard-rails. Nothing uses @angular/animations triggers (Material animates with CSS), so the app no longer provides provideAnimations(). The old page now leaves in the same microtask, before the new one is inserted, and the swap records no shift. The router itself was already atomic: NavigationEnd and the shell's currentUrl follow activation synchronously. app.config.spec.ts keeps the animations renderer out; workspace-route-swap.e2e.ts asserts that no mutation checkpoint holds both pages and that neither the dashboard nor the content pane shifts. Both fail with provideAnimations() restored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): read scroll and focus state before a view's DOM detaches Without the @angular/animations renderer a destroyed view's DOM leaves before its destroy hooks run, which the deferred removal used to hide. Two hooks read state from the detached DOM: - CategoryContentView.ngOnDestroy saved the grid's scrollTop, which reads 0 once detached, over the offset saved when a card was clicked: back from a VOD detail landed at the top of the list (catalog-sorting E2E on every OS). The view now keeps the offset of the grid that scrolled last; a grid rendered since (a new category's) saves the top. - Chromium blurs a focused element synchronously while removing it, so removing the focused player settings panel fired a focusout without a relatedTarget before the panel's destroy hook, which cleared focusInside: Escape left focus on the page instead of the tune button (player-controls-keyboard web E2E). The panel now judges a focusout a microtask later: a removed panel is detached then, while a pointer release blurs a panel that stays attached and still drops focus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
52d733e1c7 |
fix(server-storage): offline recheck checks only the instance; sign-out always navigates
The recheck on going offline ran a full refresh: when /instance answered but /user-state kept failing, each failed read set the page offline again and started the next one at once, bypassing the retry interval. It now checks the instance only; reads wait for the retry. Sign-out navigates to sign-in itself, also when the page is signed out already: a guard (unsaved Settings) may have cancelled the redirect the first time, and the state change that drives it does not repeat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cfda131c41 |
fix(server-storage): suspended tabs give up adopted writes; history stays newest first; favorite checks wait
A background tab the browser suspended past the record lifetime can have its writes adopted elsewhere, like a back-forward cache page: shown again, it now checks resumed() and reloads instead of sending them. Conflict replays appended a newly viewed item at the end of the history, where the next 50-item cut dropped it; a replayed recently viewed list is now kept newest first. isFavorite() waits for the first user-data read like the list reads, so a detail page opened during a slow first read does not toggle an existing favorite the wrong way. A parked backup restore replayed when the playlist opens waits too, and stays parked while no read succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
90a31d9be9 |
feat(sources): redesign the Sources page and the playlist switcher (#1932)
* feat(sources): add the shared source row and credential-free formatting One app-source-row (regular for the Sources page, compact for the playlist switcher) with an app-source-tile, built on pure helpers: - resolvePlaylistSourceKind: one provider rule for icons, filters and counts (the list filter and the panel counts used to disagree). - sourceDisplayName / sourceDisplayUrl: a name or URL never shows a password, token or MAC. URL imports that stored "get.php?username=…&password=…" as the title now show the host. - stripUrlCredentials (shared/logging): drops what the log redactor would mask, plus short user/pass keys. - formatSourceDate / sourceMetaParts / sourceStatusTag: one meta line order and Intl dates and durations in the UI language. - SourceStatusService + SourceHealthHistoryService: one status per source (desktop health check, browser Xtream portal status) and the last conclusive result kept across restarts, so "not responding for 3 days" survives a relaunch. No URL or credential is stored. Also fixes playlistNameHasCredentials answering differently on repeated calls (global regex lastIndex), and adds the UI-kit button, menu and segmented-control mixins (libs/ui/styles/_app-controls.scss); menus opt in with class="app-menu", styled by the components that use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(sources): rebuild the Sources page on the shared source row - Side list (UI-kit side list): title with the playlist count and an Add button, a Type group, and on desktop a Status group (Any / Responding / Not responding). The two filters combine and persist; each group counts within the other's choice. The chosen row uses one selected-row token (soft tint, outlined in light theme). - Page: a 28px title with the visible count instead of the bordered header bar; Clean up and the sort menu are secondary buttons. Clean up shows how many sources do not respond. Sort adds Last updated, Most channels and (desktop) Not responding first, and keeps Name Z-A and Custom order. - List: app-source-row with hover actions (Refresh, Edit, ⋯); a failing row keeps Retry in view. "No matches" offers Reset filters. Dragging in custom order no longer reuses positions of rows a filter hides. - Delete with Undo: no confirmation; the row leaves at once and the delete runs when the toast closes (its close button commits at once). Deleting the active source switches to the next row. A large Xtream delete shows its row as "Removing…" with progress and Cancel, as before. - The list's refresh flows moved into SourceListRefreshService; E2E and performance drivers follow the new markup and delete flow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(switcher): restyle the toolbar trigger and open it with Cmd/Ctrl+P - The trigger is a 40px secondary button: the active source's tile with its status dot, its name and "type · channels" (or the source count), and a chevron that turns while open. The name comes from sourceDisplayName, so a URL-named playlist no longer shows its URL or credentials in the toolbar. - Cmd+P / Ctrl+P opens the switcher wherever it is shown (Cmd+K stays the command palette) and is listed in the keyboard shortcuts dialog. - The trigger's inline refresh button is gone; refresh stays in the playlist's ⋯ menu and the command palette. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(switcher): rebuild the playlist switcher as an overlay popover The switcher's mat-menu becomes a CDK connected overlay (app-playlist-switcher-panel), 440px wide, with a dimmed, blurred backdrop; only its list scrolls and row menus open above it. - Header with the source count, Add playlist and All sources. - Search by name or host, autofocused; the type filter is one segmented control (All / M3U / Xtream / Stalker with counts) instead of three checkboxes, remembered between openings (the old checkbox setting is migrated). The search is no longer kept between openings. - "Recent" (the three most recently opened, kept across restarts) above "All" in the Sources page order, as compact source rows: the current one tinted with a check. Each row's ⋯ menu holds Refresh, Playlist info, Account info, Check again and Remove (with Undo); the footer's info, account and add actions moved there and to the header. - Keyboard: a listbox driven by aria-activedescendant from the search field. Arrow keys move and wrap, Enter opens, Shift+F10 opens the row's menu, Escape closes and returns focus to the trigger. Hints in the footer. - Deleting from a row menu moves focus to the toast's Undo, since the row (and its focus) left the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(sources): remove the old source row, filter state and dead styles - Delete app-playlist-item and SourceHealthIndicatorComponent, replaced by app-source-row and its tile dot. - Delete the ngrx type-filter slice (FilterActions, selectedFilters, selectActiveTypeFilters); the Sources filters live in SourcesViewStateService. - Drop the empty state's unused "no results" variant and its styles. - The switcher and Sources list components are no longer in the max-lines baseline. - Docs: UI guidelines (source row, Sources page and switcher, status dot, selected-row tokens, UI-kit mixins), the source-health contract and the zoneless inventory. Release notes for the redesign and the credentials fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(catalog): keep the refine menu whole in short windows The refine menu held 12 rows at 48px plus two section titles (657px). Angular Material 22 sets no max-height on a menu panel, so below ~800px the overlay was pushed up over its own Refine button, and at 600px the last thresholds were clipped behind an invisible scrollbar. The minimum-rating thresholds now open in a submenu from one row that names the active threshold. The menu is about 390px and opens below its button at 600px. The submenu trigger sits in a role="none" wrapper: Material offsets a submenu by the first direct mat-menu-item's offsetTop, and with the sort rows inside a role="group" that item was the trigger itself, which opened the submenu 335px off screen. The Electron E2E opens both menus by keyboard in a 600px-tall window and checks that each lies whole on screen without scrolling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(workspace): drop the side panel's divider line The context panel beside Sources, categories, Settings and collections drew a 1px outline-variant border against the content surface, whose background, shadow and rounded top-left corner already mark the edge; the line clashed with that corner. The border stays 1px but transparent, so the panel width, the persisted sidebar widths and the resize handle's position do not move. The handle keeps its hover and drag highlight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(switcher): keep the search field and type filter whole with many sources The switcher popover is a height-capped flex column. With a long list (hundreds of sources) the browser shrank every part in proportion to its size, not only the list: the type filter, which clips its overflow, collapsed to 2px and showed no labels, and the search field dropped from 36px to its 21px text line. The header, search, type filter, empty state and footer are now flex: none, so only the list shrinks and scrolls. The search field now matches the header and side-panel search fields: the shared --app-search-* tokens, a 10px radius, an 18px icon, the same text size and focus halo. The Electron E2E opens the switcher in a 320px-tall window and checks the search height, the type toggles and the scrolling list; it fails without the fix (search 21px). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): checks behind filters, keyboard Undo in the switcher, check causes Local review round 1 (Codex + Greptile) on the redesign: - A saved Responding / Not responding filter hid every source without a result, and the hidden rows were what started the checks, so the list stayed empty. keepSourcesChecked (portal shared data-access) now keeps every library source checked while the Sources page lives, on desktop where a check is one shared job that rows join. - Removing from the switcher left its focus trap open around the search, outside of which the Undo toast sits: Tab could not reach Undo. The popover now closes first; Undo takes the focus and returns it to the trigger (SourceRemovalOptions.returnFocusTo). Every switcher action closes through one path, so a closing row menu no longer pulls the focus back into the popover. - A running desktop Xtream delete brought its source back into the switcher as an ordinary option that could be opened or removed again; the switcher now leaves it out until the delete ends (the Sources page shows its progress and Cancel). - Undo re-activated the removed source over a source the user had chosen since, when nothing had replaced it (it was the only row a filter showed). Undo now restores only while no other source is active. - The switcher highlight stays on its option, or in range, when the list changes under the same search, so Enter still opens something. - The status dot named every failure "Could not connect". SourceStatus now carries the check's cause (access denied, account disabled, timeout…, kept with the health history and validated on read), and M3U dots again say the check covers the playlist URL, not its channels. Tests: new specs for keepSourcesChecked, the saved-filter check, the Undo guard and focus return, the switcher close/highlight/deleting cases and the dot's cause; the filter and Undo specs fail without their fixes. The switcher keyboard E2E now deletes and undoes from the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): own Undo timer, per-connection health history, race-free perf waits Local review round 2 (Codex + Greptile): - The delete committed whenever the shared snack bar was dismissed, so another message (a refresh finishing elsewhere) cut the four-second Undo window short and deleted the source early. The window is now the service's own timer; only the toast's close button (commitNow) or a second delete commits sooner. - The health history was keyed by playlist ID only, so an edited address or account inherited the old connection's result, failing run included. Entries now keep a 32-bit FNV-1a fingerprint of the source health key (no URL or credential); a result for another connection starts over and a stale entry no longer shows. - The M3U refresh-cancellation probe still queried the removed row's .cancel-btn, .busy-state__message and .action-spinner, so its cancel never fired; it now uses the source row's selectors (smoke run passes, cancelEffect=true). - The Xtream delete benchmark waited to catch the "Removing…" row coming back before it left, which a fast delete could finish before the wait began. The driver now arms a delete-playlist operation window before the commit click and ends on its completed event and the absent row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): sort by the shown name; start the benchmark's Undo window in its trigger Local review round 3 (Greptile; Codex was clean): - Name order compared the stored title, while the rows show a URL-named or credential-bearing source by its host: a source stored as get.php?username=… sorted by its hidden account details. sortPlaylists takes the list's name function, and Sources and the switcher pass sourceSortName, the name they display. - The Xtream delete benchmark clicked Remove while preparing, which starts the four-second Undo window; capture setup before the trigger could outlast it, so the delete committed on its own before the measurement and the close button was gone. Preparation now stops at the open menu, and the trigger clicks Remove and then the toast's close button, with the trigger time taken right before the commit click as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): never hand over to a leaving source; browser checks per connection Local review round 4 (Codex + Greptile): - Removing the active source handed over to its neighbour even when that neighbour's own delete was still running ("Removing…"), activating a source about to disappear. replacementAfterRemoval (playlist shared util) picks the nearest row that is not leaving, on the Sources page and in the switcher; SourceRemovalService also refuses a leaving replacement. - In the browser build a "Check again" for an account the user then edited could finish last and overwrite the new account's status under the same playlist ID. Portal statuses are now keyed by playlist and connection fingerprint, so the old check lands under the old key. - The switcher scrolled the highlighted option into view through a selector built from the playlist ID, which a restored ID with "." or ":" broke (or made throw). It now finds the row by its exact DOM ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep ⌘P out of open dialogs; redact refresh errors Local review round 5 (Greptile; Codex was clean): - Ctrl/Cmd+P opened the playlist switcher over an open dialog (an editor, a confirmation), taking the focus out of it and handing it back to the toolbar behind the dialog on close. The shortcut now leaves the key alone while MatDialog has open dialogs. - SourceListRefreshService logged a failed refresh's raw error, which can carry the playlist URL and its credentials. It now logs through the redacting portal logger (createLogger). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): show a new search's first match; keep inconclusive check causes Local review round 6 (Codex; Greptile was clean at 5/5): - Typing a search or picking a type in the switcher highlighted the first match but kept the list scrolled where the previous result left it, so the highlighted row could sit above the view. The list returns to the top whenever the search or type changes. - A source with no kept history whose check was inconclusive (connection checks paused by the connectivity guard, a cancelled check) lost the live cause and time, and its dot said only "Not verified". The status now keeps them; a running check still reads as checking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(switcher): fall back to All when the remembered type has no sources left Local review round 7 (Greptile 5/5 with one P2; Codex was clean): when the last source of the remembered switcher type was removed while two other types remained, the list kept filtering by the missing type. Its segment was gone, no segment was selected, and the list said "Nothing found". The list now filters by All until a source of that type returns; the remembered choice is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): clean credentials nested in kept URL parts Codex bot (PR review, P1): stripUrlCredentials copied every non-sensitive parameter value unchanged, so a URL nested in one (a proxy's ?url=http%3A%2F%2Fjoe%3Ahunter2%40origin.test%2F…) kept its userinfo and password and reached the Sources row through sourceDisplayUrl. Kept query and fragment values and path segments are now cleaned recursively, up to four levels: a nested URL loses its credentials the same way, a nested parameter list loses its sensitive keys, and a part that still names a secret (joe:hunter2@host, token=…) is dropped. Deeper nesting is dropped rather than shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep fragment and nested URL secrets out of source names Greptile bot (PR review, P1 security): an unnamed import of https://host.test/list.m3u#api_key=secret123 stored list.m3u#api_key=… as its title. isUrlDerivedName only knew names ending in a query, and sourceSecrets only read the top-level query, so the row and the toolbar showed the secret while the displayed URL hid it. A name that ends the URL with a query or a fragment is now URL-derived, and shows the host. The secrets a typed name is checked against now include sensitive fragment parameters and, as stripUrlCredentials cleans them, the credentials of URLs and parameter lists nested in parameters (nested account names included). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep deletes busy until settled and on screen; refresh E2E via the switcher PR #1932 round 1 (CI + local Codex/Greptile on the bot fixes): - CI: category-management.e2e refreshed the active playlist through the old switcher trigger's inline refresh button, which the redesign replaced with the switcher row menu (⋯ → Refresh playlist). The helper now goes through that menu; it failed on all three OS shards. - A desktop Xtream delete's completed event cleared its progress while PlaylistsService still cleaned up (parental lock, stored state), so the already-deleted source was actionable again on Sources and back in the switcher until the store dropped it. A terminal event now only ends Cancel; the row stays busy until the delete settles and the removal service clears it. - A running delete (from the switcher, say) disappeared from Sources when the page's saved filters excluded it, hiding its only progress and Cancel. Running deletes stay listed whatever the filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep counting a failure's duration while the list stays open Local Greptile (5/5, one P2) on the PR round: the row's status tag formatted "Not responding · 3 days" against the time the row mounted (or the language last changed), so a new failure never gained its duration and an existing one froze while Sources stayed open. A shared root SourceClock now ticks once a minute and only the status tag reads it, so dates and the meta line do not recompute every minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep the page header in view; mark the active source's row Local Greptile (two P2s) on the PR round, both regressions against master: - The redesigned Sources page scrolled as one, so the title, Clean up and Sort left the view in a long library (master kept a sticky header, and the UI guidelines say headers stay and lists scroll). The header now stays and only the list below it scrolls, which also keeps a row reached by keyboard from scrolling under the header. - Master marked the active playlist on the Sources page; the new row only tinted the current row in the compact (switcher) size. The active source's row is now tinted with the same --app-list-selected-* token on the page too; the switcher keeps its check. The design handoff called the header non-sticky and the current tint switcher-only; both followed master and the guidelines instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): name the Sources sort group; refit the switcher on resize Local review on the PR round: - Greptile: the Sources sort menu's radio group had no accessible name, unlike every other menu group (UI guidelines). It is named by its section, "Sort by". - Codex: the switcher popover capped its height once, when it opened, so shrinking the window while it was open left the footer and the lower rows out of reach. A window resize refits the open popover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * wip: linear URL-cleaning regexes * fix(sources): linear URL-cleaning patterns; keep the stripper off the startup path PR #1932 CI: - CodeQL (polynomial regular expression on uncontrolled data): the embedded-URL and keyed-pair patterns in stripUrlCredentials could retry from every position of a long run of letters. Matches now start only where a name can start (lookbehinds) and every name part is bounded, as is the bare-userinfo pattern; a spec times 50,000-character runs. The preceding "wip: linear URL-cleaning regexes" commit holds that change. - Initial bytes ratchet (+135 B over after master's #1928 lowered the baseline): stripUrlCredentials sat behind the shared logging barrel, which startup code imports, so esbuild placed it in an initial chunk although only the lazy Sources page and switcher use it. It now lives beside its only consumer in playlist/shared/util source-display, and the logging barrel exports the two redaction helpers it needs. Measured locally on the merge with master: 1,537,497 bytes, within 1,538,700. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): case-insensitive Xtream path markers; no removal during a refresh - Strip and redact the account segments after `/LIVE/`, `/Movie/` or an encoded marker such as `/%6Cive/`, as after `/live/`. The display-name check also treats an Xtream path password as a URL secret. - A source cannot be removed while a switcher refresh clears and refills it: the removal service ignores the request, the switcher disables Remove, and the Sources page shows the refresh as busy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): search URLs nested in path segments for name secrets A proxy URL can carry a whole source URL, or a token pair, in one path segment. The display stripper already cleans such segments; the name check now collects their secrets too, so a name holding them collapses to the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): decode nested URLs before classifying; narrow Xtream paths - A URL nested in a parameter or path segment is percent-decoded one round at a time until it reads as a URL or parameter list, so one encoded twice is cleaned too; a value still encoded after the last round is dropped. The name check unwraps nested values the same way. - The two segments after `/live/` and its siblings count as an account only when a stream follows them, so a playlist path such as `/live/news/list.m3u` keeps its file name in the URL and the title. - Adding a playlist from the phone drawer closes the drawer first, so two focus-trapped modals never stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): clean URL-shaped fragments; one registry for busy sources - A fragment is cleaned whatever its shape: `#https://joe:pw@…` loses its userinfo, an unclassifiable secret is dropped, and the name check reads a URL in the fragment as well as a parameter list. - Every refresh holds its source in the root SourceActivityService, which deletes and the startup auto-refresh already use: Sources-row refreshes, the switcher's M3U refresh and the shared Xtream flow. The switcher no longer offers Refresh or Remove for a source the page is refreshing, the removal service ignores a held source, a second refresh from either entry point is skipped, and the page disables Remove while it is held. - The long-input timing spec allows 3 s: the inputs take about 0.25 s, a backtracking pattern takes minutes, and a loaded runner took 1.6 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): keep a browser refresh busy until the backend answers The browser build's PLAYLIST_UPDATE returned at once while its request ran on, so the row stayed refreshable and removable and the shared registry saw the source as idle. The event now settles when the refresh does (the PWA service resolves after its success or error report; the Electron fallback awaits its handler), and both the Sources row and the switcher keep the source busy and held until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): name check hides values under short account keys The name check used the log redactor's key list, which leaves out the short keys `pass`, `pas`, `user` and `usr` that the URL stripper drops. Both now share `isDisplaySensitiveKey`, so a name holding `?pass=…` collapses to the host; `user`/`usr` are account names like `username` and stay shown at the top level, as by design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read keys, markers and secrets through every encoding layer Each check decoded its input once, so one more encoding layer slipped a secret past it: `?%2570assword=…` kept its value, and `/%256Cive/…` kept its account segments, both on screen and in logs. `decodeFully` (shared logging) undoes up to four percent-encoding layers and is now the one form every name check reads: the redactor's key normaliser and Xtream marker check, the display key predicate and path check, and the name check's secret values. Logs and the Sources lists gain the fix together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read Xtream paths decoded; keep short decoded forms out - An Xtream account could hide behind encoded separators (`live%2Fjoe%2Fhunter2%2F1.ts` is one raw segment). `findXtreamAccount` reads the path as decoded parts, maps them back to raw segments, and is the one scanner for the log redactor, the on-screen URL and the name check. Account segments go whole, joined ones included. - The name check required a minimum length only of the raw secret, so a decoded form as short as one letter (`%2542` is `B`) hid ordinary names. Each form must now be long enough on its own. - The decoding helpers move to `url-credential-parts.ts`, which keeps the redactor under the line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read an Xtream path both whole-segment and split Reading the path only as decoded parts misplaced the account when the user name itself held an encoded slash (`/live/joe%2Fx/hunter2/1.ts`): the password segment stayed on screen and in logs. `findXtreamAccount` now scans both readings, each raw segment decoded whole and the decoded path split on every separator, and drops what either finds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): drop JSON configs from shown URLs; read their secrets A nested value such as `?config={"password":"…"}` was neither a URL nor a parameter list, and the quoted key slipped past the free-text check. - A nested value that parses as a JSON object or array is dropped from the shown URL (JSON can spell a key so no pattern reads it, such as `w`); brackets that are not JSON, like `[1080p]`, stay. - The free-text check accepts a quoted key (`"token": …`). - The name check parses JSON values and collects what sits under sensitive keys, and follows URLs in its other strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): hold Xtream sources through re-import; match secrets in any case - An Xtream refresh released its hold when navigation ended, while the re-import ran on from the route session, so Remove came back during the import's writes. The route session now takes the hold synchronously on NavigationEnd and keeps it until its sync, and the import it awaits, settle; the store also holds the source for every content initialization, retries included. - Secret matching was case-sensitive against encodeURIComponent's upper-case escapes, so `%3a` or a lower-case MAC slipped through. Text and secrets are now compared lower-cased, the text also fully decoded. - The Xtream `· username` label is left out when it is, or holds, the password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): read a bracketed key as parameters unless it is JSON The name check returned early for any value starting with `{` or `[`, so `[password]=…` (not JSON) skipped the parameter check and its password stayed in the name. It now returns early only when the value parses as a JSON object or array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): no opening a source from the switcher mid-refresh While an Xtream refresh was still deleting the catalog, ⌘P could reopen the switcher and selecting that source navigated at once, starting its import before the user data to restore was parked. The refresh flow now exposes `isRunning` (its set of runs is a signal), and the switcher marks a refreshing source aria-disabled and ignores its selection by pointer and Enter alike. Imports alone do not block opening a source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): hand over only to an idle source; hide names that are a short secret - Removing the active source could hand over to a neighbour a refresh was still clearing, opening it and starting its import early. `SourceRemovalService.canTakeOver` (not leaving, not deleting, not held in SourceActivityService) now picks the replacement in both lists and gates it in the service. - Secrets under three characters were dropped from the check entirely, so a name that was just a two-letter password showed it. Short secrets now count as the whole text (trimmed, any case); longer ones still match inside a text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): offer Refresh only when it can run; drag handle never opens - PlaylistRefreshActionService runs one refresh at a time, but canRefresh checked only the source, so the switcher, the command palette and the dashboard offered a Refresh that refresh() then ignored. canRefresh now applies the same global guard. - In Custom order a click on a row's drag handle bubbled to the row and opened the source; the handle now stops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): no idle Refresh on held rows; Recent counts the filtered list - The Sources page offered Refresh on a source the startup auto-refresh, an import or a delete held, and refresh() then refused it. The row now offers Refresh only while it can run, keeping it (with its spinner) while that source refreshes. - The switcher showed Recent whenever the library had six sources, so a type filter down to one or two listed them twice and the keyboard visited each twice. Recent now depends on the filtered count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): compare passwords in their own case; only a MAC caselessly Matching every secret lower-cased hid any name holding the password word in another case: the Electron E2E source "Stress Xtream Portal" (password `stress`) showed its host, and the deletion test could not find its row. A password is case-sensitive, so it is compared as written; the text is still read fully decoded, so `%3a` escapes count as `:`. A MAC is the same address in either letter case and is compared lower-cased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sources): match a MAC from the URL in any letter case Only the source's own `macAddress` was compared caselessly, so an M3U URL carrying `?mac=…` (nested ones too) left the MAC visible in a name written in another case. Any MAC-shaped secret, and every form of the source's own MAC, is now compared lower-cased; passwords keep their case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(sources): keep source-health helpers off the startup path Master's #1931 lowered the initial-bytes baseline, and this branch then sat 535 bytes over its slack. Most of its growth was runtime helpers in `@iptvnator/shared/interfaces` (reachability, health history, reason and fingerprint helpers) that only the lazily loaded source services call: the bundler put them in a chunk the app preloads. They now live in `@iptvnator/portal/shared/data-access` beside those services (types stay in interfaces); initialBytes 1,350,438 → 1,349,048, 855 under the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c8afa382fe |
fix(server-storage): adopted edits keep yielding through conflicts; newest view per item; same-token sign-ins
A conflict replay collapsed every step into one edit, so a closed tab's adopted step lost `yields` after the first conflict and a second one replayed its old position over a newer one. Its steps now stay as they are, and only this page's own collapse into one edit on top of them. The move combines recently viewed items one per item, keeping the newest view and its artwork, then newest first; before, the server's older copy of an item won over a later browser view. A failed sign-in clears the stored token only on a 401: a network failure says nothing about the token, and another tab may have stored the same one meanwhile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6f1fe7929c |
fix(server-storage): recheck the instance after offline, back-forward cache pages, Stalker server rows
Going offline now marks the instance unchecked: the token and writes wait until /instance answers with this page's instance (another one reloads the page). A replaced volume at the same address no longer gets the old token and queued user-state writes on reconnect. The session checks as soon as it goes offline, so a blip costs no retry interval. A page restored from the back-forward cache whose record another tab adopted meanwhile (its timers were frozen, so the record went stale) reloads instead of sending those writes again after newer edits. The Stalker playlist metadata migration also runs on server rows, once per server: a desktop database copied to the server can predate it, and its Stalker rows lacked isFullStalkerPortal. The take-over and change-diff loops move out of the user-data facade, and the adoption specs get their own file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b56b833b96 |
fix(server-storage): a failed sign-in keeps another tab's token; moved history newest first
A sign-in that fails clears the token only while it is still the one this attempt stored: another tab that signed in meanwhile keeps its valid token. The move orders the combined Xtream recent items by viewedAt, newest first, as the app keeps them: browser views appended after the server's history were cut by the next view once the server held 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e6ef8cdd54 |
perf(web): take Angular Material off the startup path (#1931)
Takes Angular Material off the renderer's startup path: renderer.initialBytes 1,596,201 -> 1,344,067 bytes (-252,134, 15.8%) at the branch point; merged with master, where #1928 lands the form-field defaults per component, the branch measures 1,345,807 (-188,797, 12.3% against master's baseline). The bundler keeps each module in one chunk, so one eager import of any export carries the module's whole code; each item removes the last eager importer of one Material module. - EPG progress and update panels: eager host + `@defer (when visible)` view with an `@error` notice; the startup spinner is static markup copied from mat-spinner. - LazyMatDialog: the confirm dialog service, player service, parental lock prompt and shell actions resolve MatDialog on first use. - The theme emits tokens for the 21 imported components only, through a `$material-components` list and meta.apply; tools/nx/check-material-theme-scope.mjs (CI) keeps it in step with the imports in both directions. - No provideAnimations(): Material 22 animates with CSS and reads prefers-reduced-motion itself; apps/electron-backend-e2e/src/material-motion.e2e.ts proves dialogs, selects, menus, snack-bars and tooltips animate in and out and stop under reduced motion. Its renderer had deferred every DOM removal to the end of the tick, so the catalog list now tracks the grid offset from scroll events and the player settings panel decides "focus was inside" in a microtask, both with specs that detach the view first. - The snack-bar stays, and with it button, the icon-button chunk and the CDK overlay (about 190 KB): root services inject it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f4663746d5 |
fix(server-storage): Xtream reads and backups wait for the first read; keepalive deadline
Startup waits for the first user-data read only a few seconds. ready() now waits for a first read in flight without starting one (an offline page is not held up) and tells whether one succeeded: - PwaXtreamDataSource awaits it before reading favorites, recent items and playback positions, so a new device whose /user-state read is slow does not show empty lists or start playback without its resume point. - A PWA backup export and restore fail while no read succeeded; the restore stays parked, since its replacement lists would otherwise merge into the server's (an empty favorites list changed nothing). A page-hide keepalive write gets the request timeout: a server that takes it and never answers no longer holds every later write, re-read and delete behind it until reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ed956a454c |
fix(server-storage): token only to backend URLs in raw fetches; Stalker series keys in the move
PwaService joins paths to the backend URL without its trailing slashes, so BACKEND_URL=/ no longer yields a protocol-relative //… URL that names another host, and it attaches the server token only when the request URL resolves to the backend (isBackendUrl), like the interceptor. The move's item key adds the Stalker series flag to id and category: a provider category can hold a movie and a series of one id, and the series favorite was dropped as a duplicate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ec4b9286a7 |
perf(web): provide form-field defaults per component, not at the root (#1928)
* perf(web): provide form-field defaults per component, not at the root app.config.ts provided MAT_FORM_FIELD_DEFAULT_OPTIONS in the root injector. Importing the token kept @angular/material/form-field in main.js, and Material 22.2's form field imports @angular/forms/signals, which would add @angular/forms to startup as well (the reason the Angular 22.2 dependency update exceeds the initial-bytes ratchet). Every form field renders in a lazy chunk, so the outline + dynamic defaults now come from provideFormFieldDefaults() (@iptvnator/ui/components/form-field-defaults) on each component that renders <mat-form-field>. tools/nx/check-form-field-defaults.mjs (ui:form-field-defaults:validate, run in CI) reports a component that renders a form field without the provider, and any use of the token outside the shared provider. The Xtream import form test asserts outlined, dynamically sized fields with no root provider; without the component provider they render as fill. renderer.initialBytes: 1,596,201 -> 1,534,604 bytes (-61,597) on a local production build; the baseline is lowered to the measured value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): cite #1928 as the initial-bytes baseline evidence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ui): check form-field providers per component on the TypeScript AST The guard matched templates and providers with regular expressions over the whole file: a quoted inline template went unread, one component's provider covered another in the same file, an earlier array-valued provider hid the call, and provideFormFieldDefaults() in app.config or a route passed although it loads Material's form field on startup again. It now reads each @Component decorator's own template and providers array, and reports any provideFormFieldDefaults() call outside one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c51ef83b56 |
ci(deps): hold Electron at 43 while armv7l Linux packages ship (#1925)
Electron 44 publishes no linux-armv7l binary, so the 44.x update fails the Linux packaging jobs on the armv7l AppImage, DEB and Snap targets. Ignore Electron majors in Dependabot until 32-bit ARM support is dropped deliberately; 43.x minor and patch updates still arrive. Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fdaa2b5aa0 |
fix(server-storage): per-instance cache, idle tabs adopt closed ones, backups wait for the first read
The user-data cache is keyed by server instance (`iptvnator-server:cache:<instanceId>:<namespace>`): a tab still open on the old backend after BACKEND_URL changed no longer writes over the new server's cache, which the new tab's next edit would have sent there. claimCache also removes the old owner's keys and the unscoped keys of earlier builds. touch(), which runs every 30 s, adopts closed tabs' saved writes once this tab has read: a tab with nothing to sync never re-reads, so they waited until a tab switch or reload. UserDataStorage gains an optional ready(); the server storage resolves it after its first read and rejects while that fails. A PWA backup export awaits it, so a fresh browser whose /user-state read is slow or failed cannot export empty favorites, history and positions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f9e5ee97d7 |
fix(server-storage): bind the token to its instance; adopted edits yield to newer changes
The access token is stored with the instance id it was given for, and a page reads it only for its own instance: a tab still open on the old backend after BACKEND_URL changed no longer sends the new server's token there. The API client hands every 401 to reportAccessDenied(), which now also sends a page without a token for its instance to sign-in, while one that holds its token back until the instance is checked keeps it. A closed tab's adopted edits carry `yields`: replayed, they change only what is still as that tab saw it, so they never replace progress this tab or another device saved since. The flag survives the journal. The sync spec's fake server and memory storage move to a test-helpers file to keep the spec under its line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a94673b4ab |
fix(server-storage): check a remembered instance before sign-in sends the token
A page started from memory (startedOffline) withholds the token until the instance is verified, so sign-in on that page sent no token and the server's 401 cleared the right one. Sign-in now first calls ServerStorageSession.ensureVerified(): it reads /instance without a token, marks the status verified when the instance id matches, and reloads the page when another server answers at the address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b8838c1c09 |
fix(server-storage): no token for an unverified instance, sign-in remembers its server
- ServerStorageStatusService.accessToken() returns the stored token only while server mode runs on an instance this start verified. The API client, the HttpClient interceptor and PwaService's raw fetch calls use it, so after an offline start from a remembered instance no request (including the /instance reachability check) carries the token to whatever answers at that address, and a 401 then does not clear it. - Sign-in remembers the server along with the token; when storage refuses either, it reports SIGN_IN_STORAGE instead of reloading into a start that would drop the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4908803ceb |
fix(server-storage): forget the token for another server, listen before sign-in, adopt only stale records
- A server that answers as another backend URL or instance id than the remembered one makes the browser forget the stored token, so server A's token never reaches server B. - The session installs its page-hide and heartbeat listeners before it returns for sign-in, so a page holding another page load's writes still marks itself closed on unload. - Closed tabs are found by stale records only again: saved writes without a record may belong to a running tab whose record storage refused, and are left to that tab's own reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cf715a7573 |
fix(server-storage): one registry key per page load, server token only in server mode
- The unconfirmed-writes registry stores each page load's record under its own key (iptvnator-server:unconfirmed:<instance>:<page>), listed through a new keys() on the user-data storage adapter, so tabs never overwrite each other's records. Saved writes without a fresh record, also when the record was lost, are adopted as a closed tab's (#1920). - PwaService sends the stored server token on its raw fetch calls only while server mode runs, and a backend that answers without server storage makes the browser forget the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
294d630156 |
fix(stalker): keep TMDB genre and country chips on Stalker series (#1930)
The series view re-normalizes the selected item through createStalkerInfo() on every render, and that mapping carried only seven hand-picked tmdb_* fields. tmdb_genres, tmdb_countries, tmdb_media_type and tmdb_supplied_release_date were dropped right after enrichment, so the clickable Discover chips never appeared on Stalker series. The carried keys now come from a Record over StalkerVodInfo's tmdb_* keys, so a new enrichment field fails to compile until it is listed. Saved Stalker entries now also keep tmdb_supplied_release_date, and the recommendations exclusion index (trustedReleaseYear) skips a date TMDB supplied, as the flag's contract requires. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be2962f97d |
fix(server-storage): reorders on the stored favorites, URL-insensitive remembered server, prototype-free id maps
- PlaylistEffects.setFavorites$ applies the drag's order through transformPlaylistFavorites (reorderFavorites): favorites added on another device stay, ones removed there stay out. - The remembered server matches the backend URL by origin and path without trailing slashes, so /api and /api/ start offline in server mode alike. - Maps keyed by playlist id are built without a prototype (idMap), so an id such as __proto__ is an ordinary key in the cache, conflict replays, re-reads and tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
132d6ccc63 |
fix(server-storage): a re-read sent before a playlist delete skips that playlist
ServerUserDataStorage records each successful delete; a re-read sent before it drops the deleted playlist's entries and tags from the server's answer (withoutDeleted), so a late answer cannot write the deleted favorites and positions back into the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
de87c1e4d3 |
Merge branch 'master' of https://github.com/4gray/iptvnator into claude/server-storage-pwa
# Conflicts: # docs/architecture/pwa-self-hosted.md |
||
|
|
8bf92f9d65 |
fix(backup): export and restore Xtream user state in the PWA (#1905)
* fix(backup): export and restore Xtream user state in the PWA Without an Electron bridge the backup wrote empty Xtream favorites, history and positions, and import only parked the state until the playlist was next opened. PwaXtreamBackupStateService reads and writes the four PWA maps through USER_DATA_STORAGE, so self-hosted server storage is covered as well. Export converts typed keys, resolves legacy ids only through their snapshot, and fails on an unreadable map instead of writing []. Import applies at once through applyAndConsume, remapped to the imported playlist id; a failed write keeps the snapshot parked for the store. Title snapshots travel in a new optional collectionItems field that older builds ignore. The storage key names are shared with the portal data source as XTREAM_USER_DATA_KEYS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(backup): write PWA title snapshots before the Xtream lists A browser restore that failed after its first list write kept the pending state parked for the Xtream store, but that replay carries no title snapshots, so they were lost. Merge them first; a damaged cache is replaced, while a storage that cannot be read still fails the entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(backup): keep the artwork of PWA recently viewed items The browser build saves a backdrop with each view and shows it ahead of the title snapshot's, but the export dropped it, so a restore showed older artwork. Carry it as an optional backdropUrl on the archive's recently viewed items; the desktop build ignores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(xtream): keep view artwork in the PWA pending restore state A failed browser restore is replayed by PwaXtreamDataSource.restoreUserData, which rewrote recently viewed items without their backdropUrl; a refresh dropped it the same way in clearPlaylistContent. Both now carry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(backup): restore PWA Xtream lists before the snapshot cache Writing the optional title snapshots first could take the storage space the favorites, history or positions needed, failing a restore that fits without them. The lists now go first, as the snapshot-storage contract asks; after a failed list write the snapshots are still merged for the store's replay, unless storage is full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(backup): pick the Xtream backup path by the SQLite data source The backup chose its Xtream path by the mere presence of window.electron, while XTREAM_DATA_SOURCE falls back to the browser storage whenever the bridge lacks the Xtream SQLite API. On such a partial or stale preload the export read the wrong store and the restore took the SQLite path. Both now follow RuntimeCapabilitiesService.supportsXtreamSqliteDataSource. Also casts the test helper's prototype instance through unknown, which the spec typecheck requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(xtream): restore PWA playback positions under the target playlist A PWA restore that is replayed by the Xtream store kept each position's exported playlistId. When the backup was imported under another id, the VOD details page rejected those positions as belonging to a different playlist. restoreUserData now stamps the playlist it restores into. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ebb1ab3467 |
fix(portals): hide provider placeholder metadata (N/A, -, 0) in details and cards (#1923)
* fix(portals): drop provider placeholder metadata at the mapping layer Stalker and Xtream portals send "N/A", "-" or an unrated "0" score for fields they have nothing for, and the detail templates rendered them as data. Normalize them where provider rows are mapped: createStalkerInfo, the shared VOD view-model adapters and the Xtream detail selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(portals): hide placeholder ratings on catalog cards formatGridRating passed a provider "N/A" through as the badge text and rendered an unrated zero as "0.0". Read the value through the shared readProviderRating so both hide the badge and resolveGridRating reaches the generic rating behind a placeholder rating_imdb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(xtream): keep placeholder ratings off the sparse VOD fallback page A get_vod_info made only of placeholders is now stripped to sparse and routed to the fallback page, which read the top-level catalog ratings raw: "N/A" survived and a zero became "0.0". Read every candidate through readProviderRating so the next usable rating wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70e771ff27 |
perf(search): one 300 ms debounce between typing and the global search query (#1899)
* perf(search): one 300 ms debounce between typing and the global search query Typing in the header search box waited twice before a query ran: 350 ms in the workspace shell (SEARCH_INPUT_DEBOUNCE_MS), then another 300 ms in SearchResultsComponent. On J4 (large catalog, "system") the query and first page take 30-40 ms; about 650 ms of the ~690 ms from the last key to settled results was waiting. The results component now searches as soon as the applied term reaches the store, and the shell debounce is the only one, lowered to 300 ms: above the gap between keys of steady typing, so a word typed in one go still queries once. Superseded queries are dropped by the existing request versions. The component's route sync reads the store term untracked, so a term the shell applied before writing q is not pushed back to the stale q and re-queried. J4 locally (3 runs per build): lastKeystrokeToSettledMs P50 692-695 ms -> 336-337 ms, ipcCallsPerSearch 1 and sqlStatementsPerSearch 2 unchanged. At 400 ms per key both builds run five queries; only typing with 300-350 ms gaps queries per key where it did not before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(journeys): enforce J4 ipcCallsPerSearch 1 and sqlStatementsPerSearch 2 Both counters were identical in all 816 J4 iterations of the 136 CI journey jobs since #1816 and in all 18 iterations of three runner runs of this PR (run 38165392046, attempts 1-3). They guard the single debounce: a search that queries per keystroke or twice for one term raises them. Guard-only note, since the wall-clock gain came without moving them. Added to the Performance journeys --only list; the direction check reports 2 added, nothing weakened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(search): run a prefilled in-portal search again once the catalog has loaded Review follow-up on #1899 (Greptile P1). Without the results component's 300 ms timer, an in-portal search applied while the portal's catalog is still loading (a reload or deep link with q) runs against the incomplete catalog, and nothing re-ran it afterwards. The old timer only helped when loading finished within 300 ms. The component now runs the current in-portal search once more when nothing of the catalog is loading or importing any more, the readiness the Discover route already uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(search): one search effect for a new term and the loaded catalog Review follow-up on #1899 (Greptile). With separate effects, a term and the end of catalog loading arriving in the same flush each ran executeSearch for the same term: the request version dropped the first answer, but the catalog scan or desktop search still ran twice. One effect now tracks both triggers and searches once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
586ba74ea7 |
fix(server-storage): adopted writes without a known version conflict instead of overwriting
A page that never read the server has no version tags, so an adopted write went out unconditionally and replaced newer server data with its cached value. Adopted edits now use the never-matching version when this page knows none (and for a queued write without a tag), so the server answers 412 and the edits are replayed on its value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2320b64e01 |
fix(server-storage): adopt a late previous page load instead of restoring it
When touch() finds that the previous page load no longer looks live, this page may already have read and queued newer edits. Its saved writes and early edits are now adopted like a closed tab's (their steps ahead of this page's newer ones, queued at once) instead of restored as if this page had just started, which replaced newer progress with older and parked early edits that were never sent after the first read. Cache forgetting moves into UserDataCache.forget(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d45210c4f7 |
fix(server-storage): take over a reload's writes once the previous page load stops looking live
When the close flag could not be saved (storage full), a reload took the previous page load for a running duplicated tab and left its saved writes unsent. The page now remembers that page load and its 30 s touch takes it over as soon as its record is closed, stale or gone, so the session's retry sends the writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37c14d7d89 |
fix(server-storage): keep duplicated tabs off live journals, drain writes around remove-all, normalize legacy Stalker rows on the move
- A page marks its unconfirmed-writes record closed on pagehide (not into the back-forward cache); a new page takes over the previous page load's saved writes only when that record is closed, stale or missing, so a duplicated tab, which copies sessionStorage, leaves the running tab its writes. - PlaylistsService.removeAll lets queued playlist writes land first and makes writes issued meanwhile wait until it finished; the server store refuses a document without an id. - Moving browser data sets the explicit Stalker portal-mode flag on rows from before it (withExplicitStalkerPortalFlag, now shared with PlaylistsService). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |