Skip to content
This repository was archived by the owner on May 10, 2026. It is now read-only.
/ Maintainerr Public archive
forked from Maintainerr/Maintainerr

Commit 44d5cbc

Browse files
committed
refactor(overlays): tighten phase 1 review fixes
- Promote process result + request schema to @maintainerr/contracts so server and UI share one wire shape; drop the duplicate UI interface (skipped was incorrectly optional) and the inline zod schema in the controller. - Remove the appliedMediaItems recursion sentinel from the public processCollection signature so the lock check can't be bypassed by a truthy empty array; processAllCollections now calls the internal helper directly. - Gate DELETE /overlays/reset against concurrent processing and add spec coverage for both reset-while-disabled and reset-while-running. - Surface the skipped count in the inline overlay summary alert. - Replace the TriggerRuleButton re-export shim with a direct import of TriggerRuleActionButton at the one usage site, and revert the no-op canTestMedia change on CollectionDetailPage.
1 parent 78d49d8 commit 44d5cbc

14 files changed

Lines changed: 64 additions & 152 deletions

File tree

apps/server/src/modules/overlays/overlay-processor.service.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,7 @@ describe('OverlayProcessorService', () => {
247247

248248
jest.spyOn(service, 'applyTemplateOverlay').mockResolvedValue(true);
249249

250-
const result = await service.processCollection(
251-
collection as any,
252-
undefined,
253-
true,
254-
);
250+
const result = await service.processCollection(collection as any, true);
255251

256252
expect(service.applyTemplateOverlay).toHaveBeenCalledWith(
257253
'media-1',

apps/server/src/modules/overlays/overlay-processor.service.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
MaintainerrEvent,
3+
OverlayProcessorRunResult,
34
OverlayResult,
45
OverlayTemplate,
56
OverlayTemplateMode,
@@ -26,12 +27,7 @@ import { IOverlayProvider } from './providers/overlay-provider.interface';
2627

2728
export type ProcessorStatus = 'idle' | 'running' | 'error';
2829

29-
export interface ProcessorRunResult {
30-
processed: number;
31-
reverted: number;
32-
skipped: number;
33-
errors: number;
34-
}
30+
export type ProcessorRunResult = OverlayProcessorRunResult;
3531

3632
type RevertItemResult = 'restored' | 'failed' | 'no-backup' | 'item-gone';
3733

@@ -368,17 +364,8 @@ export class OverlayProcessorService {
368364

369365
async processCollection(
370366
collection: Collection & { collectionMedia: CollectionMedia[] },
371-
appliedMediaItems?: { mediaServerId: string }[],
372367
force = false,
373368
): Promise<ProcessorRunResult> {
374-
if (appliedMediaItems) {
375-
return this.processCollectionInternal(
376-
collection,
377-
appliedMediaItems,
378-
force,
379-
);
380-
}
381-
382369
if (this.status === 'running') {
383370
this.logger.warn('Overlay processor is already running, skipping');
384371
return this.createEmptyResult();
@@ -487,7 +474,7 @@ export class OverlayProcessorService {
487474
this.logger.log(
488475
`--- Processing: "${coll.title}" (${coll.collectionMedia.length} items) ---`,
489476
);
490-
const collResult = await this.processCollection(
477+
const collResult = await this.processCollectionInternal(
491478
coll,
492479
appliedMediaItems,
493480
force,

apps/server/src/modules/overlays/overlays.controller.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe('OverlaysController', () => {
4646
status: 'idle' | 'running' | 'error';
4747
processAllCollections: jest.Mock;
4848
processCollection: jest.Mock;
49+
resetAllOverlays: jest.Mock;
4950
};
5051
let collectionsService: {
5152
getCollection: jest.Mock;
@@ -65,6 +66,7 @@ describe('OverlaysController', () => {
6566
status: 'idle',
6667
processAllCollections: jest.fn(),
6768
processCollection: jest.fn(),
69+
resetAllOverlays: jest.fn(),
6870
};
6971
collectionsService = {
7072
getCollection: jest.fn(),
@@ -336,4 +338,23 @@ describe('OverlaysController', () => {
336338

337339
expect(processorService.processCollection).toHaveBeenCalledWith(collection);
338340
});
341+
342+
it('allows reset while overlays are globally disabled', async () => {
343+
processorService.resetAllOverlays.mockResolvedValue(undefined);
344+
345+
await expect(controller.resetAll()).resolves.toEqual({ success: true });
346+
347+
expect(processorService.resetAllOverlays).toHaveBeenCalled();
348+
});
349+
350+
it('rejects reset with 409 while a processor run is in progress', async () => {
351+
processorService.status = 'running';
352+
353+
await expect(controller.resetAll()).rejects.toMatchObject({
354+
status: 409,
355+
response: 'Overlay processing is already running',
356+
});
357+
358+
expect(processorService.resetAllOverlays).not.toHaveBeenCalled();
359+
});
339360
});

apps/server/src/modules/overlays/overlays.controller.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import * as fs from 'fs';
4141
import { ZodValidationPipe } from 'nestjs-zod';
4242
import * as path from 'path';
4343
import sharp from 'sharp';
44-
import { z } from 'zod';
44+
import {
45+
type OverlayProcessRequest,
46+
overlayProcessRequestSchema,
47+
} from '@maintainerr/contracts';
4548
import { dataDir as configDataDir } from '../../app/config/dataDir';
4649
import { MediaServerSetupGuard } from '../api/media-server/guards/media-server-setup.guard';
4750
import { CollectionsService } from '../collections/collections.service';
@@ -53,14 +56,6 @@ import { OverlayTemplateService } from './overlay-template.service';
5356
import { OverlayProviderFactory } from './providers/overlay-provider.factory';
5457
import { IOverlayProvider } from './providers/overlay-provider.interface';
5558

56-
const overlayProcessBodySchema = z.object({
57-
force: z.boolean().optional(),
58-
});
59-
60-
const overlayProcessRequestSchema = overlayProcessBodySchema.default({});
61-
62-
type OverlayProcessBody = z.infer<typeof overlayProcessBodySchema>;
63-
6459
@Controller('api/overlays')
6560
@UseGuards(MediaServerSetupGuard)
6661
export class OverlaysController {
@@ -194,7 +189,7 @@ export class OverlaysController {
194189
@Post('process')
195190
async processAll(
196191
@Body(new ZodValidationPipe(overlayProcessRequestSchema))
197-
request: OverlayProcessBody,
192+
request: OverlayProcessRequest,
198193
) {
199194
if (this.processorService.status === 'running') {
200195
throw new HttpException(
@@ -246,6 +241,12 @@ export class OverlaysController {
246241

247242
@Delete('reset')
248243
async resetAll() {
244+
if (this.processorService.status === 'running') {
245+
throw new HttpException(
246+
'Overlay processing is already running',
247+
HttpStatus.CONFLICT,
248+
);
249+
}
249250
await this.processorService.resetAllOverlays();
250251
return { success: true };
251252
}

apps/ui/src/api/overlays.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
OverlayLibrarySection,
33
OverlayPreviewItem,
4+
OverlayProcessorRunResult,
45
OverlaySettings,
56
OverlaySettingsUpdate,
67
OverlayTemplate,
@@ -22,13 +23,6 @@ import GetApiHandler, {
2223
PutApiHandler,
2324
} from '../utils/ApiHandler'
2425

25-
export interface OverlayProcessResult {
26-
processed: number
27-
reverted: number
28-
skipped?: number
29-
errors: number
30-
}
31-
3226
export const getOverlaySettings = () =>
3327
GetApiHandler<OverlaySettings>('/overlays/settings')
3428

@@ -144,7 +138,7 @@ export const deleteOverlayImage = (imageName: string) =>
144138
)
145139

146140
export const processAllOverlays = (options?: { force?: boolean }) =>
147-
PostApiHandler<OverlayProcessResult>(
141+
PostApiHandler<OverlayProcessorRunResult>(
148142
'/overlays/process',
149143
options?.force ? { force: true } : {},
150144
)

apps/ui/src/components/Collection/CollectionDetail/TriggerRuleButton/index.spec.tsx

Lines changed: 0 additions & 105 deletions
This file was deleted.

apps/ui/src/components/Collection/CollectionDetail/TriggerRuleButton/index.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/ui/src/components/Common/MediaCard/MediaModal/index.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import GetApiHandler from '../../../../utils/ApiHandler'
1313
import { clearMaintainerrStatusDetailsCache } from '../maintainerrStatus'
1414
import MediaModal from './index'
1515

16-
vi.mock('../../../Collection/CollectionDetail/TriggerRuleButton', () => ({
16+
vi.mock('../../../Collection/CollectionDetail/TriggerRuleActionButton', () => ({
1717
default: () => <div>trigger-rule-action</div>,
1818
}))
1919

apps/ui/src/components/Common/MediaCard/MediaModal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
rememberMaintainerrStatusDetails,
2424
} from '../maintainerrStatus'
2525
import type { ICollection } from '../../../Collection'
26-
import TriggerRuleButton from '../../../Collection/CollectionDetail/TriggerRuleButton'
26+
import TriggerRuleButton from '../../../Collection/CollectionDetail/TriggerRuleActionButton'
2727

2828
interface ModalContentProps {
2929
onClose: () => void

apps/ui/src/components/Settings/Overlays/index.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('OverlaySettings', () => {
4949
processAllOverlays.mockResolvedValue({
5050
processed: 3,
5151
reverted: 1,
52+
skipped: 2,
5253
errors: 0,
5354
})
5455
resetAllOverlays.mockResolvedValue({ success: true })
@@ -70,7 +71,9 @@ describe('OverlaySettings', () => {
7071
})
7172

7273
expect(
73-
await screen.findByText('Processed: 3, Reverted: 1, Errors: 0'),
74+
await screen.findByText(
75+
'Processed: 3, Reverted: 1, Skipped: 2, Errors: 0',
76+
),
7477
).toBeTruthy()
7578
})
7679

0 commit comments

Comments
 (0)