fix(settings): fail closed for direct worker title lookups and capture the Stalker lock dialog context before the PIN

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>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 14:43:54 +02:00
1 parent b5ba77b67b
commit 4ddacab663
7 files changed
+143 -18

No files matched your search

@@ -966,6 +966,35 @@ describe('WorkspaceContextPanelComponent', () => {
);
});
it('opens nothing when the route changed while the PIN was asked', async () => {
enableLock();
fixture.componentRef.setInput('context', {
provider: 'stalker',
playlistId: 'stalker-1',
});
fixture.componentRef.setInput('section', 'itv');
fixture.detectChanges();
const parentalLock = TestBed.inject(ParentalLockService);
jest.spyOn(parentalLock, 'requestUnlock').mockImplementation(
async () => {
fixture.componentRef.setInput('context', {
provider: 'xtreams',
playlistId: 'xtream-2',
});
return true;
}
);
const component = fixture.componentInstance;
component.loadStalkerLockDialog = jest.fn(
async () => class DialogStub {}
);
await component.openManageStalkerCategories();
expect(component.loadStalkerLockDialog).not.toHaveBeenCalled();
expect(dialog.open).not.toHaveBeenCalled();
});
it('opens nothing when the route changed during the lazy import', async () => {
enableLock();
fixture.componentRef.setInput('context', {
@@ -533,26 +533,30 @@ export class WorkspaceContextPanelComponent {
if (!this.canManageStalkerCategories()) {
return;
}
if (!(await this.parentalLock.requestUnlock())) {
return;
}
// Captured before any await (the PIN prompt loads lazily on first
// use) and re-checked after each: the store follows the CURRENT
// route, and another portal's categories must not reach a dialog
// that saves them under this playlist.
const context = this.context();
const section = this.section();
const contentType = toParentalLockStalkerCategoryType(section);
if (!contentType) {
const unchanged = (): boolean =>
this.context().playlistId === context.playlistId &&
this.context().provider === context.provider &&
this.section() === section &&
this.canManageStalkerCategories();
if (!contentType || !(await this.parentalLock.requestUnlock())) {
return;
}
// Snapshot before the lazy import: the store follows the CURRENT
// route, so reading it afterwards could hand another portal's
// categories to a dialog that saves them under this playlist.
if (!unchanged()) {
return;
}
// Snapshot before the lazy import, for the same reason.
const categories = this.stalkerStore
.getAllCategoriesForSelectedType()
.filter((category) => String(category.category_id) !== '*');
const dialogComponent = await this.loadStalkerLockDialog();
if (
this.context().playlistId !== context.playlistId ||
this.section() !== section
) {
if (!unchanged()) {
return;
}
this.dialog.open(dialogComponent, {