From e9afb56017efdba977bfe4727ea51b8266e64af7 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 27 Sep 2026 13:20:05 +0200 Subject: [PATCH] fix(settings): clear an Xtream detail from a hidden category synchronously on relock 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 --- .../parental-lock-enforcement.service.spec.ts | 27 +++++++++++++++++++ .../parental-lock-enforcement.service.ts | 9 ++++--- docs/architecture/parental-lock.md | 4 ++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/services/parental-lock-enforcement.service.spec.ts b/apps/web/src/app/services/parental-lock-enforcement.service.spec.ts index a9126df48..013cfe1d5 100644 --- a/apps/web/src/app/services/parental-lock-enforcement.service.spec.ts +++ b/apps/web/src/app/services/parental-lock-enforcement.service.spec.ts @@ -349,6 +349,33 @@ describe('ParentalLockEnforcementService', () => { expect(xtreamStore.refreshSearchResults).toHaveBeenCalled(); }); + it('clears on relock, synchronously, a detail whose category is not in the visible list', () => { + // Opened through search from a manually hidden category: the + // on-screen list cannot place it, and the unfiltered lookup is + // an awaited read that may hang. + router.url = '/workspace/xtreams/xtream-1/vod/55/900'; + xtreamStore.selectedItem.set({ category_id: 55 }); + + service.failClosedNow(); + + expect(xtreamStore.setSelectedItem).toHaveBeenCalledWith(null); + expect(router.navigate).toHaveBeenCalledWith([ + '/workspace', + 'xtreams', + 'xtream-1', + 'vod', + ]); + }); + + it('keeps a detail on relock whose visible category is not locked', () => { + router.url = '/workspace/xtreams/xtream-1/vod/7/900'; + xtreamStore.selectedItem.set({ category_id: 7 }); + + service.failClosedNow(); + + expect(xtreamStore.setSelectedItem).not.toHaveBeenCalled(); + }); + it('fails closed synchronously on relock: detail, catalog and search', () => { router.url = '/workspace/xtreams/xtream-1/vod/42'; lockProvider(70); diff --git a/apps/web/src/app/services/parental-lock-enforcement.service.ts b/apps/web/src/app/services/parental-lock-enforcement.service.ts index 1afde7747..5d005e2dd 100644 --- a/apps/web/src/app/services/parental-lock-enforcement.service.ts +++ b/apps/web/src/app/services/parental-lock-enforcement.service.ts @@ -223,8 +223,10 @@ export class ParentalLockEnforcementService { * Clears a selected Xtream item whose category the lock store already * says is locked. Electron rows carry the SQLite category row id; the * category list still on screen maps it to the provider id the store is - * keyed by, the PWA carries the provider id directly. An item that - * cannot be placed is left to the post-reload check. + * keyed by, the PWA carries the provider id directly. An item whose + * category is not in that list (a manually hidden category, opened + * through search) cannot be judged without an awaited read that may + * hang, so it is cleared too: fail closed. */ private stepOffLockedXtreamSelection( playlistId: string, @@ -249,7 +251,8 @@ export class ParentalLockEnforcementService { ) as { xtream_id?: number; category_id?: string } | undefined; const providerId = Number(category?.xtream_id ?? category?.category_id); if ( - !Number.isFinite(providerId) || + category && + Number.isFinite(providerId) && !this.parentalLock.isXtreamCategoryLocked( playlistId, categoryType, diff --git a/docs/architecture/parental-lock.md b/docs/architecture/parental-lock.md index 31a83928b..d1621a26a 100644 --- a/docs/architecture/parental-lock.md +++ b/docs/architecture/parental-lock.md @@ -188,7 +188,9 @@ on either side. database answers — ahead of the serialized apply queue, so an earlier apply still waiting on a slow or hung read cannot delay it: the selected detail is stepped off synchronously when the lock store already names its category (the pre-reload category list - maps Electron's row id to the provider id), then `withholdCatalog()` + maps Electron's row id to the provider id) or when that list cannot place + it at all (a manually hidden category opened through search), then + `withholdCatalog()` empties every catalog list and `clearSearchResults()` the stored search (retiring a search still in flight, which was issued under the previous lock state) before the filtered reads refill them; both reloads take a publish guard