mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
0b91d4d1a0d5de3b4cdcb94656b90d1187693162
3102
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0b91d4d1a0 | wip: linear URL-cleaning regexes | ||
|
|
ee35951a5e |
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> |
||
|
|
e3a4ea7259 |
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> |
||
|
|
13de38ebe5 |
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> |
||
|
|
dac79af54f |
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> |
||
|
|
a48d0b913f |
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> |
||
|
|
a4e4bcd880 |
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> |
||
|
|
89d64472c0 |
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> |
||
|
|
4e1eb15b90 |
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> |
||
|
|
9a0fc9b924 |
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> |
||
|
|
80a9cd946a |
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>
|
||
|
|
85e6cb3816 |
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> |
||
|
|
123bc170e9 |
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> |
||
|
|
366745f16d |
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> |
||
|
|
845d295474 | Merge origin/master into claude/design-handoff-prompt-d0aee4 | ||
|
|
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> |
||
|
|
7f462208b1 |
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> |
||
|
|
8f36c55e72 |
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> |
||
|
|
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> |
||
|
|
f60d797011 |
fix(workspace): don't flash the import overlay on a cached Xtream open (#1906)
* fix(workspace): don't flash the import overlay on a cached Xtream open Opening an already-imported Xtream portal reads the saved catalog in tens of milliseconds, but the session made the shell mount the full-screen import overlay right away, so it flashed for about two frames between the skeleton and the loaded page (J2 open-source). A session that has only read the saved catalog (loading-cached, store isImporting still false) now shows the overlay after a 300 ms delay. Remote import phases and refresh preparation still show it immediately, and a slow or stuck cache read still blocks and stays cancellable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): keep the overlay delay while another playlist refreshes Refresh preparation skipped the cached-load delay for any playlist, so opening a cached portal while another playlist prepared a refresh still flashed the overlay. Only preparation of the shown playlist (any on the dashboard) now shows it at once. Also words the J2 layout-shift history from #1895 in the past tense: the cached-load overlay no longer mounts on a J2 click. 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> |
||
|
|
0885d02e41 |
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> |
||
|
|
434cbf3681 |
fix(playlist): keep cleared optional fields cleared on desktop (#1904)
Clearing an optional playlist field (User-Agent, Referrer, Origin, credentials, EPG URL lists, file path) did not persist in Electron: the codec maps an empty value to `undefined`, `ON CONFLICT DO UPDATE` skips undefined keys, and `parseAppPlaylist` prefers the stale column over the updated payload, so the old value came back on the next read. `playlistConflictUpdate` now sets every optional column the document leaves empty to NULL, so both desktop upserts (`DB_UPSERT_APP_PLAYLIST` and `DB_UPSERT_APP_PLAYLISTS`) replace the stored row. The cleared-columns list moves from the web backend's playlist repository into the shared conflict module, and the server now uses the shared version. `date_created`, `name`, `type`, `autoRefresh`, `payload` and `last_usage` are never cleared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b546fe0123 |
fix(m3u): keep groups hidden in Manage groups after a restart (#1894) (#1907)
* fix(m3u): keep groups hidden in Manage groups after a restart (#1894) Electron fills the playlist store from DB_GET_APP_PLAYLIST_METAS, which reads the metadata columns only. hiddenGroupTitles lives only in the JSON payload, so after a restart the store had none and the groups rail showed every group, although SQLite still held the saved selection; saving the dialog from that state then overwrote it. M3uWorkspaceRouteSession already reads the full playlist to publish its channels and favorites. It now copies hiddenGroupTitles from that read into the store (updatePlaylistMeta, persist: false) before the channels, unless the store value changed while the read was queued behind a save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(m3u): wait for the playlist entity before hydrating hidden groups The startup inventory publishes playlists only after settings and EPG reconciliation, while a restored or deep-linked playlist route waits for the metadata read alone. The route session's full read could therefore finish first, and updateOne dropped the hydration for the missing entity for the whole session. The hydration now waits for the entity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(m3u): cover hidden groups when the Groups route opens before the inventory An Electron E2E reloads the renderer on the Groups route while the startup inventory is held (the EPG reconcile fails once and its retry is parked), and records from document start whether a hidden group is ever rendered visibly. It fails on the previous hydration that did not wait for the playlist entity. A service test pins that getPlaylist reads hidden groups from a meta update that is still being written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(m3u): type empty hidden-group arrays in the route session spec The CI spec typecheck (apps/web/tsconfig.spec.json) rejected `[]` inferred as never[] inside the PlaylistMeta and Playlist casts with TS2352. 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> |
||
|
|
86066c47ed |
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> |
||
|
|
9feab4f83b |
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> |
||
|
|
106414c118 |
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> |
||
|
|
401295c092 |
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> |
||
|
|
028a3db880 |
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> |
||
|
|
8508010ea5 |
fix(playback): resume an episode whose connection drops instead of skipping it (#1902) (#1908)
* fix(playback): resume an episode whose connection drops instead of skipping it (#1902) Embedded MPV sessions run with keep-open, so a provider that closes the connection mid-episode (and refuses ffmpeg's own reconnect) reaches the app as eof-reached: status `ended` at the drop position. The reconnect policy counted `ended` as a loss only for live playback, so on VOD it stood for the end of the file and series autoplay started the next episode. - The reconnect policy now treats a VOD `ended` with more than max(10 s, 1 % of the duration) left as a dropped connection and reloads it at the last position. A reload that ends again before getting 5 s past where it resumed proves the file ends there (bitrate-estimated duration), so it is left as the end and autoplay continues after one attempt. - The service passes the snapshot duration to the policy. - The player emits playbackEnded only for an `ended` session without a pending reconnect. - Xtream mock: `streamdrop` scenario whose episodes drop a third of the way into a local 60 s clip and refuse the retry for 4 s; Electron E2E covers the native-view engine (skips without an Embedded MPV runtime). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep the early-EOF retry on a refused reload or after a seek The resume point that tells a reload ending where it resumed from a new drop was recorded before the reload ran, so a reload the addon refused made the original EOF look like the end and the next poll cancelled the retry. Record it only once the reload started, and forget it when the user seeks: an EOF after a seek is judged on its position alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): fail the stream-drop resume check when the reload restarts the episode A reload that started the episode over would also reach the 25 s mark within the timeout, since the mock drops each episode only once. Every position played after the drop must now stay within 3 s of it. 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> |
||
|
|
6fe684af51 |
fix(workspace): slide the content and header into place when a source opens (#1895)
* fix(workspace): slide the content and header into place when a source opens
Opening an Xtream portal from the dashboard inserted the 323 px context
panel and the header Back in the frame that swaps the page: the content
pane jumped right by the panel's width and the switcher, search and actions
by Back's slot. That one frame was J2's whole layout shift (0.2327: 0.222
for the content pane, 0.011 for the header, scored with the pane's
distance), plus 0.0001 when the category search and sort buttons appeared
with the categories.
LayoutSlideDirective animates these moves as a 180 ms FLIP: the new layout
applies at once and only a transform animates, so nothing waits and no
layout shift is recorded. The content surface covers the strip it uncovers
while sliding, Back fades in, and reduced motion keeps the jump. The
category search and sort buttons render disabled while the list loads.
Locally J2 renderer.layoutShiftScore reads 0 (was 0.233), J1 0 / 0, J3
0.001, J4 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(journeys): lower J2 layoutShiftScore to 0
The slides land on the runner: run 38147072417 (
|
||
|
|
a9f4ae38ea |
perf(database): rebuild the title index once per profile, not again on the second launch (#1900)
upgradeContentTitleFtsTokenizer recreates and rebuilds content_title_fts and records migration:content-title-fts-remove-diacritics:v1, but not migration:content-title-fts-trigram:v1. On a new profile the tokenizer upgrade therefore did the first rebuild, and on the second launch ensureContentTitleFts found its own marker missing and rebuilt the whole index again, synchronously inside initDatabase on the main thread, right after the first catalog import. A trigram rebuild costs about 4.5 s per million titles (sqlite3 3.51, synthetic titles). Every J4 iteration paid it too, since each launches a copy of a freshly seeded profile. The upgrade's transaction now records both markers. The skipped-release upgrade harness counts rebuilds across every initialization of a profile and requires exactly one; it fails for all six cases without the fix. Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5f55553f41 |
test(e2e): wait for the persisted Xtream import status, not only the overlay (#1897)
* test(e2e): wait for the persisted Xtream import status, not only the overlay waitForXtreamImportToFinish gave the loading overlay 5 s to appear and then treated "no overlay" as finished. The overlay appears only once the portal's account check answers; a freshly started mock generates the large catalog on that first request, so J4's seed sometimes closed the app while the series import was still running. The template then had no series rows, and J4's query for "system" (series titles only) returned an empty list on the first launch after seeding. On an Xtream route the wait now also requires the import status the app persists to read completed for live, movie and series. The new xtream-import-wait.e2e.ts holds get_account_info past the overlay window and fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): judge the Xtream catalog complete the way the app does Review follow-ups on #1897: - A refresh deletes the catalog and re-imports it without resetting the persisted import statuses, so `completed` alone proves nothing after one. A type now counts as imported when its status reads completed and it has rows, the same rule the app uses before reusing its cache; a type without categories (a live-only panel) counts by status, and a portal without any category is not imported yet. A second spec refreshes a portal behind the slowed account check. Today the refresh keeps its preparation overlay up across that check, so it passes with the earlier wait as well; it guards the helper's contract. - The proxy is closed in its own finally, so a failed launch or close no longer leaves it listening. 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> |
||
|
|
b88a1c81e5 |
feat(self-hosted): server storage API in web-backend (#1898)
* feat(interfaces): add the server storage HTTP contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): read server storage configuration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open the server database and track revisions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): playlist and user-state repositories Playlist documents use the shared desktop row codec, so the server writes the same rows. keepPayload upserts take the stored playlist/items/header fields when the client leaves them out. Xtream user data lives in the server-only server_user_state table, one JSON value per namespace and playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): storage error mapping and access token guard Storage errors map to JSON status bodies and are logged by name and code only. An optional bearer token guards every route except /, /health, /config.js and /v1/instance, provider proxy routes included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): serve the /v1 server storage API /v1/instance is always public and reports whether server storage is on. With storage, /v1/playlists serves whole documents with ETags and 304s, accepts keepPayload upserts, and deletes playlists with their user state; /v1/user-state and /v1/app-state store the rest. The proxy routes are unchanged unless an access token is configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open server storage on start and close on shutdown With IPTVNATOR_STORAGE=server, main.ts opens the database before it listens and exits with a clear message when the data directory is not writable. SIGTERM and SIGINT close the server and the database. The native better-sqlite3 module stays outside the esbuild bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): match weakened ETags on playlist reads nginx weakens the ETag of a gzipped response, and clients send the weak form back in If-None-Match. The route now compares tags weakly, as RFC 9110 requires for GET, so it answers 304 before reading the document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(docker): persist server storage on a volume The image builds better-sqlite3 in a native stage per target platform, enables server storage on the /data volume, and raises the nginx body limit to 64 MB for /api with gzip for JSON. The compose file mounts a named volume and documents the optional access token. The PWA does not use the storage yet; docs say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web-backend): type the storage route responses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): harden server storage reads, deletes and app state - Metadata reads return the stored document without its payload fields, so fields that live only in the JSON survive a metadata round-trip. - A conditional GET checks that the playlist exists before answering 304. - Deleting a playlist and its user state commits in one transaction; the ETag moves only after the commit. - Browser app state is stored under a client: prefix and the instance id is repaired when empty, so a client write cannot stop the backend. - User-state buckets have no prototype, so ids like __proto__ round-trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(self-hosted): keep the access token unset until the PWA sends it Add the release note for the Docker server storage volume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): bound shutdown and reject padded playlist ids - SIGTERM closes idle connections at once and cuts the rest after five seconds, so a long provider download cannot keep the backend running. - Playlist ids with surrounding whitespace are rejected: the row codec trims ids, so storage and ETags would track different keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): clear optional columns on whole-document upserts The row codec maps empty values to undefined, which ON CONFLICT DO UPDATE skips, so a cleared User-Agent, Referrer or credential kept its old column value and came back on the next read. Server upserts now write NULL to every replaceable column the document no longer sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): linear token parsing, no-store app state, longer start - The access guard reads the Bearer token by string scanning instead of a regular expression that CodeQL flagged as polynomial on long whitespace. - GET /v1/app-state/:key sends Cache-Control: no-store like the other state routes, so devices never reuse a stale value. - The entrypoint waits up to IPTVNATOR_BACKEND_START_TIMEOUT (300 s) for /health, because the backend migrates its database before it listens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): metadata reads skip payloads, redact JSON parse warnings List playlist metadata from a server-only payload-free copy of each document instead of running json_remove over every payload blob, and log only the error name when a stored JSON value fails to parse, since the parser's message quotes the stored text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): copy carried-over panel clock into playlist metadata When an upsert keeps the stored serverTimezone, build the metadata copy from the saved payload so metadata and full reads agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4e9795ac54 |
fix(playback): wait for mpv's reply to every reused-instance IPC command (#1876)
Every reused-instance MPV IPC command now carries a request_id and waits, within the existing 2 s bound, for mpv's matching reply; an error or missing reply falls back to a fresh launch like a socket failure, and the racing post-loadfile seek is gone. MPV launches run one at a time through a launch queue so overlapping launches never act on the same child; Stop on a queued launch delegates to the in-flight launch's own closer, a failed exit stays retryable, and the dock keeps the newest launch. Position polling and output-error reporting moved out of mpv-session.service.ts, which leaves the max-lines baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94570bc387 |
refactor(storage): seams for server-backed playlist storage (#1896)
* docs(storage): server storage design and implementation plans Design for keeping playlists and user data on the self-hosted server (#305), revised against master, plus three implementation plans: storage seams, the web-backend storage API, and the PWA server mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(database): share the playlist row codec Move buildPlaylistRow, parseAppPlaylist and the upsert conflict helpers from the Electron database operations into libs/shared/database (@iptvnator/shared/database/app-playlist) so the self-hosted web backend can write the same rows. The Electron modules re-export them; behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(database): read the data root from IPTVNATOR_DATA_DIR Deployments that keep the database on a mounted volume, such as the self-hosted web backend in Docker, can now set the data root. The E2E override still wins; without either variable the root stays ~/.iptvnator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(services): add the playlist storage backend resolver PlaylistStorageBackend becomes the single place that decides whether playlists persist through the Electron bridge or IndexedDB, and defines the PlaylistDocumentStore contract a server store will implement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(services): resolve playlist storage through one backend PlaylistsService asks PlaylistStorageBackend for its document store instead of reading window.electron and supportsSqlite directly. Paths that only make sense on the desktop (legacy IndexedDB import, worker deletion with progress, the favorites-channel query) check for the Electron kind explicitly. Behavior is unchanged: the backend still resolves to the Electron bridge or IndexedDB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(xtream): store PWA user data through USER_DATA_STORAGE PwaXtreamDataSource and PwaCollectionStorage read and write favorites, recently viewed items, collection snapshots and playback positions through the new USER_DATA_STORAGE token (default: localStorage) instead of calling localStorage directly, so a deployment can substitute a server-synced storage. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(pwa): document the storage seams Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(database): bracket access in the shared playlist codec libs/shared/database compiles with noPropertyAccessFromIndexSignature, which the Electron backend did not enforce on the moved code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(electron): alias the shared playlist codec in the worker bundle The database worker is bundled by esbuild with an explicit alias table; without the new entry, @iptvnator/shared/database/app-playlist resolved under the library's index file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): read localStorage lazily in USER_DATA_STORAGE When the browser denies storage access, reading localStorage itself throws. The default factory returned the global, so injecting the token failed and PwaXtreamDataSource could not be constructed. The adapter now reads the global inside each call, where the data source already guards its reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(database): describe /data as an example data root The Docker image does not set IPTVNATOR_DATA_DIR yet; that arrives with the server storage backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
633fde3e0e |
test(e2e): wait for scrollend between scroll keys (#1893)
Chromium drops a keyboard scroll pressed in the frame or two after the previous smooth scroll lands but before Blink handles the compositor's finish: ScrollAnimator retargets the finished curve (kRunningOnCompositorButNeedsUpdate), then the late CompositorAnimationFinished treats that state as finished and resets it. The Live TV responsiveness spec pressed the next key as soon as an expect.poll saw scrollTop change, so a poll that sampled the final frame sent PageDown or Home into that window (2 of 5 local runs on master, all three CI attempts on PR #1886). pressScrollKey presses a scroll key and waits for that scroll's scrollend. The Live TV spec uses it for PageDown, PageDown and Home with the same position assertions; the VOD detail spec swaps its stable-frame fence for it. The validation map documents the race. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dd815676b4 |
perf(tmdb): send catalog title matches in small requests (#1891)
CatalogTitleMatchService.matchTitles sent a caller's whole batch as one DB_MATCH_TITLES request, and the DB worker answers one request at a time: a recommendations batch held every other read for 95 s on a 3.9M-title library. The service now deduplicates the titles, keeps the 200-title cap, and sends requests of five titles one after another, concatenating the answers in order. The answer stays all-or-nothing: a failed request returns no matches, a lock that starts withholding everything stops the batch, and a batch starts over when ParentalLockService.version() changes between requests (no matches after three changing attempts). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
43358e10f2 |
fix(ui): welcome screens and drop overlay follow the app theme (#1886)
* fix(ui): welcome screens and drop overlay follow the app theme The welcome dashboard, the empty Sources page and the playlist drop overlay switched on `prefers-color-scheme`, so with the app set to dark on a light OS (or light on a dark OS) they painted the other theme's colours. They now read `--app-*` tokens, which follow the `.dark-theme` class set from Settings, and the hard-coded blues are derived from the selection colour (a local "strong" accent mixed toward the heading ink keeps chips and filled labels at 4.5:1 in both themes). White rgba() fills that vanished in the light theme (season empty panel, catalog refinement chips and menu divider, Xtream archive banner and disabled paginator icons, shell download-activity track, search field) now use the widget surface, on-surface mixes or the search tokens. Reads of custom properties that nothing declares are fixed: the release notes error, the search-layout empty state and the collection reload dim now use declared tokens; unset hooks are replaced by their fallback value. New guard `pnpm run styles:theme-references:validate` (CI) rejects undeclared var() reads and prefers-color-scheme outside the settings resolver. Electron E2E os-color-scheme.e2e.ts flips the OS scheme under each explicit app theme and checks colours, contrast and screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): retry the rejected-drop comparison when the card dismisses mid-read The rejection hides itself after 1.8s, and the OS-scheme comparison reads the overlay twice with an emulateMedia call between. A slow runner could lose the card between the reads; the comparison now drops again until both reads see it. A colour that follows the OS still fails every attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): measure welcome text contrast from rendered pixels The feature and source cards paint gradients, which a backgroundColor walk ignores, so card titles, descriptions and chips could pass while falling short on the actual card. Every text on the three surfaces is now measured against the pixels under it, as the filled button labels were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): scan only runtime sources in the theme-references guard The guard read every tracked file under apps/ and libs/ except specs, so an E2E, mock-server or test-helper declaration could satisfy a runtime var() read that nothing in the app declares (dashboard-rail-focus.e2e.ts sets --cover-rail-width), and a test-only read could fail the check. It now skips spec/test/e2e files, test helpers and stubs, testing projects, the E2E and mock-server apps and the marketing website. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(xtream): set live paginator tokens through mat.paginator-overrides The live layout hand-declared --mat-paginator-* tokens, which the UI guidelines route through the overrides mixin so a mistyped name fails the build instead of silently doing nothing. Same values, same output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): skip every test-only naming convention in the theme guard The runtime scan still read .stub, .harness, .fixtures, .spec-stubs, .spec-helpers, .spec-fixtures, test-setup and test-double sources, and test-stubs/ or *fixtures/ directories, so a declaration in one could mask an undeclared runtime read. A file is now test-only when a dot segment after its name marks it (spec, stub, fixture, harness, mock, spec-*, test-*, *-fixtures), its stem is a test bootstrap, or it sits in a test directory. Runtime names such as xtream-connection-test.service.ts stay in the scan; no runtime source imports an excluded file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): keep welcome card text legible while hovered At the 16%/10% hover tint the dark theme's body text on a feature card measured 4.45:1. The hover fill steps up to 10%/6% instead (4.78:1 in dark, 6.6:1 in light); lift, border and shadow carry the rest. The E2E now measures feature and source card text while hovered, in both themes; with the old tint it fails at 4.45. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
197fbee781 |
fix(i18n): retranslate EPG source strings for local XMLTV files (#1890)
Retranslate SETTINGS.EPG_URL_LABEL, EPG_URL_PLACEHOLDER, EPG_URL_ERROR, EPG_SOURCES_DESCRIPTION and EPG_EMPTY_HINT in 16 locales that still kept the URL-only meaning after local XMLTV files became EPG sources (#1600). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f1a5b29be4 |
fix(playback): diagnostic cards wrap translated labels and fit short players (#1892)
* fix(playback): size diagnostic cards to the player and wrap their labels The recovery cards ellipsized every translated action wider than their 124px label column (de, ru, hu, pt, pl, by and others), and the banner took its padding from the window rather than the player. In a short inline player just wider than the 760px breakpoint that squeezed the content until it overflowed, and align-items: center pushed its top above the scroll origin. The panel host is now a size container over the player: padding uses container units, a compact layout applies to players up to 560px tall, auto margins centre the content without clipping its top, labels wrap, and the cards grow to 240px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep short diagnostic players at full content width Measure the layout E2E with the whole banner translated, not only the card labels: the badge, headline, description, hints and utilities set the banner's height too. With them, a by banner in a 920px-wide, 404px-tall player had 45px to spare, because 18cqi side padding left a 588px column. Short players now grow their side padding from 16px at the 760px breakpoint, which keeps the 720px column and about 90px spare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c76cf357b7 |
refactor(workspace): remove standalone-layout branches no route can reach (#1889)
* refactor(workspace): remove standalone-layout branches no route can reach Every routed view sits under the /workspace route (the other top-level paths are redirects), so isWorkspaceLayoutRoute() was true for every caller. Global search became a route on 2026-01-12 and the last route outside the workspace went with the home component on 2026-03-02; nothing opens SearchResultsComponent in a MatDialog. Removed as unreachable: - isWorkspaceLayoutRoute() and the route's data.layout marker; the channel list, Xtream live layout and unified collection read the q query param directly, the clear-view command no longer checks a flag that was always true, and the M3U EPG guide header action keeps only its supportsEpg gate. - The playback sidebar's playlist header (Home button and playlist switcher, showPlaylistHeader, its stylesheet and the macOS padding rule). The switcher itself stays: the workspace header uses it. - Xtream search's dialog mode (MAT_DIALOG_DATA initialQuery, dialogRef, close buttons) and inline search input; Back is offered whenever the search is not the global one, as before. - Stalker search's inline-input flag (Back is always offered, as before). - SearchLayoutComponent's showSearchInput, showCloseButton, closeClick, searchTermChange and focusSearchInput, and SearchFormComponent, which only it rendered. - SETTINGS.BACK_TO_HOME and PORTALS.SEARCH in all 19 locales, the zoneless checklist entry for the deleted form, the specs that only covered these paths, and two Electron E2E assertions that the deleted form is absent. Behaviour of every reachable path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): drop the macOS host class and switcher rule nothing uses The app root's `macos-platform` host class only fed the removed sidebar header's padding rule, so no stylesheet reads it any more. The shared portal-sidebar partial's `.sidebar-header .switcher` rule matched no element: the only `switcher` class lived in that removed header, which used `.current-playlist`, not `.sidebar-header`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7abf479de9 |
fix(ui): one focus ring colour everywhere via the shared mixin (#1882)
* fix(ui): one focus ring colour everywhere via the shared mixin #1866 gave the app a keyboard focus ring token (`--app-focus-ring`, at least 3:1 on every surface), but 59 component rings still drew their own colour: the selection blue under several names (`--app-selection-color`, `--mat-sys-primary`, `--app-selection-border`, the EPG's `$accent-blue`, `--embedded-mpv-accent`, `--apd-accent`, the hero's `--accent-color`), the heading colour, or the overlay's literal text colour. The selection blue falls to 2.6:1 on the stronger selection tint. - Every one now includes `focus-ring.focus-ring-declarations`, keeping its offset; the projects that newly import `libs/ui/styles` declare `ui-styles`. - The mixin reads the token alone: it is declared on `html` in both themes, and the fallback chain cost bytes in every ring. - `tools/nx/check-focus-ring-colour.mjs` joins `styles:focus-visible: validate`: an outline in a focus rule must use the token or, over video, the player's `--pc-*` palette. Three deliberate exceptions are listed with their reasons, and a stale one is reported. On master it reports these 59 rings. - The keyboard-focus E2E asserts each ring's colour equals the token where it is drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): search fields and shadow rings take the focus colour too; guard reads them Local review (Greptile P2): the ring-colour guard read only `outline`, so a focus indicator drawn as a shadow or a border kept any colour. Search fields showed focus as a `--mat-sys-primary`/selection border with a 12-16% halo, the EPG guide's keyboard cell as an inset `$accent-blue` shadow, and the downloads cards tinted their artwork border with the M3 primary. - Every one now uses `--app-focus-ring` (the halos and tints keep their strength through `color-mix()`); over video the panel's search border mixes the overlay's own ring colour. - The guard reads a focus rule's outline, its unblurred ring or line shadows and its border colours. Neutral boundaries (separator and widget-border tokens, transparent, currentColor) and blurred lift shadows are not indicators. On the previous commit it reports these 21 declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): check every focus colour on its own; count exceptions only where they excuse a ring Greptile on #1882 (2×P2): - The guard joined a declaration's colours and accepted the lot when the token appeared anywhere, so `border-color: var(--app-focus-ring) red` or a `color-mix()` of the token and red passed. It now splits each declaration into plain colours, every `color-mix()` argument included, and checks each one: the token, the player palette or a neutral boundary. An outline or shadow ring without a colour is drawn in the text colour, which only a border may keep. - An exception counted as used when its value appeared in any declaration (the diagnostic's amber is also a text colour), so a stale one was never reported. It now counts only where it excuses an off-token focus indicator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): the row a search result reveals takes the app focus ring Codex on #1882 (P2): choosing a settings search result with Enter focuses its row by script, and the keypress keeps `:focus-visible`, so the row's 60%-transparent selection outline was a real keyboard focus ring under 3:1, not the passive marker its guard exception described. The row now includes the shared mixin (keeping its 12px radius), the exception is gone, and the keyboard-focus E2E reveals a row with Enter and asserts the app ring on it; with the old rule it fails on the 60% colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): accept focus colour tokens by exact name Greptile (local, P2): the guard matched `--app-focus-ring` and the neutral boundary tokens by prefix, so `var(--app-focus-ring-other, red)` or `var(--app-separator-strong)` passed. It now reads the property a `var()` names and accepts exactly `--app-focus-ring`, a `--pc-*` palette token or one of the four neutral boundary tokens; their fallbacks are never drawn, since the tokens are always declared, so they are not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): check the body of a focus mixin, the shared ring included Greptile (local, P2): a mixin body has no selector and its includes are not expanded, so the shared `focus-ring-declarations` itself escaped the colour check; turning it red would change every ring and pass. The walker now names the mixin a declaration sits in, and the colour guard treats the body of a mixin whose name mentions focus as a focus rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read logical border sides in the focus colour guard Greptile on #1882 (P2): only `border`, `border-color` and the physical sides were read, so `border-inline-start: 2px solid red` in a focus rule passed. The guard now reads every colour-carrying border property: the shorthand and the physical and logical sides, with or without `-color`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): an exception excuses its own colour, not the declaration Greptile (local, P2): an exception matched the whole declaration, so in the player stylesheet `box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px red`, or the white mixed with red, passed. Exceptions now apply to each plain colour, by exact value, like every other check; an exception counts as used only where it excuses one of a focus indicator's colours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): tell a var() or calc() width from the ring colour Greptile (local, P2): `outline: var(--ring-width) solid var(--app-focus- ring)` failed the guard, since any non-length token counted as a colour. Math functions now count as lengths, and a shorthand with one colour slot takes its literal colour, else an accepted token (the other `var()`s are widths), else reports every candidate it cannot prove; `border-color` still checks a colour per side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read a shadow ring whose spread is computed Codex (local, P2): with `box-shadow: 0 0 0 calc(1px + 1px) red` or a `var()` spread, only three literal zeros were left as lengths, so the shadow looked flat and was skipped. A shadow is now skipped only when it is provably not a ring: a literal non-zero blur in the third place, or every length a literal zero with no `var()` that could be a spread. The test with a variable-width token ring now asserts it is read, not skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read shadow lengths by type, with the colour first or no blur Greptile (local, P2): the guard took a shadow's first three tokens as its lengths, so `0 2px` (a line in the text colour, no blur) was skipped as blurred and a colour-first lift shadow was read as a ring. Lengths are now read by type: a colour sits before or after them, never between, and a missing blur is zero. A `var()` counts as a possible length, so a shadow is skipped only when the first three possible lengths prove a blur, or every length and the first four possible ones are zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): a negated focus condition does not make a focus rule Greptile (local, P2): the colour guard read `:focus-visible` inside `:not()` as a focus rule, so a hover style such as `.button:hover:not(:focus-visible) { border-color: red; }` failed. The selector is now tested without its `:not()` arguments; a focus condition elsewhere, `:has()` included, still makes a focus rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): read a width in any CSS length unit Greptile (local, P2): only px, em and rem were lengths, so `outline: 1pt solid var(--app-focus-ring)` took `1pt` for the colour and failed. Every CSS length unit (absolute, font-relative, viewport and container) now counts as a length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): the open-in-playlist chip rings in the app focus colour Codex on #1882 (P2): the chip's inline component styles drew its focus ring in `--app-selection-color`, out of the colour guard's reach (it reads stylesheets). It was the only inline-style focus ring in the repository; it now uses `--app-focus-ring` like every other ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): move the open-in-playlist chip styles into a stylesheet Codex on #1882 (P2) suggested this over scanning TS inline styles: the chip was the only component with a focus ring in inline `styles`, which the colour guard does not read. Its styles now live in `open-in-playlist-chip.component.scss` unchanged, the ring through the shared `focus-ring-declarations` mixin, so the guard covers it (a drifted colour there is reported). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
afd7a5f331 |
fix(i18n): translate source cleanup, source health and diagnostics strings (#1884)
* fix(i18n): translate source cleanup, source health and diagnostics strings Translate every English-identical SOURCE_CLEANUP, SOURCE_HEALTH and PLAYBACK_DIAGNOSTICS value in all 18 locales, plus the external playback dock statuses (the same strings as the diagnostic ones), the EPG source list strings, and leftover English in hu, ko and el. The identical-English baseline drops 973 entries (2106 -> 1133) and gains none. Add tools/i18n/check-duplicates.mjs (pnpm run i18n:duplicates), a report of English strings defined under several keys whose translations differ per locale, and align drifted wordings: 101 diverging groups -> 65. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): call cleaned-up zh/zhtw sources invalid, not unactivated The cleanup dialog lists expired and disabled accounts too, so 未激活 / 未啟用 ("not activated") misexplained why a source is offered for deletion. Use 失效 ("no longer valid") in the title and the confirmed group heading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): one zh/zhtw wording for copying the stream URL The playback diagnostics button said 复制 URL / 複製 URL while the channel details dialog (new on master) says 复制流 URL / 複製串流網址 for the same action; use the details dialog wording in both places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37df7aceff |
perf(database): skip the per-launch duplicate scan and keep playlist payloads last (#1883)
deduplicateXtreamCache no longer scans the whole catalog on every launch once both unique indexes exist (7.8 s cold on a 3.9M-title library). The playlists table keeps payload as its last column (playlists-table.ts); ensurePlaylistsPayloadLast rebuilds an existing table once, keeping its indexes and triggers, foreign keys off, committing only if references referencing playlists gain no violations. On a full copy of that profile initDatabase went from 5.2-8.1 s to 39 ms and the playlist metadata read from 1.5-2 s to 22-34 ms; the one-time rebuild costs about 4 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
05fc8f9a2e |
perf(dashboard): load playback positions at once and start TMDB rails after Continue Watching settles (#1877)
Continue Watching and the hero gate on every playlist's playback positions (#1841). Those were requested one playlist after another and queued behind the TMDB rails' batched title match, which held the DB worker for up to 95 s on a 3.9M-title library. The positions are now loaded for all playlists at once (dashboard-playback-positions.ts), and the trending and recommendation rails start only after Continue Watching has settled (rails/dashboard-tmdb-rails.ts). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7d7d691ce4 |
fix(ui): skeletons that match the M3U header and dashboard rails (#1887)
* fix(ui): skeletons that match the M3U header and dashboard rails Loading skeletons now take their boxes from the code that draws the content, so the content lands where the skeleton was. - M3U channel list: the loading state's headers, divider, groups rail and first row share `styles/_channel-list-layout.scss` with the All channels, context and groups views. The All channels list moved down by 19px when a playlist loaded; the group header was 79px against the real 69px (the panel header's 52px minimum plus the padding that `.sidebar .sidebar-header` gives it, now one mixin both include). Group rows start at the list's top-left like the virtual-scroll rows. - Dashboard rails: `lib-dashboard-rail-skeleton` replaces six copies of the skeleton markup and takes the rail's layout and aspect ratio (channel cards for live favorites, 16:9 for sources, posters with a title and a meta line otherwise). Its header, track, cards and text line boxes come from `_dashboard-rail-geometry.scss`, which the rail itself now uses. The skeleton was 328px against the 337.8px poster rail and a 2:3 poster in front of the 83.5px channel cards. - One shimmer in `libs/ui/styles/_skeleton-shimmer.scss`, mixed from `--app-on-surface`: the rail sweep between the widget surfaces was 1.04:1 on the light theme. The dashboard hero and the detail skeletons (through a forward) use it too. - Electron E2E `skeleton-geometry.e2e.ts` holds the IPC channel behind each loading state, compares skeleton and content boxes to 0.5px at 1280x700 and caps the dashboard layout-shift score at 0.01. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dashboard): keep rail heights when a skeleton stands for a label-less or live rail Review follow-up (Greptile, local): - Rails that replace a skeleton (Continue Watching, sources, Xtream recently added, TMDB) keep an empty meta line under cards without labels (`reserveMetaLine`), as the skeleton always draws one; a Continue Watching rail of movies with no known duration came in shorter. - The recent-content skeleton follows the recent rail it waits for: posters while Continue Watching is still loading with no cards, channel cards otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ea7efdbd9b |
fix(workspace): open a source row from the keyboard (#1885)
A Sources row opened only on a pointer click: the row div was not focusable and had no key handler, so a keyboard user could Tab to its actions but never open the playlist. Following app-content-card, the row's title and meta block becomes the keyboard activation element: role="button", tabindex="0", named by the source title, aria-current on the active source, aria-disabled while busy. Enter and Space open the source (Space prevents the page scroll). The drag handle, health indicator and actions stay its siblings, so no control is nested inside a role="button" and their keys never bubble into it. A click anywhere on the row still opens it via the row. The row draws the ring with the shared focus-ring mixin, inset, because the list's scroller would cut a ring drawn outside the full-width row. playlist-shared-ui now declares its ui-styles stylesheet dependency. Tests: unit spec for Enter/Space, bubbling from an action button, busy and selected states; Electron E2E tabs to a source row, checks the ring and opens it with Enter (fails on the previous component). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4281194cb4 |
fix(a11y): accessible names for icon-only buttons and a guard (#1880)
* fix(a11y): accessible names for icon-only buttons and a guard
Icon-only buttons named only by a matTooltip (or by nothing) had no
accessible name: the icon ligature is hidden from assistive technology
and a tooltip only adds a description. 22 buttons on master, in .html
and inline templates, now carry a translated aria-label bound to their
tooltip key. The channel row's favorite star keeps one label
("Favorite") and reports its state through aria-pressed.
New guard `pnpm run a11y:icon-buttons:validate` (CI step) fails on a
<button> whose only content is mat-icons (through control flow,
ng-container and spinners) without aria-label, an aria-label binding
or aria-labelledby. The inline-template lookup moves to a shared
tools/nx/inline-templates.mjs used by the icon-ligature guard too.
web-e2e icon-button-names.e2e.ts runs axe's button-name rule on a
channel list, the channel details dialog, Sources, the playlist info
dialog and an Xtream search. Five new keys are translated in all 18
locales.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(a11y): text hidden from assistive technology never names an icon button
Greptile: a static aria-hidden="true" child's text counted as the
button's name, so <button><mat-icon/><span aria-hidden="true">Close</span>
passed the guard. Hidden subtrees now add only their icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ea515280e3 |
fix(catalog): short sort chip label and radio sort menus (#1881)
* fix(catalog): short sort chip label and radio sort menus
The catalog sort chip now shows one short label per mode at every width
("Newest", "A-Z", "Top rated") and keeps the full "Sort: ..." text in its
aria-label, so it no longer truncates in long translations. The rating
chip follows the same rule and puts its clear icon after the value. The
1120px container query that swapped full and compact labels is gone.
Single-choice mat-menus get a shared appMenuItemRadio /
appMenuItemRadioCheck pair: rows are menuitemradio with aria-checked, and
every row reserves a leading check slot that is visible only when
checked. Material projects every mat-icon ahead of the label, so the old
check rendered only on the chosen row shifted that row's label. Applied
to the catalog refine menu (sort and rating groups), Xtream live
channels, M3U all channels and groups, unified collection favorites,
workspace categories and workspace sources.
Adds WORKSPACE.SORT_CHIP keys in all locales and drops the unused
WORKSPACE.FILTER_RATING key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(catalog): read the full sort to screen readers from hidden text
A plain div cannot carry an accessible name, so screen readers could skip
the chip's aria-label and read only the short "Newest". The full
"Sort: ..." text now sits in a visually hidden span and the short label
is aria-hidden; the polite live region announces the full text on change.
The spec checks the text outside aria-hidden, and the E2E checks the
chip's accessibility tree in en, de, ru and hu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|