The Settings switch snaps back to the saved state when clicked and
follows it once the PIN action succeeds, so a cancelled or refused PIN
no longer leaves it showing the opposite state. A failed switch write is
undone to the value read after the settings retry instead of the
hard-coded inverse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Xtream and M3U management dialogs offer lock toggles, and the
Stalker lock dialog opens, only once the lock store has been read. A
draft built from the empty fail-closed snapshot would otherwise replace
the real locks with nothing on Save if storage recovered in between.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The groups view captures the playlist before the PIN prompt and drops
the management dialog's hidden and locked group lists once another
playlist is open, so they cannot be saved under that playlist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The enforcement service now checks the active M3U channel whenever it
changes while locked, so numeric zapping, next/previous and remote
commands, which select from the full channel list, cannot start a
channel of a locked group. Numeric zapping also skips such a channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A locked session can no longer change the relock timeout: the Settings
selector is disabled until the PIN is entered and the service refuses
the change while locked. An M3U right-click lock toggle now captures its
playlist before the PIN prompt and is saved only if that playlist is
still open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A rejected lock-state sync to the SQLite worker makes the locked
session withhold everything until a later sync succeeds.
- The Stalker enforcement chunk is preloaded when a Stalker route
opens; a relock runs it synchronously, or leaves the route at once
while it is not loaded, instead of awaiting the chunk.
- Xtream "Manage categories" captures playlist, provider and section
before the PIN prompt and re-checks them after it and after the
dialog import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VOD multi-source host keys its discovery session to the parental
lock version: a lock change drops the discovered sources, retires
discoveries and switches in flight, and rediscovers through the
worker's new lock state. Stale-index entries of a write still stamping
are no longer retried by a concurrent reconcile, which could re-stamp
from a store the write had not committed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog title matching and multi-source discovery query the SQLite
worker directly; they now return nothing while the parental lock
withholds everything (unreadable store or a bridge without the worker
filter). The Stalker lock dialog captures its playlist, provider and
section before the PIN prompt and re-checks them after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the lock store cannot be read every portal category is withheld
but no locked ids are known, so the rail showed no "Enter PIN to show"
row. It now shows the row without a count in that state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an Xtream lock edit's re-stamp fails and the rollback store write
fails too, memory now goes back to the previous locks and a pending
rewrite persists them on the next store access before the index is
re-stamped, so the failed edit cannot take effect through that re-stamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lock removal is now authorized right before its first write is issued;
a relock that lands after that is ordered after the write, which
completes. This drops the post-write rollback, whose own failure could
leave the persisted store diverged from memory across a restart. The
Stalker search closes a detail without a genre on relock while every
category is withheld.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When writing the previous store back after a relock-refused removal
fails, the lock store now keeps a pending rewrite, is not readable (the
locked session withholds everything) and rewrites the persisted store
from memory on the next access, so a restart cannot load the removal.
A failed "Remove all playlists" clear shares the same retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lock removal that passed the unlocked check before its write is asked
again right after the store write and, for Xtream, after the index
stamps. A relock in between writes the previous store back or rolls the
stamps back before anything is published. The stale-index bookkeeping,
index stamping and store merge move into helpers to keep the lock store
within the file size limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new runtime capability requires the lock-state and index-stamping IPC.
When Electron reads Xtream through the SQLite worker without it (a
partial or older preload), the locked session withholds every category
instead of trusting a worker that never learned the lock state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lock edits that take a lock away now commit only while the session is
unlocked, checked inside the write queue at commit time, so an editor
save still in flight (or queued) when the app relocks cannot remove
locks. Adding locks stays allowed. The Xtream category dialog drops its
lock draft after a relock, and a backup restore re-asks the PIN only
when it would remove a lock. Clearing a playlist's last lock keeps its
index stale until the store write has landed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- 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>
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>
- 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>
* 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>
- 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>
- 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>
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 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>
- 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>