Commit Graph
447 Commits
Author SHA1 Message Date
4grayandClaude Opus 5.5 73ccdebb60 fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping
- Removing a playlist's last lock clears the SQLite index first; if the
  clear or the store write then fails, the index is re-stamped from the
  previous locks at once. Title matching and multi-source discovery query
  the worker directly and trust the index, so the stale flag alone did not
  protect them.
- A rollback publishes its store revision only after every type is
  re-stamped, so a reload cannot read a later type through the attempted
  stamps.
- docs: restore the index rules the earlier surfaces rewrite dropped from
  the contract, now in the Lock store lifetime section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:52:08 +02:00
4grayandClaude Opus 5.5 5c3e675b41 perf(settings): keep the PIN dialog and the Stalker enforcement step off the initial path
Master (#1712) moved the UI component barrel and the Stalker data layer out
of main.js and tightened the initial budget to 2 MB. The parental-lock
prompt imported the PIN dialog through the ui/components barrel and the
enforcement service injected the Stalker store at startup, which pulled
both back in (2.55 MB, over budget). The PIN dialog now loads through a
local lazy file on the first prompt, and the Stalker step loads only while
a Stalker route is open: initial total 1.65 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:39:44 +02:00
4gray c035085edf Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2 2026-09-27 09:35:14 +02:00
4grayandClaude Opus 5.5 3b892f4114 fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked
- A relock during a page-1 Stalker search now filters the rows already on
  screen at once, so old unlocked results are not clickable while the
  replacement page is pending.
- When every M3U group is locked the groups rail still renders, with its
  "N locked · Enter PIN to show" row, instead of the plain empty state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:03:07 +02:00
4gray cdc79da768 Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2
# Conflicts:
#	apps/web/src/app/settings/settings-options.ts
#	apps/web/src/app/settings/settings.component.html
2026-09-27 08:49:18 +02:00
4grayandClaude Opus 5.5 5dbad2383f perf(web): keep channel lists, EPG views and the Stalker layer off the initial path (#1712)
The root shell imported WindowControlsComponent and DialogService through the @iptvnator/ui/components barrel, and esbuild keeps every Angular component module a barrel re-exports, so channel lists, EPG views, @angular/forms, date-fns and the whole Stalker data layer sat in main.js. The shell now uses file-level entries, the Stalker connection editor is a lazy proxy, and the release-notes and external-player info dialogs load on demand with a handled failure path.

renderer.initialBytes 2,714,336 -> 1,626,019 bytes (-40%); the baseline is lowered to the ubuntu ratchet measurement and the production/PWA initial budgets drop to 1.8/2 MB. J1: did-finish-load about -16 ms, first card within noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:48:01 +02:00
4gray f166ff4d47 ci(packaging): time-box the Snap and Flatpak embedded MPV runtime probes (#1704) 2026-09-27 07:54:20 +02:00
4grayandClaude Opus 5.5 e8902f472a perf(ci): skip unit coverage on PRs that cannot reach it and persist the Jest cache (#1711)
Pull requests whose changes cannot reach any Tier A test (allowlist checked against declared Tier A inputs and an AST scan of cross-project reads) skip the unit coverage suite; master pushes always run it. Jest's transform cache is persisted with actions/cache: PRs restore only, master pushes start empty and save. Paired CI runs: Tier A 9m04s cold -> 6m09s warm. Nx Cloud is intentionally not used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:50:07 +02:00
ea51cae3db feat(settings): search settings from the header and the command palette (#1714)
* feat(settings): search settings from the header and the command palette

The header search on the Settings page was shown but disabled. It now
searches a shared index of all 56 settings rows by translated title,
description and English synonyms, replaces the section page with ranked
results, and opens a result by scrolling to, focusing and briefly
highlighting its row. Enter opens the best match, and the section
navigation shows per-section match counts.

The command palette gains a "Settings" group that lists the best six
matches for a non-empty query, so any setting is one Ctrl/Cmd+K away.
Rows hidden by the current form state fall back to the control that
reveals them; rows the runtime cannot render are never returned.

The index ships through a new @iptvnator/workspace/shell/util/settings-search
sub-entrypoint so it stays out of the eager bundle, and a registry spec
keeps it in step with the section templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): let search reveals win over pending input and gate embedded MPV rows

- A reveal (result click, command palette, Enter) now cancels a search
  keystroke still waiting for its debounce, so its q navigation can no
  longer supersede the reveal and leave the results open.
- Embedded MPV extra options and auto-reconnect require a lazily probed
  embedded MPV capability; frame copy also needs frameCopyAvailable, so
  search never offers a row the settings page cannot render.
- Keyboard users keep a focus-visible ring on the revealed row after the
  highlight fades.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(workspace): wait for palette probes without Promise.allSettled

The web tsconfig lib predates Promise.allSettled; use Promise.all over
rejection-safe probes instead.

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>
2026-09-27 07:42:53 +02:00
4grayandClaude Opus 5.5 b87291e388 ci(e2e): run the macOS Electron suite as two shards (#1707)
macOS runners are the scarcest on this account, so macOS now runs the
Electron E2E suite as two Playwright shards instead of three; Ubuntu and
Windows keep three. macOS shards get a 40-minute timeout. The summary job
needed no change because it reads each shard's total from its report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:32:37 +02:00
4grayandClaude Opus 5.5 34b3d37df0 fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check
- The idle relock no longer interrupts a playing radio station: playing
  <audio> counts as activity, like video.
- A restore retries a failed lock-store read before checking a reused id
  for stale locks, and aborts while the store stays unreadable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:31:36 +02:00
4grayandClaude Opus 5.5 2161c6a859 fix(settings): retry a failed clear-all of the lock store and keep restored new playlists free of stale locks
A lock-store clear that failed after "Remove all playlists" only logged,
so a later restore reusing a playlist id could inherit the deleted
playlist's locks. The in-memory store now empties at once and the
persisted clear is retried on the next access; a restore that creates a
playlist starts it from empty locks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:19:39 +02:00
4grayandClaude Opus 5.5 5d6c378f22 fix(settings): show the locked-groups row on the M3U rail and roll the relock timeout back to the recovered value
- The M3U groups rail now renders the same "N locked · Enter PIN to show"
  row as the portal category rail, so locked groups no longer vanish
  without an in-context unlock.
- A failed relock-timeout write rolls back to the value read after the
  settings retry, not to the pre-retry default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:56:15 +02:00
4grayandClaude Opus 5.5 b32940f690 fix(settings): retry a failed settings read before any parental-lock settings write
updateSettings writes the whole settings object, which after a failed
startup read is the defaults; enabling the lock or changing the relock
timeout then replaced the user's persisted preferences. The read is
retried first and the write refused while settings stay unreadable. The
settings writes move to parental-lock-settings-writer.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:33:59 +02:00
4grayandClaude Opus 5.5 54f27fe656 fix(settings): apply single-row lock toggles inside the lock store's write queue
The right-click Lock/Unlock (portal categories and M3U groups) built the
new list before entering the queue, so two quick toggles shared one
snapshot and the second dropped the first. Lock writes now accept an edit
of the current list, evaluated inside the queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:22:15 +02:00
4grayandClaude Opus 5.5 0f9c7a6364 fix(settings): serialize lock-store writes and drop a deleted playlist's locks
- Lock-store mutations run through one write queue: each rewrites the
  whole persisted store, so overlapping edits could otherwise snapshot
  the same store and the later write would drop the earlier edit.
- Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP
  hook; "Remove all playlists" clears the lock store once the deletion
  has succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:14:27 +02:00
4grayandClaude Fable 5.1 8ebb7e3424 perf(ci): run Tier A coverage concurrently with isolatedModules ts-jest (#1701)
Tier A coverage runs projects a few at a time (largest first, bounded Jest workers, buffered output, fail-fast kept) and ts-jest transpiles with isolatedModules instead of type-checking per process; five type re-exports become export type, two decorated inputs use import type. Unit Tests and Typechecks job: 26 min -> 9 min (Tier A step 23 min -> 6.5 min).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:05:26 +02:00
4gray a030e1af2f Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2
# Conflicts:
#	apps/electron-backend/src/main.ts
2026-09-26 23:04:22 +02:00
Kate Markson e619a2be86 fix(xtream): scroll to the selected category (#1663) 2026-09-26 21:59:09 +02:00
4grayandClaude Fable 5.1 e39c854a41 perf(electron): load the main-process startup wiring after the window starts loading (#1702)
Registering IPC handlers costs 0.4 ms; evaluating the modules behind them (axios, drizzle-orm, better-sqlite3, electron-updater, fix-path) before the window could load was the real cost. main.ts now keeps only the pre-paint wiring and loads the rest as the deferred-events.js chunk inside the main window's did-start-loading listener, where the import and its registrations complete before any renderer invoke can arrive.

Interleaved A/B on the performance build: app.whenReady 323 -> 265 ms, did-finish-load 499 -> 447 ms; J1 journey spawnToDidFinishLoad ~405 -> ~365 ms with identical counters. Packaging ships the chunk explicitly, verify:package-layout requires it, and the benchmark build identity hashes it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 21:51:19 +02:00
4grayandClaude Fable 5.1 497b6076fa ci(e2e): shard the Electron Playwright suite per OS (#1700)
Run the sequential Electron E2E suite as three Playwright shards per OS
(one runner each) and summarize all shards of an OS in one follow-up job.
The semantic summary script accepts a directory of shard reports, merges
them and refuses to write when a shard is missing, duplicated or
malformed, or when an explicit input does not exist.

Slowest shard per OS in the final run: ubuntu 12.5 min (was 26),
macOS 13.7 min (was 34), Windows 24.5 min (was 35).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 21:49:07 +02:00
4grayandClaude Fable 5.1 587e19541f perf(electron): compile the main-process bundle from a V8 code cache (#1696)
main.js is now a small entry that enables Node's on-disk V8 compile cache under userData/v8-compile-cache and then requires the application bundle main.app.js. Warm launches reach app.whenReady about 13 ms sooner at the median; IPTVNATOR_DISABLE_COMPILE_CACHE=1 turns it off and IPTVNATOR_COMPILE_CACHE_DIR relocates it.

Packaging now ships main.app.js explicitly and verify:package-layout requires the main-process entry files in app.asar, because nx-electron copies the backend through an allowlist. The Xtream benchmark build identity hashes both the launcher and the bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 19:19:30 +02:00
0f1cad4b16 test(performance): add the J1 launch-to-usable journey benchmark (#1698)
* test(performance): add the J1 launch-to-usable journey benchmark

Implements plan items A1, A3 (J1 only) and the minimal A4 from
.plans/2026-09-25-performance-journeys-ratchet.md.

- journey-renderer-probe.ts: init-script probe counting DOM mutations,
  layout shifts and long tasks until the first source card is visible on
  /workspace with the splash removed; unit-tested with jsdom fixtures.
- journey-main-ipc-capture.ts: counts bridge invocations from the preload's
  renderer-API trace channel up to a sentinel call the probe fires, so the
  IPC counter is exact without touching production code.
- launch.journey.ts + playwright.journeys.config.ts: seeded profile (one
  M3U source, one Xtream portal on the loopback mock), one warm-up and five
  measured iterations, fresh process and data directory each, writing
  dist/performance/journeys/<timestamp>/summary.json with exact counters
  and P50/P90 wall-clock.
- Nx target electron-backend-e2e:journeys and root script perf:journeys.
- docs/architecture/performance-journeys.md, README, context and
  validation map entries.

renderer.cdTicksToFirstCard and main.sqlStatementsBeforeReadyToShow are
reported as unavailable with the reason instead of being faked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(performance): gate the renderer load so the probes never race startup

Review follow-up for #1698.

- journey-renderer-gate.cjs: a main-process hook loaded with `-r` (the
  mechanism Playwright uses for its own loader) makes the first
  loadFile/loadURL navigate to about:blank and holds the real load until
  the test releases it. Playwright reports no page before a navigation
  commits, so this is what lets the renderer probe be registered on the
  page before the real document exists; the IPC capture is installed
  before the release too. A safety timeout releases the gate on its own
  and marks the iteration invalid. Unit-tested with a fake BrowserWindow.
- launch-journey-app.ts: registers the probe on the parked page, releases
  the gate, waits for the real document to commit, fails fast when the
  probe is missing, and refuses an iteration whose gate timed out, saw a
  second load, or released before the probe was in place.
- journey-renderer-probe.ts: entries delivered live after the terminal
  batch are buffered and filtered by the same cutoff as queued ones, and
  the cutoff is sampled in a timer queued from the first rAF, i.e. after
  the card's frame is painted, so the render task's long task and layout
  shift are consistently included.
- launch-journey-record.ts: layoutShiftScore rounded to three decimals; a
  0.0001 shift flipped in and out of the cutoff between iterations.
- playwright.journeys.config.ts: reuse a mock server left on the journeys
  port locally (its fixtures are deterministic); CI still starts its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(performance): validate the journey gate after the probe completes

Codex follow-up on #1698: the gate state returned by release() cannot see a
reload or recovery navigation that happens before the first card. Re-read the
live state once the renderer probe has finished and validate that instead,
so an iteration spanning an extra navigation is rejected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(performance): fail an iteration whose performance observers were unavailable

Codex follow-up on #1698: a renderer that cannot observe layout-shift or
longtask entries used to pass the probe with zero counters, which a ratchet
could not tell apart from a genuine zero. The probe assertion now rejects
such an iteration and names the missing observer.

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>
2026-09-26 19:18:19 +02:00
4grayandClaude Fable 5.1 225e8de78e feat(settings): one lock entry point per rail plus a right-click Lock/Unlock
- Stalker's dedicated lock button becomes the same "Manage categories"
  (tune) button the Xtream rail has; it opens the lock-only dialog, so
  every portal type shares one entry point and the rail header keeps
  three actions.
- Right-clicking a category (Xtream, Stalker) or an M3U group offers a
  single-row Lock / Unlock through the shared CategoryLockMenuComponent,
  behind the same PIN gate and lock store as the dialog.
- The settings hint explains where locks are set; group lock strings added
  to all locales (ru/de translated).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 19:10:48 +02:00
4gray 0e4e1d2169 chore(release): begin 0.25 development and publish 0.24 article (#1674) 2026-09-26 17:13:13 +02:00
4gray 1d945e91b1 Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2
# Conflicts:
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change app language.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change app theme.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Change video player.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Check settings page.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Deep links open one section page and unknown sections redirect.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Leaving with unsaved edits asks for confirmation.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Save-and-leave persists the staged edit.png
#	apps/web-e2e/dist/.playwright/apps/web-e2e/screenshots/settings/@settings @web Unsaved bar survives section switches and discard reverts.png
2026-09-26 14:18:36 +02:00
4grayandClaude Fable 5.1 9b8f58b84a fix(settings): retire a resolving Stalker live playback when the session relocks
The embedded player defers selecting the channel until its stream
resolves, so the enforcement service's cleared selection could not retire
the request; it now carries the lock version it was issued under and is
dropped when a relock happened meanwhile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 14:08:38 +02:00
4grayandClaude Fable 5.1 512e9787a8 perf(web): load Angular date locales lazily per language (#1695)
Plan thread C1, journey **J1 `launch`**, counter **`renderer.initialBytes`**. Stacked on #1694 → #1693 → #1692; merge in order.

`apps/web/src/app/app-date-locales.ts` imported the locale data of all 18 supported languages eagerly, so every user shipped and parsed all of it at startup. Each locale is now a dynamic import keyed by the Angular locale id that `normalizeDateLocale()` derives from the app language (`by` → `be`, `ary` → `ar-MA`, `zhtw` → `zh-Hant`); English needs no data.

Ordering is preserved so no template renders a locale whose data has not arrived (Angular throws in that case):
- `main.ts` awaits the initial language's data (from `getInitialLanguage()`) before `bootstrapApplication`.
- Both `TranslateService.use()` call sites, `AppComponent.initSettings()` and `SettingsFormFacade.applySavedSettings()`, register the data first through the new `AppDateLocaleService`.
- A failed load never leaves the locale without data: English formatting is registered under the requested id (eager 1.1 KB `@angular/common/locales/en`), so `DatePipe` renders instead of throwing; the locale is not marked registered, so the next call retries and a success replaces the fallback (review follow-up).
- `AppDateLocaleService.use()` orders switches by request, not by completion: a switch whose data arrives after a newer request is dropped, so the language chosen last wins (review follow-up).

No kill switch: behavior is identical once the locale resolves, and the only new failure mode (a same-origin chunk failing to load) is shared with every lazy route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:44:29 +02:00
4grayandClaude Fable 5.1 d3b6e548cc ci(performance): fail when the web app's initial bytes grow (#1694)
Third step of the performance-journeys ratchet, stacked on #1693 (which is stacked on #1692; merge in order, GitHub retargets each to `master`).

- New `Initial bytes ratchet` job in `.github/workflows/ci.yml` (ubuntu-latest): install, `pnpm nx build web --skip-nx-cache` (production configuration, the one users download), then `pnpm run perf:initial-bytes:check`. The job fails when `renderer.initialBytes` exceeds `tools/performance/journey-baselines.json`.
- `dist/performance/` is uploaded as the `performance-journey-summary` artifact on every run, so a failing or tightenable run carries its evidence.
- After review: the job first runs the new `tools/performance/check-baseline-direction.mjs`, which compares `journey-baselines.json` with the revision the change is measured against (the target branch of a pull request, `github.event.before` for a `master` push, `master` for a manual dispatch) and fails on any raised enforced limit (`value × toleranceRatio`), any widened or newly added tolerance, or any removed entry, so a PR cannot grow the payload and raise the baseline to match (lowered limits and new entries pass; a target branch without the file has nothing to weaken). Node tests cover it.
- Docs: the performance-journeys contract and the validation map name the job, and the contract now states that this runner is the canonical measurer (take baseline values from its output, not from a local build).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:44:01 +02:00
4grayandClaude Fable 5.1 7abaad29e7 chore(performance): commit the initial-bytes baseline and ratchet checker (#1693)
Second step of the performance-journeys ratchet, stacked on #1692 (merge that first; this PR retargets to `master` automatically).

- `tools/performance/journey-baselines.json`: J1 `launch` / `renderer.initialBytes` = **2,739,510 bytes**, the ubuntu runner's production build of `apps/web` at this content (after #1692 stopped bundling `package.json` into `main.js`). A local macOS build of this pre-#1695 code is 2 bytes smaller in `main.js` (the eager locale imports); once #1695 removes them the two are byte-identical. Correction to an earlier version of this description: the "556-byte macOS vs Linux difference" was almost entirely `package.json` text embedded in `main.js`, which moved with every script edit in this stack, plus this 2-byte residue. The runner is the canonical measurer; the CI run on the stacked #1694 branch (this content plus the job) is where the number is confirmed.
- `tools/performance/check-journey-ratchet.mjs` compares a journey summary with the baselines: a counter above its value fails (exact, no slack), wall-clock entries fail above `value × toleranceRatio`, a baseline without a measurement fails so dropping a measurement cannot disable the ratchet, values below baseline print a "tighten" hint, and measured counters without a baseline are noted only. After review: checking nothing (empty file, or `--only` naming a missing entry) fails; a counter is read only from `counters` and a wall-clock entry only from `wallClock`; the repeatable `--only <journey>/<counter>` flag scopes a check.
- Root scripts: `perf:initial-bytes:check` (measures into its own `dist/performance/initial-bytes.summary.json`, then checks `--only launch/renderer.initialBytes`) and `perf:ratchet:check` (full check); `perf:tools:test` runs both test files, as does `pnpm nx test performance-tools`.
- `docs/architecture/performance-journeys.md` gains the Ratchet section (file format, rules, "baselines only move down"); the validation map lists the check.

The CI job that runs the check on every PR is #1694; C1 (lazy Angular date locales, #1695) then lowers the baseline with the measured output as evidence.

Note: `ci.yml` only triggers on pull requests targeting `master`, so this stacked PR shows no Actions runs until #1692 merges. The evidence runs above were dispatched with `gh workflow run ci.yml --ref <branch>`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:43:33 +02:00
4grayandClaude Fable 5.1 8bc877b625 chore(performance): measure initial bytes of the built web app (#1692)
First step of the performance-journeys ratchet (plan thread: J1 `launch`, counter `renderer.initialBytes`).

- `tools/performance/measure-initial-bytes.mjs` reads the built `dist/apps/web/index.html` and sums `index.html` plus every same-origin `<script src>`, `<link rel="stylesheet">` and `<link rel="modulepreload">` it references. Manifest, icons, external URLs and lazy chunks are not counted. A referenced file missing from the build fails the measurement instead of counting as zero bytes.
- `--json` prints the breakdown; `--summary <file>` writes the `journeys.<journey>.counters` shape a ratchet checker will consume (next PR).
- New Nx project `performance-tools` (test + lint targets), Tier B in `tools/coverage/coverage-policy.json`, root scripts `perf:initial-bytes` and `perf:tools:test`.
- New contract `docs/architecture/performance-journeys.md`, linked from the validation map, the agent context map and the README.
- **Review follow-ups:** resources are deduplicated by request URL (query kept, fragment dropped); `index.html` is parsed with parse5 (already a repository dependency, scripting enabled), so comments, bogus comments, raw-text bodies (script/style/noscript/title/textarea), inert `<template>` contents and character references in attributes all follow the HTML5 algorithm instead of a hand-written scanner; the review's edge cases stay as regression tests; docs show the `pnpm --silent` form for JSON output and explain how the counter relates to Angular's rounded "Initial total".
- **Found while measuring:** the environment files and the playback diagnostic panel imported the whole `package.json` (`import packageJson from '@package'`), which esbuild cannot tree-shake, so `main.js` carried the complete file and the counter moved with every script or dependency edit. They now import `{ version }` only (eb662c485): `main.js` shrinks by **11,539 bytes** and the counter no longer depends on `package.json`. Jest's ESM loader exposes JSON only as a default export, so the two web Jest configs map `@package` to a stub that serves the real file's fields as named exports. Release note: `.changes/web-version-only-from-package-json.md` (`type: perf`).

Production build after this PR measures **2,739,508 bytes** (11 files + `index.html`); Angular's "Initial total" is this minus `index.html` and `assets/app-config.js`. The baseline file and the CI check land in the follow-up PRs; C1 (lazy Angular date locales) then lowers it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:31:27 +02:00
4grayandClaude Fable 5.1 efece8628f fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps
- clearSearchResults() advances the search request version, so a search
  issued under the previous lock state cannot republish what a relock
  just cleared.
- A lock write publishes its store revision only once every touched type
  is stamped, so a reload triggered by it cannot read a later type
  through its old stamps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 13:23:10 +02:00
4grayandClaude Fable 5.1 d2d9a56096 fix(settings): gate the Electron cached category/content reads while locks are unknown
The warm-route hydration reads the cache directly; it now returns nothing
while the lock store withholds everything, like the live reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 12:44:56 +02:00
4grayandClaude Fable 5.1 870a2ec7fd fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity
- A hidden-category lookup that lands after a later playback (same
  provider id, another playlist) no longer overwrites the newer channel's
  category; resolutions are generation- and playlist-checked.
- Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id
  or the row's cmd/name, so id-less rows no longer collapse onto one key
  and stall paging past locked pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 12:07:29 +02:00
4grayandClaude Fable 5.1 4bd4bf2fc2 fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries
- The workspace route resolver awaits ParentalLockService.initialize()
  next to the settings load, so no route or catalog activates before the
  PIN and lock store are known.
- Every lock write re-reads a failed store before building its edit, so a
  recovered store is edited rather than overwritten.
- The deferred hydration reload runs under the publish guard of the
  request that deferred it.
- Backup import validates every parental lock entry and rejects a damaged
  list instead of erasing the persisted locks on restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 11:10:48 +02:00
4grayandClaude Fable 5.1 d4d0754e12 fix(settings): step off the M3U channel and Stalker selection before awaiting the Xtream relock reload
The Xtream store stays populated after leaving that portal, so its reload
runs on every apply; a locked M3U channel no longer keeps playing behind a
slow database or provider read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 10:28:12 +02:00
4grayandClaude Fable 5.1 5c141b24b8 fix(settings): resolve hidden live categories before relocking playback and reload categories in the deferred hydration path
- The Xtream live layout resolves a playing channel's category through
  the unfiltered rows when the visible list lacks it (search can play a
  hidden category's channel); until that lookup lands the category is
  unknown and a relock stops the channel.
- A relock that overtakes the initial hydration now withholds the
  category publications too and reloads categories with the content.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 08:54:38 +02:00
4grayandClaude Fable 5.1 b5be8b2773 fix(settings): keep unlocked category routes reachable and defer a relock reload that overtakes the initial hydration
- The Xtream category guard no longer runs the item check on category-only
  routes (Number(null) is 0), which prompted for the PIN on every unlocked
  VOD and series category while the lock was active.
- A lock change during the initial Xtream hydration withholds the rows the
  hydration publishes and runs the filtered reload once it has settled,
  on every path that marks the content initialized.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 08:17:52 +02:00
4grayandClaude Fable 5.1 bfdfe18a9c fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read
- ElectronXtreamDataSource serves no categories, content or search hits
  while the lock store withholds everything; its SQLite index may still
  carry a stale stamp.
- setupPin decides whether to persist the switch from the settings value
  before the PIN is stored, since enabled follows hasPin while the switch
  is unknown.
- A lock store recovered by a later read marks its playlists stale so the
  index is re-derived, a persisted entry must carry all three lists, and a
  stale Stalker search page is dropped before touching the withheld-id
  bookkeeping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 07:36:23 +02:00
4grayandClaude Fable 5.1 4f931030e1 fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store
- A write that removes a playlist's last lock clears the SQLite index
  first and drops the store key afterwards, so an interruption between the
  two can only leave a state the startup reconcile repairs toward locked.
- A PIN hash read failure is distinct from an absent PIN: the session stays
  locked and every PIN-protected step re-reads it first.
- Backup export awaits parental lock initialization and refuses to run
  while the lock store is not readable, since an absent lock field means
  "no opinion" on restore.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 06:57:27 +02:00
4grayandClaude Fable 5.1 02f5b09201 fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save
- A relock now fails closed immediately: the selected detail is stepped
  off against the lock store, the catalog lists and stored search results
  are emptied, and the filtered reloads publish only while the captured
  lock version is still current.
- Backups carry M3U lock titles verbatim (exact dedup), since the locks
  match group titles exactly.
- A relock-timeout write that fails reverts the in-memory value and shows
  the settings save-failure snackbar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 06:21:18 +02:00
4grayandClaude Fable 5.1 559ec5508c fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries
- The Electron mirror of the feature switch is awaited; a mirror that
  cannot be written undoes the settings write, so a reload never starts
  from a mirror that disagrees with the persisted switch.
- A failed multi-type re-stamp rolls the store back AND re-stamps every
  touched type from it, since earlier types may already carry the new
  locks; a failed rollback keeps the playlist stale (fail-closed).
- A persisted lock store whose nested entries are not what writeLocks
  produces is a failed read, not an empty store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 05:40:21 +02:00
4grayandClaude Fable 5.1 b64defb576 fix(settings): await the startup lock-index reconciliation and withhold genre-less rows when failing closed
- The lock store is readable only once the SQLite index has been re-derived
  from it, and a re-stamp that keeps failing keeps the session fail-closed,
  so catalog reads can never serve rows stamped unlocked by a stale index.
- While everything is withheld, Stalker rows without a genre are withheld
  as well (the store filter and the renderer predicate).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 05:00:45 +02:00
4grayandClaude Fable 5.1 4b14c0929c fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps
- The window before the initial lock store read settles now withholds
  everything, like an unreadable store: settings can report the feature as
  on before the locks are known.
- The store commits before the SQLite index re-stamp; a failed re-stamp
  now rolls the store back, a failed rollback re-stamps on the next
  access, and every launch re-derives the index from the store.
- Xtream category/content reloads fail closed: a rejected reload empties
  the affected lists (content types drop back to idle) instead of keeping
  rows read under the previous lock state.
- Enabling/disabling the feature persists through one guarded path that
  undoes the in-memory switch and skips the Electron mirror on a failed
  settings write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 04:20:06 +02:00
4grayandClaude Fable 5.1 f5c3543c28 fix(settings): drop pre-relock Stalker search pages and fail closed on a corrupt lock store
- A Stalker search page issued before a relock was filtered with the
  pre-relock withheld set and could still be applied after it; the
  staleness check now includes the parental lock version.
- A lock store payload that does not parse or is not an object is a
  failed read (everything withheld until it reads again), no longer an
  empty store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 03:39:28 +02:00
4grayandClaude Fable 5.1 c114cd04d3 fix(settings): fail closed on an unreadable lock store and make lock writes reliable
- A lock store that cannot be read is no longer treated as empty: while
  the lock is active every category is withheld (renderer predicates and
  set-based filters alike) until the PIN is entered or the store reads
  again, and writes are refused meanwhile so an empty in-memory store can
  never wipe the persisted locks. The lock set now lives in its own
  ParentalLockLockStore service.
- The M3U group dialog's lock write is awaited and a failed save is
  reported in a snackbar instead of being silently dropped.
- The Electron categories.locked re-stamp clears and re-locks inside one
  transaction, so a failed restamp keeps the previous index.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 02:59:54 +02:00
4grayandClaude Fable 5.1 41bab4dcaa fix(settings): fail closed on unreadable settings and finish relock clean-up
- Unreadable settings (IndexedDB load failure) left the feature switch at
  its default and announced "unlocked" to the main process. A stored PIN
  now stands in for the switch, and without one nothing is announced, so
  the worker keeps its mirrored locked default.
- Lock applies run one at a time and abandon superseded results; the
  Electron data source keys its in-flight share by lock version so a
  relock can never reuse an unlock refresh's unfiltered rows.
- The stored in-portal Xtream search is re-run on a lock change.
- Stalker live/radio selections are judged by tv_genre_id, and both live
  layouts drop the playback of a channel whose category became withheld.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 01:10:16 +02:00
4grayandClaude Fable 5.1 bc1b0eef41 fix(settings): close lock editors on relock and clear withheld details opened from All
The Xtream, Stalker and M3U category editors are gated by the PIN only when
they open; an idle relock left them on screen listing locked names with a
lock-rewriting Save. Each now closes itself when the session relocks.

ParentalLockEnforcementService also judges the selected Xtream/Stalker
item by its own category: a detail opened from All, recently added or
search has no selected category to vanish with, so it stayed open after a
relock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 00:25:01 +02:00
4grayandClaude Fable 5.1 45661e311e fix(settings): arm the relock timer on enable and harden Stalker search relock
- The idle timer follows the unlocked transition instead of `active`, so the
  session that just enabled the lock still locks itself later.
- Stalker search closes an open detail whose genre became withheld on
  relock and advances by itself past pages made only of locked rows (only
  while they add ids the list has not seen).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 23:50:39 +02:00
4gray 3ed612ba65 fix(release): repair Snap uploads and retry published releases (#1691)
* fix(release): allow Snapcraft scratch extraction and retry public releases

* test(release): detect local Snap permission test prerequisites
2026-09-25 23:34:44 +02:00