mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
f6a216f59ac3d6b16404165684a2fa693c78ff52
189
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
176108b682 |
fix(web): keep server storage off the startup bundle; form-field defaults on sign-in
PwaService imported isBackendUrl from the @iptvnator/services/server-storage barrel, which pulled the whole storage subsystem (sync, migration, API client) into main, though server mode loads it by dynamic import. The helper now lives with the token helpers in @iptvnator/services (re-exported from the interceptor): renderer.initialBytes 1,392,902 -> 1,357,795 bytes locally. The remaining 11,988 bytes over #1931's baseline are what must run before the first route (instance probe, connection status, token bound to its instance, storage backend and write serialization); the baseline is raised with a note, under the PR's perf-baseline-increase label. The sign-in component provides the form-field defaults per component, as master's check (#1928) now requires. 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
de87c1e4d3 |
Merge branch 'master' of https://github.com/4gray/iptvnator into claude/server-storage-pwa
# Conflicts: # docs/architecture/pwa-self-hosted.md |
||
|
|
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> |
||
|
|
2a93100ec4 |
perf(workspace): style the server banner from the shell, baseline 570
The offline banner's own stylesheet added a <style> element to every launch, one of the two mutations the launch journey counted on top of master. Its rules move into the workspace shell's stylesheet. The other one is a parser-inserted modulepreload link: the new lazy server-storage chunks split one shared initial chunk in two. A/B on macOS: master 555, this branch 556 (was 557); the baseline note now says so instead of blaming @if anchors, which cost nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cedc468325 |
Merge branch 'master' of https://github.com/4gray/iptvnator into claude/server-storage-pwa
# Conflicts: # libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts |
||
|
|
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 (
|
||
|
|
abae207d02 |
fix(server-storage): unsent writes survive reloads, guarded move, lazy storage
- User-data writes the server has not confirmed are kept under iptvnator-server:unsent-writes and sent first on the next visit. - Moving browser data does not start while user-data writes are unsent, and a duplicate of a failed upload counts as failed, not merged. - The session reaches localStorage through the lazy adapter, so a browser that denies storage still boots. - Raise launch/renderer.domMutationsToFirstCard by 2 (569 → 571): the rail server shortcut and the offline banner each leave an @if anchor in every build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
77df7922c1 |
feat(i18n): server storage strings
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
17a2b1b3b0 |
fix(ui): visible keyboard focus everywhere via a global focus-visible fallback (#1866)
* fix(ui): visible keyboard focus everywhere via a global focus-visible fallback The global stylesheet removed the outline from every input, button, textarea and `:focus`, so Tab users saw no focus on about 110 raw buttons: detail actions, season tabs, chips, title results, list rows. - Remove the outline only under `:focus:not(:focus-visible)` and draw a fallback ring on every other `:focus-visible` element. Both rules sit in `:where()`, so any component that styles its own focus still wins. - One recipe: `focus-ring-declarations` moves to `libs/ui/styles/_focus-ring.scss` and reads `--app-focus-ring`, declared per theme (light #1d63e0, dark #8cbaff) with at least 3:1 on every app surface and selection tint; `m3-theme.spec.ts` measures it. The card ring, the detail actions, the portal sidebar and both context panels use the mixin (the panels' selection-blue ring fell to 2.97:1 on the active item). - Material Tab stops (buttons, switches, button toggles, checkboxes) take the ring over their 12% focus state layer; menu items and options keep Material's highlight. - Surfaces over video (player controls, vendor chrome, fullscreen panels, Up Next rail) point the ring at the player's text colour. - The selected season tab drew its border with `outline`, which outranks the fallback; it is a spread shadow now. - Guard: `pnpm run styles:focus-visible:validate` (CI) rejects a global `outline: none` on an unscoped selector and a missing fallback. - Electron E2E `keyboard-focus-ring.e2e.ts` tabs through the detail actions and season tabs, a catalog grid with its refinement chips, the Sources list and Settings in both themes: one ring per stop, 3:1 where measurable, none after a click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): keep the player ring in the fullscreen panel; tighten the focus guard and E2E Local review round 1 (Codex P2, Greptile 3×P2): - The fullscreen channel panel carries `dark-theme`, whose context declares the theme ring again, so its own controls ringed in #8cbaff. Point the token back at the player's text colour on `.fullscreen-channel-panel .dark-theme`; the web series-playback E2E checks the close button's ring. - The guard took a container-scoped rule (`.panel :focus-visible`), a mixin body or a media query as the global fallback. The fallback is now the whole selector, outside any at-rule. - The keyboard-focus E2E now fails a ring that an `overflow: hidden` ancestor (up to the scroll container) cuts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): show focus where a component removes its outline; catch invisible rings in the guard GitHub review round on #1866 (Codex P2, Greptile P2): - A component rule that sets `outline: none` outranks the zero-specificity fallback. Re-audited every one: two hid a Tab stop with nothing else to show. The EPG list row (`role="button"`) now draws an inset ring, and the command palette underlines its search row while the field has focus. The others already show focus on the field's wrapper, on another element, or as a highlight inside an arrow-key composite (the "…" menu, the guide's search listbox); the guidelines now state the rule. - The guard read only a bare zero or `none` as a removal. It now reads a zero width, a `none`/`hidden` style or a transparent colour anywhere in the shorthand or longhands, so `outline: 0 solid transparent` is reported and `outline: 2px solid transparent` is no fallback. - The keyboard-focus E2E walks the live EPG list and the command palette too, and reads an inset or offset shadow line as a ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(player): keep --pc-text a literal palette colour Master's palette spec (#1854) reads `--pc-text` from the controls host as a literal; this branch had made it `#{palette.$text}`, failing "sets the timeline label on the dense glass" on the merge ref. The host declares the literal again, and a spec keeps the palette's `$text` (the focus ring token for surfaces beside the host) equal to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(epg): declare the ui-styles dependency of the list row's focus ring The EPG list row now `@use`s `libs/ui/styles/_focus-ring.scss`; Nx cannot infer a Sass import, so `ui-epg` declares `ui-styles` like the other consumers of the shared partials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tooling): a styleless outline shorthand hides focus too `outline: 2px` or `outline: red` resets the style to `none`, yet the guard counted either as a visible fallback. A shorthand without a drawn style (or a `var()` that may carry one) now counts as removing the outline, as do `initial` and `unset`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2b400cb81d |
fix(i18n): translate the remaining hard-coded labels (#1871)
* fix(i18n): translate hard-coded labels, snackbars and missing keys Catalog screens, snackbars and external player messages showed English in every locale, and seven keys used in code were missing from en.json, so ngx-translate rendered them raw. - Catalog: "All items", item and channel counts, channel sort menus and tooltips, unnamed-category and empty-category labels, LIVE/PAUSED badges and the Xtream global search summary are translated. The Xtream and Stalker stores no longer bake an English name into the every-item sentinel; the facades return a null title and the view translates it. - Snackbars: Xtream/Stalker request failures, the 413 upload error, backup export/import results and the category visibility failure use keys; every "Close" action uses CLOSE. - External player: the main process sends an error code instead of an English sentence (player-error event, session errorCode, and a tag in rejected launch errors); the renderer, dock and VOD primary button translate it. The external player info dialog is translated. - Adds the seven missing keys and 43 new ones, translated in all 18 locales; seven legitimately identical values are baselined. - tools/i18n/check-usage.mjs fails on keys used in code but missing from en.json; it runs in i18n:check (and so in CI through i18n:validate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): translate catalog counts in the template and skip inline template comments - The category subtitle hands a key to the translate pipe instead of caching translate.instant(), so a cold start re-renders it once the language file loads. - The Xtream live root count uses the singular/plural item keys, so one result no longer reads "1 channels". - check-usage.mjs strips HTML comments inside inline templates of TypeScript files, so a commented-out key no longer fails the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): check keys in parenthesised translate pipe operands `(expanded() ? 'SHOW_LESS' : 'SHOW_MORE') | translate` yields keys that neither precede the pipe directly nor contain a dot, so the usage check missed them. It now reads the ternary and fallback branches of a parenthesised operand; a literal compared in the condition is not read as a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): expect the translated external player failure in the dock A launch failure without an error code now shows the translated generic status in the playback dock, with the raw main-process detail as its tooltip. The ClearKey DASH flow asserted the raw English text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep the IPC error tag out of the stored session detail A tagged launch failure reached ExternalPlayerSession.error unchanged, so the dock tooltip showed "[iptvnator:external-player:start-failed]". The registry now strips the tag when it stores the detail; the rejected IPC error keeps it for the renderer to read the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web): expect translated portal request failure toasts #1861's new resolved-failure specs asserted the English toast text; the toasts now go through PORTALS.REQUEST_ERRORS keys, so the specs check the key and its message/status params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): read only returned branches of a grouped translate operand A literal at the start of a condition, as in `('ERROR' === status() ? 'CLOSE' : 'CLOSE') | translate`, was taken for a translation key, so a valid template could fail the usage check. A grouped literal now counts only when it ends its operand (end of group, `:`, `||` or `??`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): translate the remaining hard-coded labels Follow-up to the UI-26 pass. The remaining English UI literals now come from translation keys, translated in all 18 locales: - Windows/Linux window controls, playlist switcher title and actions menu, the portal status tooltip, dashboard carousel roles and rail scroll buttons, the search placeholder, the card remove tooltip, the EPG offset unit, the REC chip, the Stalker EPG source label and the export file type. - Embedded MPV failures raised in the renderer and the release-notes dialog without a bridge. Settings never showed its English reasons, so they are dropped. - Unnamed Stalker episodes: data access leaves the title empty and the series view labels it after the TMDB overlay. Synthetic season names stay, because they key persisted episode progress. - The phone remote-control page, which follows the first browser language with a translation and sets <html lang>. Removes the dead getStatusMessage(), the unused favorites layout and the translateWithFallback() helpers whose keys now exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(remote-control): keep the remote build off shared-interfaces The language resolver imported the Language enum, which made remote-control-web depend on shared-interfaces. Its build-performance chain then hit Nx's recursive-invocation guard through the existing shared-interfaces/shared-logging build loop, failing the Electron E2E and performance journey builds. The resolver now keeps its own list of translation codes, and a spec checks it against the locale files the page loads. Also drops the deleted favorites layout from the zoneless checklist, whose guard spec requires ticked entries to exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(journeys): raise the launch DOM mutation baseline for translated attributes Launch now sets 569 DOM mutations before the first dashboard card instead of 557 (identical in all three CI iterations). The extra twelve come from attributes this PR moved from static English to translated bindings on the launch path: the window-control labels and tooltips on Linux, the hero carousel and slide roles, and the rail scroll-button labels. A translated attribute is a binding set after the element is attached, so each costs a mutation. Accepted as a deliberate trade-off; it needs the perf-baseline-increase label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6e18983400 |
fix(i18n): translate hard-coded labels, snackbars and missing keys (#1867)
* fix(i18n): translate hard-coded labels, snackbars and missing keys Catalog screens, snackbars and external player messages showed English in every locale, and seven keys used in code were missing from en.json, so ngx-translate rendered them raw. - Catalog: "All items", item and channel counts, channel sort menus and tooltips, unnamed-category and empty-category labels, LIVE/PAUSED badges and the Xtream global search summary are translated. The Xtream and Stalker stores no longer bake an English name into the every-item sentinel; the facades return a null title and the view translates it. - Snackbars: Xtream/Stalker request failures, the 413 upload error, backup export/import results and the category visibility failure use keys; every "Close" action uses CLOSE. - External player: the main process sends an error code instead of an English sentence (player-error event, session errorCode, and a tag in rejected launch errors); the renderer, dock and VOD primary button translate it. The external player info dialog is translated. - Adds the seven missing keys and 43 new ones, translated in all 18 locales; seven legitimately identical values are baselined. - tools/i18n/check-usage.mjs fails on keys used in code but missing from en.json; it runs in i18n:check (and so in CI through i18n:validate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): translate catalog counts in the template and skip inline template comments - The category subtitle hands a key to the translate pipe instead of caching translate.instant(), so a cold start re-renders it once the language file loads. - The Xtream live root count uses the singular/plural item keys, so one result no longer reads "1 channels". - check-usage.mjs strips HTML comments inside inline templates of TypeScript files, so a commented-out key no longer fails the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): check keys in parenthesised translate pipe operands `(expanded() ? 'SHOW_LESS' : 'SHOW_MORE') | translate` yields keys that neither precede the pipe directly nor contain a dot, so the usage check missed them. It now reads the ternary and fallback branches of a parenthesised operand; a literal compared in the condition is not read as a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): expect the translated external player failure in the dock A launch failure without an error code now shows the translated generic status in the playback dock, with the raw main-process detail as its tooltip. The ClearKey DASH flow asserted the raw English text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep the IPC error tag out of the stored session detail A tagged launch failure reached ExternalPlayerSession.error unchanged, so the dock tooltip showed "[iptvnator:external-player:start-failed]". The registry now strips the tag when it stores the detail; the rejected IPC error keeps it for the renderer to read the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web): expect translated portal request failure toasts #1861's new resolved-failure specs asserted the English toast text; the toasts now go through PORTALS.REQUEST_ERRORS keys, so the specs check the key and its message/status params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): read only returned branches of a grouped translate operand A literal at the start of a condition, as in `('ERROR' === status() ? 'CLOSE' : 'CLOSE') | translate`, was taken for a translation key, so a valid template could fail the usage check. A grouped literal now counts only when it ends its operand (end of group, `:`, `||` or `??`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2dff91c9f2 |
fix(ui): replace icon ligatures missing from the font and guard them (#1864)
* fix(ui): replace icon ligatures missing from the font and guard them The download and recording queues showed the literal text "file_off" for a missing file: the bundled Material Icons font (material-design-icons-iconfont 6.7.0) has no such ligature, so it rendered as text overflowing the icon box. Use error_outline, which the missing state's tertiary palette keeps distinct from the filled error glyph of a failed download. Add `pnpm run styles:icon-ligatures:validate` (CI): it parses templates with @angular/compiler and TypeScript with the compiler API, collects static <mat-icon> text, the string results of its bindings, icon/*Icon inputs and icon-named TypeScript values, and fails on a name missing from the font's codepoints file. Two listed codepoints the font has no ligature for (rounded_corner, stairs) are excluded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): check quoted inline templates in the icon guard Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): accept every codepoint and report escaped template lines The first measurement rendered the names without the package's CSS. With material-design-icons.css, all 2,193 codepoint names, rounded_corner and stairs included, draw as one glyph, so the guard accepts the whole file. Inline templates now map each cooked position back to the source through escapes, line continuations and CRLF, so a name after `\n` in a quoted template is reported on the line it is written on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
015ea203b7 |
fix(player): keyboard-reachable, bounded labels for timeline segments (#1854)
Seek-bar segments (catch-up programmes, file chapters) now reach keyboard, touch and screen-reader users through the shared timelineSegments path: - aria-valuetext reads the translated "<title> · <time>" inside a titled segment, the plain time otherwise. - ControlsTimelineLabel anchors the label on keyboard focus and drag previews as well as pointer hover. - Two-part label (ellipsized title, whole time), clamped by its measured width and re-placed on resize to stay 8px inside the player. - Opaque --pc-timeline-track with white separators clamped to the track, so boundaries hold 3:1 over any frame. - Translated LIVE badge; LIVE and --:-- are named role="img" elements in both docks. - epglong Xtream mock scenario and an Electron E2E in both themes at 1280/800px and in de/ru. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8030ced95a |
feat(settings): grouped navigation, cards and switches from the settings concept (#1853)
* feat(settings): grouped navigation, cards and switches from the settings concept Implements the Claude Design "Settings concept" handoff. Navigation: the sidebar is a 20px "Settings" title over App, Library, Devices and Data groups, with About pinned to the footer beside the installed version and an update badge. The active item is a soft fill without a border. Esc leaves settings like the header Back. Reset is no longer a page: its one destructive action is the last card of "Backup & data". Pages: a title and one-line subtitle, then rows grouped into titled cards with one right-aligned control column. Selects are compact without floating labels, checkboxes are switches, segmented controls are pills, and descriptions are capped at 56ch. "Show subtitles" moves to Playback, the Embedded MPV note becomes a callout under the player select (only while it is selected), and the TMDB attribution becomes the About footer. EPG: Add and Refresh all sit in the Sources header, the empty state has its own Add, the format examples are one line, Clear EPG data is its own row and the offset is a stepper. About: version, update state and channel share one card; the nightly warning is a callout shown only while Nightly is selected, with a shortcut to the backup page; the support buttons are neutral with coloured icons. Save model: the save bar stays (design option B) and now floats over the content column, counts the staged changes, answers Cmd/Ctrl+S, and marks pages with staged edits in the sidebar. A saved change that waits for a restart shows a dismissible notice. Rows stack under 600px of pane width (the page is a size container, so the persistent sidebar is accounted for). Page-specific styles moved into the About, EPG, Backup and Remote control section stylesheets to stay within the component style budget. New SETTINGS keys are translated in all 19 locales and NAV_RESET is removed; brand names and loanwords that stay English are in the identical-value baseline. E2E: Material slide toggles report aria-checked after the next render, so both suites toggle through a setSwitch helper; the settings nav is a navigation landmark, so the Dashboard rail link is scoped to the rail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compare restart notices against the running app, fix CI fallout The restart notice now lists only the restart controls whose saved value differs from what the running app uses: the stored values at first load, or what the embedded engine reports it actually runs for the frame-copy opt-in. Saving the launch value back withdraws the notice instead of leaving it up with no chip to explain it. The refresh reads the current list untracked and writes only a changed one, because the engine probe effect calls it. CI: the zoneless checklist dropped the deleted Reset section component, and the theme-tokens Electron E2E floats the recording folder label on the Playback page now that the settings selects carry no floating label. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep a dismissed restart reminder away until the setting changes "Later" now records the saved value it dismissed, for the rest of the app run, so an unrelated save (or reopening Settings) does not bring the same reminder back. It returns once a restart setting is saved with another value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f3c576d182 |
fix(tools): follow weight variables in both readings of a keyframe (#1850)
* fix(tools): follow weight variables in both readings of a keyframe A keyframe that sets a font only while it runs makes scanWeights scan the file twice: once with the keyframe over the rule that runs it, once with the rule after it. The wrapper merged only the findings and the deferred weights of the second pass, so a weight read through a variable (`font-weight: $w`, `var(--w)`, `font: $w ...`) that meets JetBrains Mono only after a non-holding animation was never followed to its definition. Merge the references too; findIndirectWeights already dedupes a reference both passes record. Definitions, call sites, loads and declaration counts do not depend on the reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(tools): cover a keyframe run by another module's mixin Since #1795 the second reading also starts when an included mixin from another module runs a keyframe that does not hold its frame. Its references went through the same wrapper and were dropped the same way; cover that path too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): report a weight definition once however it is reached findIndirectWeights follows each Sass reference from its own read site, so a variable two rules read was reported twice at its definition. Both readings of a non-holding keyframe now reach it too, with different caps, so a computed weight (`$w: calc(400 + 100)`) got the same warning twice. Deduplicate its findings by what they report, the key findWorkspaceWeights already used for landed findings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8581bddcaf |
perf(web): keep the NgRx store devtools out of production bundles (#1810)
* perf(web): keep the NgRx store devtools out of production bundles app.config.ts imported @ngrx/store-devtools statically and gated it on AppConfig.production at runtime, so the optimizer kept the module in main.js for the production, PWA and performance builds. The providers now come from environments/store-devtools.providers.ts, an empty list in every build; only the development and electron-e2e configurations swap in the devtools through fileReplacements. A build-config test keeps it that way. renderer.initialBytes: 1,608,610 -> 1,596,045 bytes (-12,565) 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 #1810 as the initial-bytes baseline evidence 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> |
||
|
|
e998d7418a |
chore(deps): bump the actions-minor-patch group across 1 directory with 2 updates (#1840)
* chore(deps): bump the actions-minor-patch group across 1 directory with 2 updates Bumps the actions-minor-patch group with 2 updates in the / directory: [pnpm/action-setup](https://github.com/pnpm/action-setup) and [github/codeql-action](https://github.com/github/codeql-action). Updates `pnpm/action-setup` from 6.0.10 to 6.1.0 - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v6.0.10...v6.1.0) Updates `github/codeql-action` from 4.37.7 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.7...v4.38.2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-patch - dependency-name: pnpm/action-setup dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> * test(packaging): allow pnpm/action-setup v6.1.0 in the Snap build workflow policy The bump moves every workflow to pnpm/action-setup@v6.1.0; the build workflow's allowlist pins the exact version and must move with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0a006cb027 |
ci(perf): enforce the journey counters stable on master (#1829)
* ci(perf): enforce the journey counters stable on master Promote the J1, J2 and J3 counters that were identical in all 55 measured iterations of the 11 master runs from 2026-10-03 to 2026-10-04 to journey-baselines.json, and check them in the Performance journeys job (still warn-only), in one step together with #1828's two validated J1 entries. None of the new ones has Principle 3 evidence, so each carries a "guard only, not validated" note that the checker prints with a failure. A performance-tools test keeps the job's --only list equal to the journey entries. Number formatting uses three decimals, the precision of the layout-shift scores. J2 renderer.layoutShiftScore is 0.233, not the window's 0.222: every master run from #1814 (page Back buttons in the header) on reads 0.233. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(perf): check the journey counters whenever a summary was written Review follow-up (Greptile): the check ran only after the composite action succeeded, so a failed job-summary report after a written summary.json skipped every baseline. It now runs unless the job was cancelled, as long as the action produced a summary path. 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> |
||
|
|
de2e1b19d7 | docs(website): publish player controls and library guides | ||
|
|
b315f8564d |
fix(electron): show the main window once its document has loaded; enforce J1 IPC and mutation counters (#1828)
* fix(electron): show the main window once its document has loaded; enforce J1 IPC and mutation counters Re-lands #1788, which merged into #1782's branch after #1782 had already reached master, so none of it is on master. The hidden main window was shown on ready-to-show only. On Linux under X11, when the startup scripts run before the window's first frame, the next frame comes about a second later: nothing is on screen and the splash's requestAnimationFrame waits, so J1's first card came ~940 ms after load instead of ~480 ms in most runner launches (18 bridge calls / 1,031-1,033 DOM mutations instead of 15 / 558). The window is now shown at ready-to-show or the main frame's did-finish-load, whichever comes first, with the splash colour as its background so showing before the first paint does not flash. The journey gate keeps the app's did-finish-load listeners away from its about:blank detour, as it already does for ready-to-show. Three dispatched runs on this branch (37192092882, 37192097790, 37192103151) read 15 calls and 558 mutations in all 18 iterations, stable: true. Both become baselines (slack 0), and the Performance journeys job checks them with check-journey-ratchet.mjs --only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(perf): record the evidence PR of the J1 runtime baselines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: 4gray <fourgray@proton.me> |
||
|
|
bf3cf87b65 | docs(website): publish updates and backup guides | ||
|
|
7fd3d1dab0 | fix(tools): capture the Xtream guide shot against the current connection test (#1807) | ||
|
|
040acbd976 |
feat(tools): land mixins from another Sass module where they are included (#1795)
A mixin included from another Sass module (`@use 'x'; @include x.m`) now lands where it is included, as a same-file mixin already did: its top-level weights meet the including rule's family, and its family becomes that rule's, in the order Sass writes them out. Include sites resolve through the existing `@use`/`@forward` scope resolution, the definition Sass resolves is the one that runs, and an include inside a mixin body resolves where that mixin runs. The header's "Not traced" list keeps what stays out (positional arguments, content blocks placed by another module's mixin, a name two `@import`ed files define). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e8b181fcea |
fix(ui): Cyrillic/Greek weights, html lang, weight normalisation (#1780)
Load Roboto 600/700 and DM Sans 700 so Cyrillic and Greek headings render real semibold and bold faces instead of a synthetic bold, keep <html lang> in step with the UI language, and move every font weight onto the 400/500/600/700 scale (JetBrains Mono at 500 or lighter; the dashboard LIVE badge now uses the interface font at 700). Add the `styles:font-weights:validate` ratchet guard and its CI step. It reads stylesheets much as Sass and the browser do (cascade, layers, mixins, content blocks, `@extend`, `@at-root`, `:is()`/`:where()`, keyframes) and lists what it deliberately does not trace in its header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ab8460338a | feat(ui): cinematic movie and series details pages and dashboard hero (#1792) | ||
|
|
0768ae5ea2 |
ci(i18n): fail on new English-identical translations (#1793)
* ci(i18n): fail on new English-identical translations The drift check only warned about locale values identical to English, so untranslated strings kept landing. It now fails on any such value that tools/i18n/identical-en-baseline.json does not record for that locale and key. The baseline captures today's 2,015 entries: legitimately identical values (brand and technical names, language autonyms, PIN) and the existing debt. An entry only covers the English text it recorded, so copying reworded English into a locale fails too. Baseline entries that are no longer identical are reported, not fatal. `pnpm run i18n:baseline:update` rewrites the baseline deliberately; CI runs `pnpm run i18n:validate` (node tests, then the check) and never rewrites it. `--fail-on-identical` remains as a strict audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): keep the baseline intact on failed updates and strict audits `--update-baseline` now writes nothing while any locale is unreadable or has missing or extra keys, so an incomplete translation cannot reshape the baseline. `--fail-on-identical` no longer reads the baseline it ignores, so a damaged file cannot block a strict audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a8dd1eaa97 |
fix(import): consistent add-source forms with masked passwords and URL errors (#1796)
* fix(import): consistent add-source forms with masked passwords and URL errors - Mask the Xtream password in add and edit (and the Stalker one in edit) behind a shared PasswordVisibilityToggleDirective: one translated "Show password" label, state in aria-pressed, type="button". - Give the Xtream server URL its own mat-error and a neutral hint instead of the EPG file error; give the M3U URL a mat-error. - Use "Playlist title" in every add form, "MAC address" casing, a single ellipsis in "Validating portal…" and one "Add playlist" submit label; translate the method radiogroup's aria-label. - Show Stalker refusals inline under the portal URL (role="status", like the Xtream connection test), translated in the template and cleared by edits; translate the snackbars for outcomes that close the dialog. - Translate new strings into all locales; reuse the identical Stalker URL error translations; fix MAC casing and ellipses; drop unused keys. - Unit specs per form, edit-dialog spec, new add-source-forms web E2E; update E2E locators; UI guidelines Forms section; Stalker contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): mask the password again when an add form is cleared Clear erased the password but left the visibility toggle on, so the next password typed in the Xtream or Stalker form showed in plain text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb252940b2 | fix(workspace): move detail Back into the header and drop the rail brand (#1789) | ||
|
|
572034f3be | fix(ui): declare Material system tokens and migrate dead --mdc overrides (#1775) | ||
|
|
8b6fcf3560 | fix(e2e): let web-e2e:e2e run outside CI (#1779) | ||
|
|
ec8b931dbf | ci(perf): add the weekly baseline tightening workflow (#1760) | ||
|
|
388fa9e29d |
chore(release): pick the 0.25 highlights and add a settings search screenshot (#1727)
* chore(release): add 0.25 highlights and a settings search screenshot Mark the deferred Electron startup wiring as the "Faster startup" highlight next to settings search, and give the settings search note a screenshot: a new `open-settings-search=<term>` capture action types the term into the header search and waits for the ranked results. Guard the manifest tooling with tests that validate the committed screenshots.manifest.json and keep KNOWN_ACTIONS in step with the capture navigation action tables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(release): hash the capture action tables for release-tools:test The KNOWN_ACTIONS parity test reads capture-navigation-*-actions.ts, so those files must invalidate the cached test result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(release): trim 0.25 to three highlights Keep redesigned player controls, parental lock and the cinematic dashboard hero as the headline changes. Settings search, faster startup, the player settings panel and the up next card stay as regular notes; settings search keeps its screenshot. 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> |
||
|
|
963a431bb7 |
docs(coverage): record why two-core Tier A runs stay serial (#1741)
* docs(coverage): record why two-core Tier A runs stay serial Answers two review notes on the concurrent Tier A runner with measurements instead of code changes: - Two cores stay serial. Two in flight would give each project one Jest worker, which runs Jest in-band; on a 2-core / 7 GB container ui-playback and web ran out of their 2 GiB default heap. With a 4 GiB heap it passed about 15% faster on a warm cache for 1.2 GiB more peak memory. CI runs on 4 cores. A test pins that the defaults never drop to one worker. - Per-project output buffering is bounded in practice: a big project with every test failing printed 1.8 MB while its Jest process peaked at 850 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(coverage): say two-core runs keep two workers per project 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> |
||
|
|
66e9a71600 | docs(website): publish fullscreen and stream info guides | ||
|
|
76dd8c099e | ci(test): download the Electron binary once before unit specs run (#1739) | ||
|
|
af44e2368b |
ci(performance): give the initial-bytes ratchet slack and a labelled override (#1744)
* ci(performance): give the initial-bytes ratchet slack and a labelled override The exact renderer.initialBytes counter failed PRs for reasons outside their diff: two concurrent merges left master 108 bytes over the baseline for hours, and bundler identifier renaming moves the counter by hundreds of bytes. PRs growing it by 243 and 302 bytes had no way to pass at all, because the direction check refuses any raised baseline. - Counter entries accept `slack` (integer, entry unit): the ratchet enforces `value + slack`, reports how much slack a measurement uses, and still prints the tighten hint below `value`. renderer.initialBytes gets 4096 bytes, so growth can accumulate at most 4 KiB past the last lowered baseline while regressions such as +35 KB still fail. - check-baseline-direction.mjs compares `value + slack`, treats widened slack like a widened tolerance, and takes `--allow-increase`, which reports weakened entries as ALLOWED instead of failing. - CI passes `--allow-increase` only when the pull request (or, for a master push, the pull request merged as the pushed commit) carries the perf-baseline-increase label, read from the API so a job re-run picks up a label added later. This change widens the slack itself, so its own PR needs the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs * fix(performance): report slack usage only for entries that have slack A wall-clock measurement above `value` but within `value × toleranceRatio` fell into the slack branch and was reported as using "slack", conflating timing tolerance with counter slack. Only entries with `slack` report it now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs * fix(performance): scope the push-time label and refuse raised counter values - A master push compares the whole push, but the label was read from the head commit's PR only, so one labelled PR could cover another commit's increase in the same push. The label now counts only when the push added exactly one first-parent commit (a squash or merge of one PR); any other push that weakens a baseline fails. - Raising a counter's `value` while narrowing its `slack` lowered the enforced limit and was reported as "lowered". A counter's value is the measured evidence and only moves down, so that raise is now a weakening that needs the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs * fix(performance): treat a counter/wall-clock type switch as a weakening Turning `{ value: 100, slack: 10 }` into `{ value: 105, toleranceRatio: 1 }` skipped the raised-counter-value rule and was reported as a lowered limit. Switching an entry between counter and wall-clock now needs the perf-baseline-increase label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs * ci(performance): paginate the label lookups of the direction check The labels endpoint returns 30 entries per page by default, so a PR with more labels could miss perf-baseline-increase. Both lookups now request 100 per page and paginate, like the release-note gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d8229b98fa |
feat(playback): record recently viewed only after the stream plays (#1732)
* feat(playback): record recently viewed only after the stream plays A channel, movie or episode used to enter Recently Viewed (and the dashboard's Continue Watching hero) the moment it was selected or its link was resolved, so streams that failed straight away cluttered the history. Writers now defer the write to a root PlaybackHistoryGate, keyed by the stream URL and/or the playback session key. The inline players confirm those keys once the owned engine's position has advanced by two seconds (seeks, stalls, pauses and a previous stream's progress do not count), the radio player does the same, and a launched MPV/VLC session confirms on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on selection. Covers M3U (live, radio, movie detail), Stalker (live, radio, VOD, series), Xtream VOD and series, and the global live collection. The M3U host's embeddedPlayback is now compared by value: the history write updates the playlist meta mid-playback, and a new but identical playback object remounted the engine and restarted the stream. E2E flows that relied on recording-on-click now play local fixtures (HLS/TS/WebM routed in place of unreachable or public streams) and wait for confirmed playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): tighten recently viewed confirmation per review - Correlate by session key first: when both the deferred write and the confirmation carry a playbackSessionKey, only that is compared, so the same stream URL played in another playlist no longer records a failed attempt. URLs remain the fallback (portal writes, MPV/VLC sessions). The M3U radio player now receives the host's session key. - Count only playing progress: engines report `playing` (not paused, not seeking) with each time update, so short seeks of paused media no longer confirm a view. - Xtream: a write confirmed after a playlist switch still saves to its own playlist but no longer replaces the current playlist's recent list. - Global live tab: a row confirmed after another row was selected still moves to the top of an open Recently Viewed list (only a disposed tab skips the notification). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): confirm session-keyed history only by its own session A write deferred with a playback session key (M3U) is now confirmed only by that key. The app-wide MPV/VLC session confirmation carries just the URL, so opening the same stream externally from another playlist could still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is instead confirmed by the WebPlayerViewComponent that requested it, under its own session key, once the launch has opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): keep the history gate off the initial bundle The `@iptvnator/services` barrel ships in the initial bundle, so adding PlaybackHistoryGate there (and subscribing to it from the app-wide ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes. - Move the gate to a new lazy-only `playback-data-access` project (`@iptvnator/playback/data-access`; scope:shared, domain:playback, type:data-access) and register it in the coverage policy. - The gate subscribes to MPV/VLC session updates itself; it is created by the first deferred write, which precedes the launch it waits for. ExternalPlaybackService is back to master. - The Xtream "playlist switched before confirmation" check moves to the lazy helper; the initial-path store only takes a `skipListRefresh` flag. Net effect on this branch: +27 bytes over master (master itself is 108 bytes over the ratchet baseline already). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): drop late Xtream confirmations off the initial path The Xtream store ships in the initial bundle, so even the small `skipListRefresh` flag cost 27 bytes there. A confirmation can only arrive after a switch to another playlist from a slow MPV/VLC launch (the inline player goes with the page), so the lazy helper now drops it instead: recording it would misfile the item or replace the other playlist's recent list. with-recent-items is back to master. This branch is now 3 bytes below master on renderer.initialBytes; the ratchet still reports master's pre-existing overage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(playback): pass the spec type-check gate from master - playback-data-access: align tsconfig.spec.json with the epg-data-access config #1705 updated (bundler resolution, global.d.ts for window.electron). - M3U recent-history spec: type the selectSignal override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep late Xtream confirmations in their own playlist history A confirmation that arrives after a switch to another playlist (a slow MPV/VLC launch) is no longer dropped: the lazy helper saves it to the captured playlist through the data source, without reloading the store's recent list, which belongs to the other playlist by then. The store and its barrel ship in the initial bundle, so the save path stays in the feature helper; renderer.initialBytes stays under the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): correlate global live-tab history by its session key The unified Favorites/Recent live tab deferred its history write by stream URL only, so the same URL played from another playlist could confirm a failed selection, and a switch to catch-up before confirmation could never match. It now defers with the tab's playlist-scoped playbackSessionKey (the key its players confirm with), and the tab's radio player receives it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep MPV/VLC rows of the live tab confirmable by URL The live tab's session key can only be confirmed by its own inline players; MPV/VLC confirm the launched URL alone. A row that goes to an external player (also later, after a double-click) now defers by URL, and only rows played inline carry the session key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): per-channel M3U history attempts, capability-based Xtream fallback - M3U: the recently-viewed dedupe key now includes the channel id, so a second row of the same URL defers its own write (its session key) and is recorded when it plays after the first row failed. - Xtream late write: key uncached content by Xtream id per supportsXtreamSqliteDataSource (the data-source factory's contract), not by a generic Electron bridge. 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> |
||
|
|
807b5ea259 | docs(website): illustrate feature guides with app screenshots | ||
|
|
650da4a1d3 |
ci(test): type-check Jest spec programs and gate it in CI (#1705)
* build(test): make spec tsconfigs resolve what Jest resolves Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot see Angular's exports-only secondary entry points, and dropped global.d.ts, so tsc reported thousands of resolution errors and no window.electron typing. Switch them to module: preserve with bundler resolution (ts-jest still forces CommonJS emit outside ESM mode), add global.d.ts to every spec program, type jest.unstable_mockModule for the ESM workspace, include the ui-epg and ui-playback specs that jest.web-esm.workspace.ts runs under the web spec config, and drop the snack-bar stub that shadowed the real Material types. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci(test): gate spec type-checking with typecheck:spec Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into the unit-and-typecheck job after typecheck:ci, and document the gate and the spec tsconfig conventions in the validation map. Also bring the non-Tier-A spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to the same conventions so the gate covers the whole workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: fix the spec type errors surfaced by typecheck:spec With the spec programs resolving modules and ambient typings correctly, tsc reported 432 genuine errors across the Tier A projects: read-only capability flags assigned on Partial<> doubles, signal-store values used as types, fixtures missing required fields, index-signature property access, partial bridge doubles cast through incompatible shapes, and deferred resolvers narrowed to never. Type the doubles instead of casting to any: writable mapped types for capability flags, InstanceType<typeof StalkerStore>, typed jest.fn signatures, protectedState: false on test signal stores, and completed fixtures. Production changes are limited to bracket access for index-signature properties under the libs' noPropertyAccessFromIndexSignature setting and two narrowing guards in the global favorites loader. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(playback): use the ESM setup's jest global in the controls fixtures The fixture imported jest from @jest/globals, which is not a direct dependency. Jest provides that module at runtime, so tests passed, but on a clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI. The ESM test setup already installs import.meta.jest as the global, typed by @types/jest, as the other ESM specs use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: type the parental lock doubles merged since the gate was written The parental lock feature (#1601) and the Stalker actor route landed on master with spec doubles declared as zero-argument jest.fn()s that the tests then drive with the real arguments, plus a copy of the ResizableDirective override imported from a library that does not export it. Give the doubles the lock service's real signatures, drop the dead override as in the sibling layout specs, use bracket access for the actor route's personId param, and keep the Stalker layout spec within the 1200-line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |