* 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>
* 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>
* test(performance): skip every test-only file suffix in the zoneless guard
#1813 added serial-details.test-stubs.ts, whose stub components set
ChangeDetectionStrategy.Eager. The zoneless checklist guard listed only
some test-only suffixes, counted the stub file as production code and has
failed the performance-harness job on master since. It now skips every
`.spec` / `.test` file with or without a suffix, test-setup.ts and
test-stubs/ directories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(performance): accept multi-segment test-only suffixes in the zoneless guard
Review follow-up (Greptile): `(-\w+)?` allowed one suffix segment, so a
file such as `rail.test-data-stubs.ts` would be scanned as production.
The suffix now repeats, and a classifier test pins which names are
skipped and which ship.
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>
* docs(portals): document forced MPV/VLC launch and pending-start contracts
The rules #1792 settled for forced external launches and playback-start
bookkeeping lived only in code comments and specs. Record them as
contracts in the owning documents:
- embedded-inline-playback.md: the shared detail-host rules (one external
player per title, unconfirmed teardown cancels, ownership rechecked
after every await, owner-scoped pending state).
- xtream-portal-compatibility.md: the series launch chain, page-token
duplicate guard and queued choice.
- vod-multi-source.md: the movie menu launch and reset follow the copy
the primary button acts on; pending resets are a list of targets.
- stalker-portal.md: the per-series launch queue, the batch-held choice
and the movie launch/reset pending start.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(portals): correct launch-contract claims against the code
- A launching session is published before the launch IPC resolves; what
it lacks until then is an exact closer.
- The series watched/reset batch and the Xtream movie launch gate are
page-wide, not owner-scoped.
- Only the Stalker movie hosts retire a pending start, and that does not
clear the repeat guard of a launch still in flight.
- Name only the specs that exercise the Xtream series rules.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(portals): tighten closer timing and page-wide gate wording
A launching session may already have its closer before the launch IPC
resolves, the Xtream movie launch gate does not cover the inline
player's diagnostic fallback, and the hero-state spec covers only the
external-player and reset rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(portals): fix stale session-timing comment and search guard wording
- serial-details-external-launch.ts: Electron publishes a `launching`
session as soon as the launch IPC arrives, not only after the launch
settles. Comment only; no behaviour change.
- stalker-portal.md: the search host's selection check does not include
the content type; a switch to a series supersedes the launch through
the playback owner key instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(xtream): split the series details page into focused services
serial-details.component.ts sat at the 400-line max-lines limit and its
playback service and spec were close behind. Move cohesive concerns out
without changing behavior:
- route params, the provider-only flag and the (re)load of the addressed
series go to SerialDetailsRouteService; the component still registers
the effect, so effect order is unchanged
- season descriptions, posters and the TMDB season enrichment go to
SerialDetailsSeasonsService
- actor, Similar and Discover navigation go to injectXtreamDetailNavigation,
shared with the movie page
- the watched toggles and the episode playback payload leave
SerialDetailsPlaybackService for SerialDetailsWatchToggles and
buildSerialEpisodePlayback
- the spec's TestBed moves to a harness and the watched-toggle cases to
serial-details.season-watch.spec.ts
Counted lines: component 400 -> 318, playback service 388 -> 342,
component spec 1196 -> 674.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(xtream): move VOD route selection and position state out of the page
vod-details-route.component.ts grew from 618 to 741 counted lines and
vod-details-playback.service.ts reached 395. Extract without changing
behavior:
- the route-derived read model (selected movie, category, catalog item,
fallback view, multi-source identity, session and content keys) goes to
VodDetailsSelectionService; the component keeps the same member names
- trailer state and the Similar-rail click move into the hero presenter,
the cancel-download prompt and the progress-ring geometry into the
downloads service, navigation into injectXtreamDetailNavigation
- stored positions (last seen, route row, guarded load) become
VodDetailsPositionState and the external-launch bookkeeping becomes
VodExternalLaunchClaim
- drop the unused MatTooltip import (NG8113) and eight unused imports
Counted lines: route component 741 -> 492, playback service 395 -> 344.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(stalker): extract series-view positions and watched toggles
stalker-series-view.component.ts grew from 1601 to 1813 counted lines and
its spec reached 1198 of 1200. Move code out verbatim:
- saved positions, their reconcile and the persist/clear writes go to the
component-provided StalkerSeriesPositionsService; the ordering of reads
and writes goes to StalkerSeriesPositionQueue
- episode, season and series watched toggles go to
StalkerSeriesWatchToggleService, the batch core to
runStalkerWatchToggleBatch
- the lazy VOD season loads go to StalkerVodSeasonEpisodeLoader
Every effect stays registered in the component constructor in its original
order; template bindings and public member names are unchanged.
The spec setup moves to a shared harness and the spillover-prefetch and
TMDB season cases to their own specs; the 184 cases of the directory are
unchanged.
Counted lines: component 1813 -> 1136, component spec 1198 -> 670.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(stalker): extract catalog-detail position and search paging
- stalker-catalog-detail.component.ts (397): the stored VOD position and
its runtime updates become StalkerCatalogVodPosition, the Play/Resume
start becomes startStalkerCatalogVodPlayback
- stalker-search.component.ts (776, baselined): the paging resource, the
accumulated results and the parental-lock bookkeeping become
StalkerSearchPagingController, with pure helpers in
stalker-search-results.util.ts. The spec reaches the moved members
through component.paging; its cases and assertions are unchanged.
Counted lines: catalog detail 397 -> 317, search 776 -> 482.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(ui): give season-container and vod-details real line headroom
- season-container.component.ts (396): season auto-selection and the
seasonSelected emission move to createSeasonAutoSelectState, called at
the same place in the constructor so effect order is unchanged; the
episode subline becomes buildEpisodeSubline
- vod-details.component.ts (393): the cross-portal Similar loader, the
provider-only download state and the actor/Similar route helpers move to
sibling modules
Inputs, outputs, selectors and public members are unchanged.
Counted lines: season container 396 -> 341, vod details 393 -> 337.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test(web-e2e): move Stalker portal helpers into fixtures
stalker.e2e.ts was at 1196 of 1200 counted lines. Move the mock endpoints,
scenario MACs and page helpers to stalker-portal.fixture.ts and the
embedded-series steps three tests repeated to
stalker-embedded-series.fixture.ts. Every test stays in stalker.e2e.ts in
the same order, with the same serial mode and OWNED_MACS reset.
Counted lines: 1196 -> 981.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: follow the detail-page extractions
Name the Stalker watched-toggle and position services that now own the
batch and reconcile code, point AUTH_REJECTED_MAC and the scenario MACs at
stalker-portal.fixture.ts, and drop two stale statements about components
sitting at the max-lines cap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(xtream): note why the seasons service is injected last
Its TMDB enrichment effect keeps the position it had as the first effect
of the component constructor only while it is created after the other
services' effects.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
UI-24 from the UI consistency audit.
- No-artwork slides paint their gradient in CSS from the slide hue: a light
tint in the light theme, unchanged near-black in the dark one. The dark
gradient under the light page-coloured scrim read as a grey slab.
- The side scrim holds 88% of the page colour up to the slide's right edge
(inset + min(560px, 55%)), so the end of a full slide no longer sits on
about 45%.
- Narrow layout (container <= 720px): a full-bleed 90% scrim behind the text
block, a scrim-coloured text shadow, and an entrance without a fade so
that scrim never flashes the art on a rotation.
- --hero-body is 85% of the heading colour (was 72%).
- Light --app-rating-color #a16207 -> #7a4a00: measured 3.36:1 on the chip
over artwork, now 5.10:1. The details pages share the chip and token.
- Buttons cap at the slide width and end long labels in an ellipsis.
- The page gets one visually hidden h1 ("Dashboard"); slide titles are h2.
- One live region outside the re-created slide announces slide changes;
progress bars are named and VOD ones read "N% watched"; dots are 24px.
dashboard-hero-legibility.e2e.ts replaces every image with a checkerboard
and measures each piece of slide text from the screen in both themes, wide
and narrow, for backdrop, poster, no-artwork and live slides. On master the
worst cases were 2.35:1 (body text) and 2.65:1 (pills).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(performance): inventory the zoneless change-detection migration
Plan item C6 step 2. docs/architecture/zoneless-migration.md lists the 66
production files (67 components) that still set
ChangeDetectionStrategy.Eager, the ten places where a template-read plain
field is written outside an Angular event, the NgZone and
ChangeDetectorRef calls to remove at the flip, and the IPC, player,
observer, timer and dialog paths checked as signal-safe.
On Angular 22 an unset changeDetection already means OnPush, so only the
explicit Eager components re-render on every tick.
zoneless-migration.spec.ts in the performance harness compares the
checklist with the code: a new Eager component, or a converted one left
unticked, fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(ui): make the libs/ui Eager components OnPush
Plan item C6 step 3 for libs/ui/components and libs/ui/epg: eleven
components (twelve with the EPG trust dialog) set
ChangeDetectionStrategy.Eager and were checked on every tick, among them
the always-mounted EPG progress panel the idle audit found re-rendering
on every idle tick. Their template state is already signals, signal
inputs, immutable dialog data or fields written from template events, so
they switch to OnPush without other changes.
The epg-item-description spec mutated dialog data after creation and
marked only the fixture's host view; it now marks the component's own
view, which OnPush requires. The libs/ui playback and remote-control
components stay Eager for their own PRs.
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>
* fix(playback): keep the saved Embedded MPV player when the mpv check is inconclusive
On Linux native-view the support check runs `mpv --version` by bare name
and waits for the login shell PATH first. Since #1784 that lookup is
asynchronous with a 10 s budget; when it ran out, the check ran on the
inherited PATH and answered a plain `supported: false`. The settings store
took that as a verdict and persisted the default player over a saved
Embedded MPV selection. The main process probed again once the shell
answered, but nothing restored the setting.
`EmbeddedMpvSupport` now carries `inconclusive`. The native service sets it
on a missing mpv while its probe has only seen the inherited PATH; the IPC
handler declares that state before probing and registers the re-probe
before the check, so a throwing check cannot leave it stuck. Every other
answer stays final.
Consumers no longer settle on an inconclusive answer: the settings store
keeps the saved player, and the command palette and the settings search
probe again on their next use instead of caching it for the session.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(playback): keep asking for Embedded MPV support while the answer is inconclusive
Keeping the saved player on an inconclusive answer left a mounted player
stuck on it: the session controller asked for support once, in its
constructor, and the session effect never starts while unsupported, so the
player did not recover after the login shell answered. The settings page
held its one answer the same way.
`watchEmbeddedMpvSupport()` asks again every 3 s until the answer is final
or the surface is destroyed. The player controller and the settings page
facade load support through it, so playback starts by itself and the
Embedded MPV option appears without reopening the page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings): follow an inconclusive Embedded MPV answer to a final decision
The settings store checked a saved Embedded MPV selection once. After an
inconclusive answer it kept the selection and never looked again, so when
mpv turned out to be really missing the player stayed on Embedded MPV
instead of falling back to the default one.
The store now follows the answer with `watchEmbeddedMpvSupport()` until it
is final and only then decides. It acts on an answer only while Embedded
MPV is still the saved player, so a player picked meanwhile, also while
the first answer was pending, is never overwritten.
The watch backs off from 3 s to 30 s between rechecks, so a login shell
that never answers does not keep the app polling at the first rate, and it
no longer schedules a recheck after its answer handler stopped it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings): keep the settings search following an inconclusive Embedded MPV answer
The settings page asks the search service for Embedded MPV support once,
when its search facade is created. After an inconclusive answer the service
only probed again on its next call, so with the page left open the
Embedded MPV rows stayed unsearchable after the login shell answered,
while the player option on the same page already updated.
The service now follows the answer with `watchEmbeddedMpvSupport()` until
it is final, which updates the open page and the command palette alike. A
call made while the answer is still inconclusive restarts the watch, so it
asks at once as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings): follow Embedded MPV support for search only while the settings page is open
The settings search service is provided in the root injector, so the
watch it started on its first use had no owner: with a login shell that
never answers it kept asking every 30 s until the app quit, long after
the settings page or the command palette that needed the answer was
closed. A failed recheck also ended the watch as if it were a final
answer, hiding the Embedded MPV rows for the rest of the session.
The service now separates the two uses. `ensureEmbeddedMpvSupportLoaded()`
is a single request again, for the command palette. The settings page
calls `followEmbeddedMpvSupport()` and ends it when the page is destroyed.
Only a final answer is kept: after an inconclusive one or a failed
request the next use asks again, for the palette's player commands too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
Every push to a pull-request branch starts the CI matrix and both review
bots, and runs from several open pull requests queue behind one another.
Move the fix rounds off GitHub: a branch is reviewed with the Codex and
Greptile CLIs until both are clean, then pushed once.
- AGENTS.md: the rule, linked to the procedure
- agent-workflow.md: commands, loop, stop conditions and exemptions
- agent-context-map.md: route the topic to the workflow document
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* 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>
* 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>
#1773 added `ambiguousTimelineCompletions` and `timeline` to
`JourneyMainIpcCaptureState`, while #1774 merged the J3 playback record
spec with a fixture of the old shape, so the spec no longer type-checks
(TS2739). The harness runs it through tsx without type checking, so CI
stayed green.
The timeline holds one start per counted call, matching `callsByMethod`
and `callsBeforeSentinel`, so the fixture stays a capture a real run
could produce.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(electron): look up the login shell PATH without blocking the main thread
fix-path ran $SHELL -ilc env synchronously right after the first load.
With a typical zsh profile that held the main thread for 1-2 s, while the
database worker's ready message and the renderer's first IPC calls waited,
so the launch journey's first card came that much later. Use shell-path's
async shellPath() with fix-path's fallback, so the resulting PATH is the
same and the main thread stays free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(perf): name the PR that made the login shell PATH lookup async
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): let bare-name player spawns wait for the login shell PATH
With the lookup now asynchronous, an external player launched (or the
Linux embedded MPV support check, which runs and caches a bare
`mpv --version`) within the first seconds could see the inherited PATH.
The OPEN_MPV_PLAYER / OPEN_VLC_PLAYER handlers and every embedded MPV
handler now await waitForLoginShellPath() (settled lookup, at most 10 s,
immediate on Windows), restoring the guarantee the blocking lookup gave.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): wait for the login shell PATH only for bare-name spawns
External players wait only when they resolve to a bare name (no
configured path, no well-known install found); a path to an executable
starts at once. Embedded MPV waits only on Linux and only for support and
prepare, which run the cached bare-name `mpv --version` check; sessions
and controls never wait.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): wait for the login shell PATH only before the mpv probe
Embedded MPV support and prepare waited on every Linux call, although
getSupport() returns before the bare-name `mpv --version` probe for the
frame-copy engine, native Wayland, a disabled feature or a cached result.
willProbeLinuxMpvExecutable() now gates the wait on the probe actually
running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): stop waiting for a login shell that already timed out once
After the first wait for a hung `$SHELL -ilc env` runs out, later
bare-name player launches and Linux mpv probes proceed at once instead
of each waiting the full limit again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): bound login shell PATH waits by the lookup's own budget
Replace the latch on the first expired wait with a deadline set when the
lookup starts (10 s). Every wait ends when the lookup settles or the
deadline passes: a launch retried while the shell is still within its
budget waits for the PATH again, and once the budget is spent no launch
waits, so a hung shell still delays at most the first seconds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): re-probe a missing mpv once a late login shell answers
When the PATH lookup runs out of budget, the Linux embedded MPV support
check probes `mpv --version` with the inherited PATH and caches a
missing result for the rest of the session. waitForLoginShellPath() now
reports whether the lookup settled; after a timed-out wait the handler
forgets a cached "missing" once the lookup finishes, so the next support
check probes again with the login shell PATH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): share one deadline among waits before the PATH lookup starts
A bare-name launch that waited before the lookup was scheduled started
its own 10 s limit, so while startup was stuck every retry paid the full
delay again. The first early wait now sets the shared deadline; the
lookup still replaces it with its own budget when it starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): re-probe mpv after a late login shell PATH either way
A Linux mpv probe that ran on the inherited PATH can be wrong in both
directions: the login shell PATH may add mpv or drop the directory the
inherited one found it in. forgetLinuxMpvExecutableProbe() now clears a
found result as well as a missing one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(electron): skip the login shell PATH wait for Flatpak host launches
In Flatpak, players start through `flatpak-spawn --host`, which resolves
the name with the host's PATH; the sandbox's login shell lookup cannot
change it. The launch handlers now decide from the same launch context
the player uses: no wait in flatpak-host mode, otherwise only for a bare
player name.
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>
* fix(dashboard): scroll a focused rail card fully into view
Chromium skips its focus scroll when 32px or more of the element already
shows, so Tab onto the last source card of a rail that overflows by less
than a card left it half-hidden under the edge fade. The rail track now
handles focusin and scrolls to the first card-start snap position that
reveals the whole card; a plain "nearest" scroll is not enough because
mandatory snapping can round it back (seen on the live channel rail).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dashboard): keep mouse clicks on partly hidden rail cards
A mouse press focuses the card link on mousedown. Revealing the card at
that moment could slide it from under the pointer when the target snap
position overshoots (the live channel rail moves 316px for a 306px
card), so the click landed elsewhere. The rail now reveals a card only
for keyboard and programmatic focus, using the CDK FocusMonitor origin.
Adds an Electron E2E that checks the final layout after snapping: Tab and
focus() leave the last source card fully visible, and a mouse press keeps
the rail still and still opens the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dashboard): tell pointer focus apart without touching the DOM
FocusMonitor toggles cdk-*-focused classes on the monitored track, so a
mouse press on a source card mutated the DOM before the click. The J2
"open a source" performance journey rejects iterations with DOM activity
between its settle snapshot and the click. Read the input modality from
the CDK InputModalityDetector in a focusin handler instead: it only
listens, so the rail stays untouched until the click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dashboard): reveal script-focused rail cards after a mouse click
The input modality stays "mouse" after any click, so a later focus() on
a partly hidden card left it clipped. The rail now skips the reveal only
for focus caused by a press inside the track: the focus has to arrive
within 100ms of that pointerdown (650ms for touch, whose focus comes
with the tap's compatibility mouse events, as in the CDK FocusMonitor).
Only event timestamps are compared, so the DOM still stays untouched
before the click.
The E2E now clicks elsewhere before the script focus, and unit tests
cover a tap and focus() after an earlier mouse press.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dashboard): keep an over-wide focused rail card in view
In a window narrower than a card (or under zoom), a focused card could
never fit, so its own snap offset fell short of the needed scroll and
the rail jumped to the next card's snap point, moving the focused card
offscreen. Such a card is now aligned at its own start instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(dashboard): select rail internals through stable test ids
The dashboard contract makes data-test-id hooks the supported Electron
E2E selector surface. The rail now exposes -viewport, -track and
-card-link hooks next to its existing ones, and the focus E2E selects
those (and the rail heading by role) instead of internal class names.
The dashboard doc lists the new hooks and records the focus-reveal
contract.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): destructive confirmations, verb labels and provider icons
Confirmations: ConfirmDialogData.confirmLabel is required, so no dialog can
fall back to "Yes"/"No"; the dismiss defaults to "Cancel" and
`tone: 'destructive'` styles the confirm with .app-destructive-button. Every
caller names its action ("Remove playlist", "Clear", "Refresh playlist",
"Cancel download" with a "Close" dismiss). The confirm button has the
confirm-dialog-confirm test id and drops its no-op color="primary".
The no-op `warn` color input becomes .app-destructive-button on the EPG
mapping, playlist item, error view, EPG/reset settings, delete-all and source
cleanup buttons, and on the unsaved-changes dialog's Discard.
Provider icons come from SOURCE_TYPE_ICONS in shared/interfaces (Xtream
cloud, Stalker cast, M3U playlist_play / link / description / subject) in the
add dialog, auto-import, empty state, playlist switcher, playlist rows,
dashboard source rail, command palette, Sources filters and both reset
summaries. Stalker no longer borrows the Dashboard icon, and Xtream no longer
shares a glyph with M3U URL playlists.
The playlist error view removed a playlist through the stale
PlaylistActions.removePlaylist: it dropped the playlist from state before the
delete ran, swallowed failures, skipped the source activity guard and showed
no toast. It now uses PlaylistDeleteActionService like every other removal,
commits only a completed delete, toasts and goes home. The unused action and
its effect are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): one provider icon per playlist row, imperative Korean remove label
A restored Stalker or Xtream playlist can also carry a URL, and the row's
independent checks then showed the M3U URL icon next to the provider icon.
The row now switches on resolvePlaylistSourceIconKey(), the precedence every
other surface uses, so each source shows exactly one icon.
HOME.PLAYLISTS.REMOVE now names the confirm button and the row's delete
tooltip; in Korean it read "the playlist has been removed". It now says
"remove playlist", like every other locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep the auto-refresh badge on playlist rows with one provider icon
Showing one provider icon per row moved the auto-refresh badge into the M3U
branches only, so a restored Stalker playlist with a URL and auto-refresh
lost it although the URL is still re-fetched. The row now renders one icon
container: the provider icon from the shared precedence, then the badge for
any row with a URL or a local M3U, exactly the rows that showed it before.
The Xtream portal-status dot, used without source health, keeps that corner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): let the playlist row's cancel action render in the error color
The row's action buttons set `color: inherit`, and the selected row does so
again with more specific selectors. Both beat Material's token-driven icon
color, so the .app-destructive-button cancel action kept the row color
(selection blue on the active row). Pin the cancel button to
--mat-sys-error in both row states.
The large-deletion Electron E2E now checks the cancel color in both themes;
without this rule it reads rgb(47, 123, 255) instead of the error red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(ui): give the dialog service spec the now-required confirm labels
ConfirmDialogData.confirmLabel became required, and the spec still built
confirmations without one. Jest only transpiles, so the suite stayed green,
but the "Typecheck Jest spec programs" CI step rejected it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): count change-detection ticks in the electron-performance build
J1 renderer.cdTicksToFirstCard, J2 renderer.cdTicksToFirstPage and the
J1 idle baseline renderer.cdTicksIdle30s. Angular's ɵsetProfiler is only
reachable through the dev-mode window.ng global, so the electron-performance
configuration alone swaps environment.ts for environment.performance.ts,
which re-exports the production AppConfig and wraps ApplicationRef._tick.
Production and PWA sources and output are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): refuse a J1 idle window that opened late after the settle point
Addresses review: the idle window opens in the settle timer's callback while
the settle point is that timer's deadline, so a late callback left ticks
uncounted between the two.
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>
* test(perf): measure the serial IPC depth before the J1 first card
Adds renderer.ipcSerialDepthToFirstCard to the launch journey: the length
of the longest chain of bridge calls in which each call started after the
previous one completed, among calls that completed before the first card.
The main IPC capture now records the ordered start/completion timeline;
the depth, its lower bound, the chain and the timeline are per-iteration
evidence, and the CI job summary prints the chain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): keep the IPC timeline consistent around the J2 start marker
A call that started before the start marker no longer records its
completion in the timeline, and completions of a method with calls in
flight both inside and outside the timeline are attributed outside and
counted, instead of skipping the first marker-method completion.
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>
* test(perf): add the J3 playback journey
J3 clicks a live channel of an Xtream portal and ends at the built-in
HTML5 player's first `playing` event, with `loadedmetadata` as a
secondary phase. It follows J2: every iteration is a fresh J1 launch on
a copy of a profile seeded through the app's dialogs, and the click
happens after the app has settled in the portal's first live category.
The portal is the mock's `live-fallback` account, whose `.ts` live URLs
serve the local H.264/AAC MPEG-TS fixture that mpegts.js plays through
MSE on every platform. The marketing accounts' local live bytes are
zero-filled and never reach `playing`. Seeding selects the HTML5 player
and the `ts` stream format; the catalog's picsum.photos logos are
cancelled from the test side so no request leaves the machine.
Counters: renderer.ipcCallsToPlaying, renderer.httpRequestsToPlaying,
renderer.domMutationsToPlaying, renderer.layoutShiftScore and
renderer.longTasks; wall-clock click->loadedmetadata and click->playing.
renderer.ipcSerialDepthToPlaying is listed as unavailable until the
serial-depth helper lands. No baseline yet.
The probe gains a media-event terminal; J2's pre-click settle moves to
journey-click-settle.ts so both journeys share it unchanged, and the
probe spec's jsdom fixtures move to a shared test helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): show J3's HTTP boundary margins and watch 1 s after playing
Review follow-up. renderer.httpRequestsToPlaying compares the ledger's
arrival stamps with the renderer's click and playing stamps, which come
from different processes on the same host clock. Each iteration now
records the distance of the nearest request on either side of both
boundaries, so a count a clock difference could flip is visible.
Requests after playing were a single snapshot taken right after the
probe; the test now watches the ledger for a fixed 1 s after playing.
A live stream never leaves the mock quiet, so J2's quiet wait does not
apply.
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>
* fix(player): keyboard focus, contrast and ARIA for controls and settings
Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.
Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.
The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".
Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): arrows check settings radios; subtitle chip reads On
Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
native radio group does (the directive clicks it, so the template's
handler applies the choice); an option the engine already reports as
checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
report the switch before the track list), the subtitle chip reads and
announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
is not clipped at the scroll edge of the panel body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): re-apply a settings radio while a switch is pending
The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(e2e): let per-file E2E targets run without mock serve dependencies
Since #1710 the Playwright configs start the Stalker and Xtream mocks
themselves (`node --import tsx …`), so @nx/playwright can no longer map
those webServers to Nx tasks and infers the atomized `e2e-ci--*` targets
as non-parallel. The `e2e-ci--src/*.e2e.ts` target default still made
them depend on the continuous `stalker-mock-server:serve` and
`xtream-mock-server:serve` targets, and Nx refuses to run a
non-parallel task with continuous dependencies, so every per-file
target failed before running anything.
Drop the redundant mock dependencies and keep the Electron build.
The mock-launch guard spec now also rejects any nx.json target default
that depends on a mock-server task.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(e2e): scope the mock dependency guard to E2E targets
Check only the `e2e*` target defaults in nx.json, so an unrelated
default may still depend on a mock, and also check every target in the
`apps/*-e2e` project.json files, where a mock `serve` dependency would
break the same targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(e2e): build Electron only before Electron per-file E2E targets
The `e2e-ci--src/*.e2e.ts` target default also matched web-e2e, so
every browser-only per-file target built electron-backend first. Split
it into project-filtered entries: Electron targets keep `build-e2e`,
web targets get an empty dependency list (which also keeps the
inferred `web:serve` dependency, rejected by Nx on a non-parallel
target, out of them).
The mock dependency guard now also catches `^serve`-style
dependencies, which schedule the mocks through the E2E projects'
implicit dependencies.
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>
* fix(portals): treat an undetermined audio language as unknown
ICU 78, which Electron 43 and Node 26 ship, canonicalizes the ffprobe
marker `und` to the subtag `und` instead of an empty one. The source
metadata then recorded it as a stated language, so a switch between a
copy tagged `und` and one tagged English raised the "dub may differ"
warning. CI's older ICU hid this: the existing spec only fails on the
newer runtime.
Decline `und` explicitly, and cover `und-US` plus the dub comparison.
* test(portals): cover undetermined language across runtimes
---------
Co-authored-by: 4gray <serega05@gmail.com>
* fix(playback): show the Up next card near the real end of an episode
The card appeared a fixed 8 minutes before the end: most of a short
episode, and far into the story of a long one, with no way to hide it.
- Adaptive lead: 4% of the episode, clamped to 40 s … 3 min.
- A closing-credits chapter in the last third, when timeline segments
carry one, brings the card forward to its start (capped at 5 min).
- Close button and Escape dismiss the card for the current next episode.
- After 10 s (not while hovered) the card collapses into a one-line pill.
- Seconds countdown in the last minute.
- New playback setting "Up next card" (default on) turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): address Up next card review feedback
- Offer the Up next card setting for Embedded MPV only under the
frame-copy engine; native view never mounts the shared controls.
- Hovering pauses the collapse delay instead of restarting it.
- Document that the card stays visible when the controls auto-hide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): restart the Up next collapse delay for a new episode
- A card that stays mounted while its next episode changes gives the new
item the full delay instead of the previous item's leftover.
- The setting description no longer promises credit-based timing: no
current series path supplies chapters yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep dialog action rows on one line
Settings "Unsaved changes" dialog:
- Cancel / Discard / Save replace the phrase labels in all 19 locales, and
the dismiss now comes first; the shared CANCEL key replaces the unused
UNSAVED_DIALOG_STAY.
- At the 640px phone breakpoint the actions stack one per row, full width,
in DOM order.
EPG programme dialog:
- mat-dialog-title gives the dialog an accessible name.
- The footer Close is the only dismiss; it comes first and the primary
action last.
- The archive copy/download tools move under their notice, so the footer
stays on one row.
- Channel rows now open it through EpgProgrammeDialogService, which owns
the 540px config and a panel class scoping the surface overrides.
Adds a web-e2e layout spec (en, de, ru, fr, hu, ar on one row; de and ru
stacked on a phone), extends the Electron EPG spec to all three openers,
and documents the dialog contract in the UI guidelines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(epg): stack programme dialog actions on phones
Below the 640px phone breakpoint the viewport caps the programme dialog,
and a long translated primary label ("Regarder depuis le début") no
longer fit beside Close. The footer had no wrap, and the dialog hides
overflow, so the label was clipped.
- At the phone breakpoint, the archive tools and the footer now stack one
full-width button per row, in DOM order.
- Buttons grow to fit their label, so a long label wraps inside its
button instead of being clipped.
- On desktop the footer can wrap again as a last resort.
The new Electron test opens a past programme in French at a 360px
viewport and measures both rows. It fails against the previous
stylesheet (the footer buttons are 44px narrower than the row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(playback): give the fullscreen side panel the settings panel's glass look
The fullscreen channel/episode panel was a full-height graphite strip while
the controls' settings panel is an inset, rounded glass card. The side panel
now wears the same surface: 16px inset (8px under 560px), 20px corners, the
controls' glass fill, hairline border, blur and shadow, the same open motion
and a 32px square close button. The edge hint uses the same glass, and the
episode list marks the playing row with the settings panel's selected cyan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): dismiss the side panel from the exposed edge gutter
The inset card leaves the hot zone's left strip visible beside it, above the
scrim, so a click there re-ran show() instead of dismissing. A completed
primary press in the zone now closes an open panel; hovering there still
counts as inside, since a hover-opened panel leaves the pointer resting in
that strip. The playing episode row also keeps its cyan tint on hover/focus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): decide an edge press from the panel state at pointerdown
A press held on the edge past the hover dwell opened the panel before the
release, which then read the open state and dismissed it. The press now
remembers whether it began on an open panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(dashboard): fill the hero rotation dot on the compositor
The active hero dot animated `width` 0 → 18px for every 8 s rotation, so
an idle dashboard with two or more slides ran style, layout and paint on
every frame. The fill is now a full-width bar that slides in with
`transform` under the pill's rounded clip. The `animationend` advance,
the pause and reduced-motion behaviour are unchanged.
Measured on the E2E build (visible, four slides, 120 s): layouts
10,405-10,677 -> 366-369, renderer process CPU 14.2-16.4 s -> 3.7-4.1 s,
GPU process CPU 14.8-18.8 s -> 14.0-14.8 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(e2e): drive the real hero rotation animation
The unit specs dispatch `animationend` on the dot span by hand, so a fill
whose real event no longer reached the handler would still pass. The new
Electron spec shortens `--hero-rotation-ms` and checks that the running
`::before` fill advances the slide, that pause holds it and that Play
resumes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): translate parental lock strings in 16 locales
The parental lock feature (#1601) added 57 keys that only de and ru
translated; ar, ary, by, el, es, fr, hu, it, ja, ko, nl, pl, pt, tr, zh
and zhtw still showed the English text. Translate them using each
locale's existing terms for categories, groups, sources and settings,
and quote each locale's own "Manage categories" / "Manage groups"
labels in the how-to text.
nl WORKSPACE.DASHBOARD.HERO_DETAILS stays "Details": it is correct
Dutch and belongs to the dashboard hero, not the parental lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(i18n): use gender-neutral locked counts
LOCKED_COUNT and LOCKED_CATEGORIES_ROW count both categories and M3U
groups. The generic masculine plural in es, fr, it and pt read wrong for
(feminine) categories, so count the locks with a noun instead. el now
uses the feminine plural that fits both nouns.
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>
* test(perf): add a settled layout-shift counter to the J1 launch journey
renderer.layoutShiftScore stops at the first-card cutoff, so the dashboard
shift fixed in #1738 (about 0.23, roughly 15 ms after the first card) read
as 0. renderer.layoutShiftScoreSettled sums the same non-input layout-shift
entries until the workspace content has been quiet for 500 ms, capped at
3 s after the cutoff. The first-card counter is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): attribute late layout shifts and record the first measurement
evidence.settle.lateShifts lists the counted shifts after the first-card
cutoff with the nodes the browser attributes them to. On master the settled
score is 0.236: the recent-sources rail moves up 316 px about 12 ms after
the first card and back down shortly after, a flicker #1738 did not cover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): end the settle window at its scheduled deadline
A cap or quiet timer delayed by a busy main thread used the moment it ran
as the settle point, so shifts after the 3 s cap could enter the settled
counter. The settle point is now the deadline the timer was scheduled for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(perf): record the runner's settled layout-shift measurement
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>