A backup merge now asks for the PIN again right before it replaces a
playlist's locks when the app relocked during the import, instead of
relying on the answer given at the start. The wrong-PIN count and the
30-second pause move from the dialog into the lock service, so
dismissing and reopening the prompt no longer resets them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reload issued for playlist A no longer publishes into the shared Xtream
store after the user opened playlist B: the store's reloads guard on the
playlist they read for, and the enforcement apply retires its search
refresh and selection checks on a playlist switch as on a newer lock
version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A backup carrying lock lists replaces the matching playlists' locks,
possibly with an emptier set; the import now asks for the PIN (after the
file was chosen) and aborts when it is refused.
- While a write re-stamps the SQLite index the playlist counts as stale,
so a relock inside that window reloads fail-closed instead of through
the old stamps. The internal store write now needs only a readable
store, so a rollback can still land.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On relock the synchronous fail-closed steps (M3U channel, Stalker
selection, the locked Xtream detail, catalog lists, stored search) run
immediately instead of queueing behind an earlier apply that may still
wait on a slow or hung read.
- The post-reload Xtream checks decide by the lock store through the
unfiltered category rows rather than by absence from the reloaded list,
which also omits merely hidden categories; unreadable rows fail closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A backup restore now writes the parental locks last, so a failed Xtream
merge leaves the playlist's previous locks in place instead of the
backup's possibly smaller set.
- The Stalker lock dialog snapshots the category list before its lazy
import and opens only if the route is unchanged, so another portal's
categories can never be saved under this playlist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Electron a relock that overtook the initial content hydration waited
for the category reload before the content reload set the deferral flag;
the older unlocked hydration could publish its streams in that window.
withholdCatalog() now sets the flag itself, before anything is awaited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected chunk (e.g. a stale PWA page after a deployment) escaped
applyStalker(), so a locked Stalker selection kept playing after a relock
and the Xtream step was skipped. The step now leaves the Stalker route on
a load failure, which clears the selection and stops playback, and the
Xtream step still runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With every group locked the filtered channel list is empty, so the
container showed its generic empty state and the groups rail's
"N locked · Enter PIN to show" row never appeared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
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>
- 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>
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>
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>
* 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>
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>
- 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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
* 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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>