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>
The synchronous relock step now clears a selected Xtream item whose
category the visible category list cannot place (a manually hidden
category opened through search), instead of leaving it usable until the
awaited reloads and lookup finish.
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>
- 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>