* 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>
* 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>
* perf(electron): let hidden and minimized windows report themselves hidden
The main window was created with backgroundThrottling: false (since #1123,
without a stated reason). Electron then keeps document.visibilityState at
"visible" for a hidden, minimized or fully covered window and never lets
Chromium throttle it, so every renderer timer, rAF and CSS transition ran at
full rate in the background, and the playback keep-awake gate, which
releases the display for a minimized window, could never see one.
Use Chromium's default. Audible media and picture-in-picture are exempt
from background throttling in Chromium, and a local check confirmed HLS
playback continues unchanged through more than six minutes minimized,
audible and muted.
Playwright's focus emulation pins every page it attaches to as visible, so
the new window-visibility E2E launches the app without Playwright and
drives it over raw CDP (electron-unautomated-launch.ts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(e2e): harden the unautomated Electron launch
Review follow-ups for the window-visibility E2E:
- Resolve `electron` in the main process through a require created from
the `node:module` builtin instead of `process.mainModule`, which only
exists when the app entry is CommonJS.
- Bound teardown like closeElectronApplicationAndConfirmExit: SIGTERM,
then SIGKILL, 5 s each, then fail instead of waiting forever.
- Surface CDP protocol errors from Runtime.evaluate instead of returning
undefined.
- Wait until the window is actually shown before hiding it. The app shows
its window on ready-to-show, and a hide() that lands earlier is undone
by that show(); this was the first-attempt failure on the macOS shard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(e2e): check the playback display lock is released while hidden
Review follow-ups for #1724:
- Add an E2E that plays the webm fixture in the unautomated launch,
records the main process's prevent-display-sleep blockers, and asserts
the keep-awake lock is taken while visible, released when the window is
hidden, and taken again when it is shown. The visibility tests alone
would still pass if the renderer gate or the bridge stopped updating
powerSaveBlocker.
- Validate CDP replies before dispatch (CodeQL
js/unvalidated-dynamic-method-call): only a numeric id with a pending
settle function is called.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(e2e): skip killing an Electron that already exited
stopElectron now checks the recorded exit state before each signal and
tolerates a kill that races the exit: on Windows taskkill throws for a PID
that no longer exists. Startup cleanup can no longer replace the startup
error that explains the failure; a cleanup failure there is logged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(stalker): keep the watchdog cadence while the window is hidden
With background throttling on, Chromium wakes a hidden, silent page's
timers at most once per minute after five minutes, so a portal that asks
for get_events every 30 s would see pings at half its cadence while the
window is minimized. Tick the watchdog from a dedicated worker
(createBackgroundInterval, an inline blob worker allowed by the renderer
CSP), whose timers are not subject to page throttling. It falls back to a
page setInterval where no worker is available or the worker fails to load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(stalker): keep a stopped watchdog interval stopped
A worker error that arrived after stop() started the page fallback
interval, which nothing cleared, so pings continued for an inactive
playlist. stop() now marks the interval stopped, detaches the worker
handlers, and the fallback refuses to start afterwards.
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>
* perf(dashboard): slide live EPG progress fills with a compositor transform
The live-programme bars on channel rail cards and the hero animated their
width over 0.4 s whenever the 30 s live-EPG tick moved them. Width is a
layout property, so each tick re-laid out the whole document for about 24
frames, also while the window was minimized. Slide a full-width fill with
translateX driven by a --live-progress custom property instead; Chromium
runs that transition on the compositor. Reduced motion drops it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(dashboard): format the live progress spec
* perf(dashboard): stop animating the live EPG progress fills
A live-EPG tick moves the bar by under one percent, so the 0.4 s transform
transition was invisible but still produced a burst of compositor frames
on every tick. Place the fill without a transition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(dashboard): pin the hero progress fill to its custom property
The hero moved into DashboardHeroComponent (#1738); its progress bar gets
the same transform fill as the rail cards.
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(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>
* test(perf): add the J2 open-source journey
Measure the click on the Xtream portal card until the category list and
the first page of the opened section are painted, in the same fresh
process as J1 after its counters are final and the app has settled.
- journey-renderer-probe: optional click start (capture-phase listener on
window, start sentinel before the app sees the click, entries before the
click dropped), companion selectors, recent-input layout shifts tallied
- journey-main-ipc-capture: optional start sentinel; counts calls between
the two sentinels
- journey-mock-request-ledger: loopback proxy that counts every request
the app sends to the mock without storing credentials
- open-source-journey-record: J2 counters and evidence
- journey-run / journey-summary: every journey spec of one perf:journeys
run adds its entry to the same summary.json
- docs: J2 contract in performance-journeys.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): stop echoing the request URL from the ledger spec's upstream
CodeQL flagged the fake upstream as reflected XSS. It now records what it
received server-side and answers with a fixed text/plain body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): address J2 review findings
- Sentinels use cancelSourceProbe: the preload traces the call before
forwarding it and SOURCE_HEALTH_CANCEL is an in-memory map lookup, so a
marker no longer runs a SQLite query on the worker ahead of the measured
work (Codex P1). A spec pins that handler contract.
- A run is started only in the Playwright runner, replacing inherited
values, and carries a random harness.runId; summaries from another
invocation are never merged (Greptile P1, Codex P2).
- The mock ledger tracks in-flight requests; settling and the HTTP window
require none in flight (Codex P2).
- clickToFirstPagePaintMs reports click to the committed paint next to
the terminal-batch clickToFirstPageMs (Codex P1).
- The Playwright attachment carries the whole summary (Greptile P2).
- jsdom probe specs wait for the post-paint cutoff instead of a fixed
40 ms, which flaked when the harness runs all files in parallel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(validation): describe perf:journeys as running J1 and J2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): settle J2 on pending bridge calls and start HTTP at the click
- The journey IPC capture pairs every traced start with its success or
error and exposes the calls still in flight. J2 settles only when J1's
capture, installed before the document loaded, has none pending, so a
slow startup call cannot resolve after the click and count as J2.
- The mock HTTP window starts at the renderer's click stamp instead of
the test-side mark taken before Playwright's actionability checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): restart the J2 quiet period when pending work completes
Both waits in the open-source journey (settling before the click, closing
the mock window after the terminal) now use one waitForJourneyQuiet
helper that compares whole samples, in-flight counts included. The poll
that first sees a request or bridge call complete restarts the quiet
period, so the window is never measured from a poll at which work was
still pending. A fake-clock spec covers the in-flight to zero case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): align J2 with the main-process counters from #1715
After rebasing on #1715, J1 measures main.sqlStatementsBeforeReadyToShow,
so J2's reason for listing main.sqlStatementsToFirstPage as unavailable
(no countable channel) was stale. State the actual limit: the running
total is read from the test process and cannot be bounded at the click
or the first-page batch. The performance-journeys CI job comment now
names both journeys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): launch J2 without SQL counting and stamp mock requests in sub-ms
- runLaunchJourney takes the launch instrumentation; only J1 turns on the
main-process counters and IPTVNATOR_PERF_COUNT_SQL, so J2's click is not
measured under the hook that wraps every SQLite statement. The flags are
built in journey-launch-environment.ts, which the SQL opt-in guard now
expects, and a launch record without main counters is rejected.
- The mock ledger stamps arrivals with performance.timeOrigin +
performance.now(), the same sub-millisecond epoch as the renderer's
click, so a request later in the click's millisecond is not counted
before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): reject J2 iterations with activity after settling
- The open-source record compares the settle snapshot with what the probe
and the IPC capture counted up to the click event, and with the mock
requests between the snapshot and the click stamp. Any change means
background work began during Playwright's actionability checks and
could land in J2, so the iteration is rejected.
- The SQL opt-in guard also checks who passes mainCounters: true: only
measureLaunchJourney may, and J2 must pass false.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): count the long task that dispatches the J2 click
A long task's startTime precedes the click event's timestamp when the
listener runs inside it, so the start-time filter dropped the task that
performs the interaction. Long tasks now count when their range overlaps
the window: on one main thread only the dispatching task can overlap the
click. Layout shifts keep the start-time filter. J1 is unchanged (its
window starts at -Infinity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): bound J2's late-request check by the quiet sample's mark
The late-activity check compared requests against a fresh ledger mark
taken after waitForQuiet returned. A request that arrived while the final
quiet sample was still reading the IPC capture advanced that mark and
escaped the check. The boundary is now the ledger position read by the
accepted sample itself, like its DOM and IPC counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): end J2's HTTP window at the accepted quiet sample
The post-terminal window read the ledger after waitForMockQuiet returned,
so a request arriving in between was counted although its completion was
never waited for. waitForMockQuiet now returns the ledger position its
accepted sample read; later requests are kept as evidence
(httpRequestsAfterSettledByRoute) instead of the counter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): observe late mock requests before reading J2's ledger
httpRequestsAfterSettledByRoute read the ledger right after the accepted
quiet sample, so late requests had no chance to appear in it. The ledger
is now read after another quiet interval; the counter stays bounded by
the quiet sample's mark and late traffic shows up in the evidence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): fail the J2 quiet wait when a sample stalls past its deadline
waitForJourneyQuiet accepted a sample that returned unchanged after a
stall longer than the timeout as the end of a quiet period, before the
deadline check ran. The deadline is now checked first, so a stalled
sample fails the wait instead of letting the click go ahead unobserved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): detach J1's IPC capture before the J2 click
J2 used J1's capture to see pending launch bridge calls while settling,
but its ipcMain listener stayed attached and ran for every bridge call of
the measured click. The capture can now be detached; J2 detaches J1's
right after settling, before the click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(perf): sample both J2 settle captures in one main-process snapshot
The settle sample read J1's capture (pending calls) and J2's capture
(call count) in two evaluate calls, so a call starting in between was
counted with a stale zero in flight and its completion went unseen. Both
states are now read in one synchronous pass, where no ipcMain event can
be handled in between.
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(e2e): explain black canvases in the packaged frame-copy smoke
When the smoke's canvas stays black, print and attach the session
snapshot (with mpv's drop counter), every slot of the helper's
shared-memory frame rings read from /dev/shm, and the session's verbose
mpv log (log-file). Together they separate these cases: the helper never
published a frame, mpv rendered black, or the preload pump did not draw
a real frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): stop random black frames on software GL in frame-copy
mpv's LUT scalers (the default lanczos/spline family) build their weight
texture from an uninitialized talloc_array: mp_compute_lut fills only
kernel->size of every stride floats, so each row's padding is heap
garbage (reinit_scaler in video/out/gpu/video.c, mpv 0.41 and master).
The LUT is an rgba16f texture sampled with linear filtering. When the
padding holds NaN, or a value large enough to become Inf as half-float,
Mesa's llvmpipe carries it through the zero-weight neighbour texel, the
weights become NaN and the frame clamps to pure black. Whether it happens
depends on heap contents, so the packaged smoke's paused frame was black
in most attempts on the CI runner.
Evidence from CI (8 repeats each, no retries): baseline 2/8 passed;
LUT-free bilinear scalers 8/8; gpu-dumb-mode 8/8; LP_NUM_THREADS=1 6/8.
A synchronous glReadPixels right after mpv_render_context_render()
showed the black frame already in the helper's FBO. The readback, the
shared-memory ring and the preload pump were correct.
On a CPU rasterizer the helper now selects mpv's LUT-free bilinear
scalers and disables sigmoid upscaling. Software GL cannot afford the
LUT scalers anyway. A session option for the same key still wins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(playback): report software scaler results and scope smoke ring diagnostics
The software-renderer fallback now reports which scaler options mpv
accepted and which it rejected (with mpv's error), instead of always
printing success. The smoke's black-canvas diagnostics read only the
failed session's rings (<sessionId>-g<N>), not every impv-fc ring in
/dev/shm.
Addresses Greptile review feedback.
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>
* perf(workspace): share the startup inventory read and defer non-first-card IPC (J1)
Journey J1 / renderer.ipcCallsToFirstCard: 12 -> 7.
- PlaylistsService.getAllPlaylists() shares one in-flight SQLite read
between concurrent callers (the playlist effect and the XMLTV source
reconciliation at startup). Settled reads are never reused and every
SQLite write detaches the pending read.
- getM3uFavoriteChannels() stops re-reading the write-once IndexedDB ->
SQLite migration receipt once it has been seen.
- StartupDeferralService holds the download list, app update status and
the dashboard's recent items and favorites until one task after the
render that reveals the routed content (5 s safety timeout).
IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1 is the kill switch.
- reconcileEpgSources and the two distinct migration-flag reads stay on
the critical path: the first is the #1548 revision fence, the second
are different keys, not duplicates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(services): trust a migration receipt written in this session; correct J1 note
- Mark the IndexedDB -> SQLite receipt as confirmed after an empty-store
receipt write or a committed dbMigrateAppPlaylists, not only when it was
already present, so M3U favorites skip the per-playlist re-read on the
first launch after an upgrade too.
- The release note no longer claims a wall-clock speedup the J1 benchmark
did not show.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(services): keep only the shared startup inventory read (J1)
Drop the startup deferral gate, its kill switch and the deferred loads:
they lowered renderer.ipcCallsToFirstCard but moved neither
spawnToFirstCardMs nor load->card beyond drift, and they grew
renderer.initialBytes. Keep the shared in-flight inventory read, inline
it in PlaylistsService with short property names, and copy the joiner's
result with structuredClone (the Electron renderer has it; the services
test setup polyfills it for jsdom).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(performance): describe the shared inventory read and the J1 validation result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(services): drop the migration receipt memo for M3U favorites
Skipping the receipt read let the dashboard ask the database for M3U
favorite channels before a just-toggled favorite was written: the write
waits in the per-playlist queue and the cross-context lock, and the
dashboard does not reload when the store's favorites are unchanged.
The extra round trip had been masking that race, and the Windows E2E
run hit it (epg.e2e.ts "dashboard live rails find a programme that only
another playlist's XMLTV carries"). The memo was off the first card's
path, so it bought nothing measurable for J1; restore the per-call read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(services): share only the startup inventory read
A pending read was shared with any concurrent caller, but not every
playlist write goes through PlaylistsService: the settings reset deletes
all playlists through DatabaseService, so a caller could join a read
taken before that deletion (Codex review). Share only the first read,
which the startup pair (playlist effect and XMLTV reconciliation) needs
while the startup screen still hides every writing action; sharing ends
when it settles or a PlaylistsService write starts.
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(settings): hide cached title matches the parental lock withholds
Cross-playlist title matches are cached by their consumers (Actor and
Discover routes, the dashboard trending and recommendation rails and
the four "similar in your portals" rails), so matches found while
unlocked kept advertising locked titles and their playlist names after
a relock. Each consumer now filters on read through a reactive
predicate on the match's provider category, so a relock hides them at
once and an unlock shows them again, without a re-query.
Closes#1723
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): fall back to unlocked copies of a withheld title match
The dashboard trending and recommendation rails and the "similar in your
portals" rails picked one match per title at load, so hiding a withheld
match lost a copy of the same title in an unlocked portal. They now keep
every row the lookup returned and pick the match on read from the rows
the parental lock does not withhold. Adds the release note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(settings): derive the recommendations seed heading from the visible cards
The rail kept only the seeds that contributed a card to the original
selection, so a seed whose recommendations filled in after a relock (or
an offline prune) was missing from the "Because you watched" heading.
Every seed of the load is kept in order and the heading lists those that
contribute a card now.
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>
* 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>
MPV and VLC wait 2 s and 1.5 s before their first position poll, but the
delay timer was never stored, so stopping the poll during that wait could
not cancel it. A player that exited, errored or was replaced early left a
5 s (MPV) or 2 s (VLC) interval polling a dead socket or RC port until the
next launch or app quit. Keep the delay handle and clear it with the
interval.
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(dashboard): hold rail skeletons back so empty rails stop shifting the page
On every launch with sources the dashboard shifted by about 0.23 (the
"good" CLS threshold is 0.1). The rails render as soon as their own data
arrives, and each loading rail showed a 328 px skeleton immediately. On a
normal profile the live-favorites and recent-content sources resolve empty
15-20 ms later, so their skeletons flashed and collapsed and every rail
below jumped up by about 360 px. J1 never saw it: its layout-shift window
ends at the first card, which is painted just before the collapse.
Rail skeletons now wait out a 300 ms grace period (createRailSkeletonGrace)
and appear only for a rail still loading after it; the hero keeps its
immediate skeleton because it reserves the top of the page. Recorded over
three renderer reloads of a seeded profile, the dashboard's layout shift
drops from 0.219-0.234 to 0.0004, with the real rails painted at the same
time as before. Plan thread C5, journey J1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(dashboard): gate rail skeletons per rail, never above visible rails
Addresses the Codex and Greptile reviews on #1738. The component-wide grace
timer started when the dashboard was created, so a rail that begins loading
later (Xtream recently added, TMDB) showed its skeleton at once and could
still flash and collapse; and after the grace period a slow rail's skeleton
could appear above rails that already showed cards, pushing them down and,
if it resolved empty, back up.
createRailSkeletonGates now keeps one gate per rail, in template order: the
grace period counts from that rail's own loading start, a skeleton is never
inserted above a rail that already has cards (the real rail inserts at most
once instead), and a shown skeleton stays until its own rail finishes so the
first arriving rail does not collapse the others in a cascade. Nine specs
cover the fast path, per-rail start, the no-content-below rule, latching,
reloading, destroy and a zero grace period. The seeded-profile timeline is
unchanged at 0.0004 across three renderer reloads.
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>
* 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>
The watch-state About block rendered its poster whenever the URL was
non-empty, so a URL that failed to load showed the browser's broken-image
glyph with clipped alt text. The hero above already falls back on error.
A failed URL now counts as missing, matching the block's own degradation
rule. The failure is keyed by URL so a different poster gets a fresh attempt.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(e2e): stop mock servers from outliving Playwright runs
Playwright stops a webServer with a SIGKILL to the process group it
spawned, but `nx run-commands` starts its command in a detached process
group of its own. Launching the Xtream/Stalker mocks through
`pnpm nx run *-mock-server:serve` therefore left the tsx server running
(reparented to PID 1) and holding its port after every run, so the next
run failed with "…/health is already used" or silently reused a stale
server.
Every Playwright config now starts the mocks as a single
`node --import tsx apps/<mock>/src/main.ts` process with
TSX_TSCONFIG_PATH=tsconfig.base.json, which stays in Playwright's group.
A project-config spec guards all playwright*.config.ts files against
regressing to the Nx launch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(e2e): read sidebar categories atomically; tighten mock launch guard
- category-management: readVisibleSidebarCategoryNames read items one by
one; when Save removed an item between isVisible() and textContent(),
textContent() auto-waited for the gone label through the whole 15 s
poll, so expect.poll never retried (ubuntu shard 1 failed 3/3 while the
UI already showed "No categories available"). Take one snapshot with
filter({ visible: true }).evaluateAll() instead.
- project-config.spec: pin which Playwright configs start which mock,
reject any Nx form that mentions a mock server, and fail when a new
config starts a mock without being listed (the old count check passed
vacuously on zero matches).
- docs: state which configs start which mock instead of "every config
starts both".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* revert(e2e): leave the sidebar category read race to #1728#1728 fixes the same readVisibleSidebarCategoryNames race with a shared
helper; keeping a second copy here would only conflict. This PR stays
about mock-server lifecycle.
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(performance): count startup phases and SQL statements for the J1 launch journey
Implements plan item A2. With IPTVNATOR_PERF_CAPTURE=1 the main process
keeps named counters and registers a main-only performance:read-counters
IPC handler; without the flag nothing is counted and the handler does not
exist.
- debug-trace.ts owns the registry; traceStartupPhase replaces the
trace('startup', ...) sites and counts main.startupPhases.
- The database worker counts executed statements through better-sqlite3's
Statement prototype (the verbose callback expands every statement and
made bulk inserts 2-4x slower) and posts the count over its message
port, flushed before every other worker message. The main-thread shared
connection is counted through a new connection observer in the shared
database library.
- The first main window freezes main.modulesRegisteredBeforeWindow at
creation and main.sqlStatementsBeforeReadyToShow at ready-to-show.
- The journey gate drops the ready-to-show that Electron emits for the
about:blank detour, so the app sees the real document's first paint,
and taps the counters handler; the J1 record reads both counters after
the renderer probe completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(database): require one SQL statement per exec during initialization
The performance capture counts one exec call as one statement, because
SQL cannot be split reliably in the counter (trigger bodies contain
semicolons). The historical-upgrade driver now wraps exec on every
connection initDatabase opens and fails on a batch, so that counting
assumption holds for the fresh profile and all historical schemas.
Documents the definition in the counter and the architecture docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(performance): count SQL statements only for the launch journey
Codex review: the M3U import, refresh-cancellation and Xtream benchmarks
also run with IPTVNATOR_PERF_CAPTURE=1, so the statement hook wrapped
every row of their bulk inserts and changed what they measure.
SQL counting now also needs IPTVNATOR_PERF_COUNT_SQL=1, which only the
launch journey sets; a harness test fails if another source sets it.
Startup phases, the window snapshot and the read handler stay on the
capture flag. Without SQL counting no ready-to-show listener is attached,
so a zero is never reported for statements nobody counted.
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>
The "allows restoring live categories after every category is hidden"
Electron test failed intermittently on shard 1 across unrelated PRs and
master. The app hid every category correctly (the failure screenshot
shows "No categories available"), but readVisibleSidebarCategoryNames
looped over count() with per-row nth(index) reads. When the sidebar
removed a row between isVisible() and textContent(), textContent()
auto-waited for the missing element, so the expect.poll predicate never
returned and the poll timed out with "waiting on the predicate" instead
of retrying.
Read the visible rows with a single evaluateAll() snapshot in a shared
sidebar-categories.e2e-support.ts helper, use it for the category
picker in category-management and backup-roundtrip too, and record the
rule in the validation map.
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>