diff --git a/code/frontend/src/app/core/feature-badges/feature-registry.ts b/code/frontend/src/app/core/feature-badges/feature-registry.ts index 4d04ab53..3ac063be 100644 --- a/code/frontend/src/app/core/feature-badges/feature-registry.ts +++ b/code/frontend/src/app/core/feature-badges/feature-registry.ts @@ -59,4 +59,5 @@ export const NEW_FEATURES: NewFeature[] = [ { id: 'seeker-search-specials' }, { id: 'seeker-search-anime' }, { id: 'seeker-reset-cycle' }, + { id: 'seeker-cycle-notification' }, ]; diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index 28bba0b6..73b79816 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -156,6 +156,7 @@ export class DocumentationService { 'onSearchItemGrabbed': 'event-configuration', 'onForceImported': 'event-configuration', 'onErroredStrike': 'event-configuration', + 'onSeekerCycleCompleted': 'event-configuration', }, 'notifications/notifiarr': { 'apiKey': 'api-key', diff --git a/code/frontend/src/app/features/events/events-stats-card/events-stats-card.component.ts b/code/frontend/src/app/features/events/events-stats-card/events-stats-card.component.ts index 145cf3da..3954d433 100644 --- a/code/frontend/src/app/features/events/events-stats-card/events-stats-card.component.ts +++ b/code/frontend/src/app/features/events/events-stats-card/events-stats-card.component.ts @@ -33,6 +33,7 @@ const TYPE_COLORS: Record = { [EventType.DownloadCleaned]: '#22c55e', [EventType.DownloadStopped]: '#6366f1', [EventType.StrikeReset]: '#10b981', + [EventType.SeekerCycleCompleted]: '#a855f7', }; const FALLBACK_COLOR = '#94a3b8'; diff --git a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.html b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.html index 18896d03..545396c3 100644 --- a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.html +++ b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.html @@ -220,6 +220,10 @@ + diff --git a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.spec.ts b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.spec.ts index e409bdc6..e365ea44 100644 --- a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.spec.ts +++ b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.spec.ts @@ -26,6 +26,7 @@ const DEFAULT_EVENTS = { onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }; const NTFY_PROVIDER: NotificationProviderDto = { @@ -45,6 +46,7 @@ const NTFY_PROVIDER: NotificationProviderDto = { onSearchItemGrabbed: true, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }, configuration: { serverUrl: 'https://ntfy.example.com', @@ -690,6 +692,30 @@ describe('NotificationProviderModalComponent', () => { expect(onSaved).toHaveBeenCalledTimes(1); }); + it('renders the Seeker Cycle Completed toggle and sends its value on save', () => { + const { fixture, component, api } = setup({ initialType: NotificationProviderType.Discord }); + + const toggle = fixture.nativeElement.querySelector( + '.event-flags button[aria-label="Seeker Cycle Completed"]', + ) as HTMLButtonElement; + expect(toggle).not.toBeNull(); + expect(toggle.getAttribute('aria-checked')).toBe('false'); + + toggle.click(); + fixture.detectChanges(); + expect(toggle.getAttribute('aria-checked')).toBe('true'); + expect(component.modalModel().onSeekerCycleCompleted).toBe(true); + + component.modalModel.update((m) => ({ ...m, name: 'Alerts', webhookUrl: 'https://discord.com/hook' })); + component.saveProvider(); + fixture.detectChanges(); + + expect(api.create).toHaveBeenCalledWith( + NOTIFICATION_PROVIDER_DESCRIPTORS[NotificationProviderType.Discord], + expect.objectContaining({ onSeekerCycleCompleted: true }), + ); + }); + it('updates an existing ntfy provider with the loaded configuration', () => { const { fixture, component, api, toast, onSaved } = setup({ editingProvider: NTFY_PROVIDER }); diff --git a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.ts b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.ts index f0af2581..dba6fe8b 100644 --- a/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.ts +++ b/code/frontend/src/app/features/settings/notifications/notification-provider-modal.component.ts @@ -65,6 +65,7 @@ function createDefaultModalModel(): NotificationProviderFormModel { onFailedImportStrike: false, onStalledStrike: false, onSlowStrike: false, onQueueItemDeleted: false, onDownloadCleaned: false, onDownloadStopped: false, onCategoryChanged: false, onSearchTriggered: false, onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }; } @@ -358,6 +359,7 @@ export class NotificationProviderModalComponent { model.onSearchItemGrabbed = provider.events.onSearchItemGrabbed; model.onForceImported = provider.events.onForceImported; model.onErroredStrike = provider.events.onErroredStrike; + model.onSeekerCycleCompleted = provider.events.onSeekerCycleCompleted; return model; } diff --git a/code/frontend/src/app/features/settings/notifications/notifications.component.html b/code/frontend/src/app/features/settings/notifications/notifications.component.html index d813571f..a9c1cd77 100644 --- a/code/frontend/src/app/features/settings/notifications/notifications.component.html +++ b/code/frontend/src/app/features/settings/notifications/notifications.component.html @@ -68,6 +68,9 @@ @if (provider.events.onSearchItemGrabbed) { Search Item Grabbed } + @if (provider.events.onSeekerCycleCompleted) { + Seeker Cycle Completed + } } @empty { diff --git a/code/frontend/src/app/features/settings/notifications/notifications.component.spec.ts b/code/frontend/src/app/features/settings/notifications/notifications.component.spec.ts index bed73783..b7d8982b 100644 --- a/code/frontend/src/app/features/settings/notifications/notifications.component.spec.ts +++ b/code/frontend/src/app/features/settings/notifications/notifications.component.spec.ts @@ -27,6 +27,7 @@ const DISCORD_PROVIDER: NotificationProviderDto = { onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }, configuration: { webhookUrl: 'https://discord.com/api/webhooks/abc' }, }; @@ -48,6 +49,7 @@ const TELEGRAM_PROVIDER: NotificationProviderDto = { onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }, configuration: { botToken: 'token', chatId: '-100' }, }; diff --git a/code/frontend/src/app/shared/models/enums.ts b/code/frontend/src/app/shared/models/enums.ts index aa1fbfe6..59ec4862 100644 --- a/code/frontend/src/app/shared/models/enums.ts +++ b/code/frontend/src/app/shared/models/enums.ts @@ -206,4 +206,5 @@ export enum EventType { StrikeReset = 'StrikeReset', ForceImported = 'ForceImported', DownloadStopped = 'DownloadStopped', + SeekerCycleCompleted = 'SeekerCycleCompleted', } diff --git a/code/frontend/src/app/shared/models/notification-provider.model.ts b/code/frontend/src/app/shared/models/notification-provider.model.ts index ea34fde0..cd7fd418 100644 --- a/code/frontend/src/app/shared/models/notification-provider.model.ts +++ b/code/frontend/src/app/shared/models/notification-provider.model.ts @@ -18,6 +18,7 @@ export interface NotificationEventFlags { onSearchItemGrabbed: boolean; onForceImported: boolean; onErroredStrike: boolean; + onSeekerCycleCompleted: boolean; } export interface NotificationProviderDto { diff --git a/code/frontend/src/app/shared/utils/event-display.util.spec.ts b/code/frontend/src/app/shared/utils/event-display.util.spec.ts index 00b86bd4..911ce804 100644 --- a/code/frontend/src/app/shared/utils/event-display.util.spec.ts +++ b/code/frontend/src/app/shared/utils/event-display.util.spec.ts @@ -12,6 +12,7 @@ describe('eventTypeSeverity', () => { it.each([ [EventType.StrikeReset, 'success'], [EventType.DownloadCleaned, 'success'], + [EventType.SeekerCycleCompleted, 'success'], [EventType.FailedImportStrike, 'error'], [EventType.QueueItemDeleted, 'error'], [EventType.StalledStrike, 'warning'], @@ -78,6 +79,7 @@ describe('eventIcon', () => { [EventType.DownloadCleaned, 'tablerDownload'], [EventType.QueueItemDeleted, 'tablerTrash'], [EventType.CategoryChanged, 'tablerTag'], + [EventType.SeekerCycleCompleted, 'tablerSearch'], [EventType.DownloadStopped, 'tablerCircle'], ])('maps %s to %s', (eventType, expected) => { expect(eventIcon(eventType)).toBe(expected); diff --git a/code/frontend/src/app/shared/utils/event-display.util.ts b/code/frontend/src/app/shared/utils/event-display.util.ts index 04d4bde8..e154a75d 100644 --- a/code/frontend/src/app/shared/utils/event-display.util.ts +++ b/code/frontend/src/app/shared/utils/event-display.util.ts @@ -17,6 +17,7 @@ export function eventTypeSeverity(eventType: string): BadgeSeverity { case EventType.StrikeReset: case EventType.DownloadCleaned: case EventType.ForceImported: + case EventType.SeekerCycleCompleted: return 'success'; case EventType.FailedImportStrike: case EventType.QueueItemDeleted: @@ -77,6 +78,8 @@ export function eventIcon(eventType: string): string { return 'tablerTrash'; case EventType.CategoryChanged: return 'tablerTag'; + case EventType.SeekerCycleCompleted: + return 'tablerSearch'; default: return 'tablerCircle'; } diff --git a/code/frontend/src/app/shared/utils/notification-provider.descriptors.spec.ts b/code/frontend/src/app/shared/utils/notification-provider.descriptors.spec.ts index 2c7b782d..d9c0769a 100644 --- a/code/frontend/src/app/shared/utils/notification-provider.descriptors.spec.ts +++ b/code/frontend/src/app/shared/utils/notification-provider.descriptors.spec.ts @@ -56,6 +56,7 @@ function baseModel(overrides: Partial = {}): Noti onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, ...overrides, }; } @@ -72,6 +73,7 @@ const eventFlags = { onSearchItemGrabbed: false, onForceImported: false, onErroredStrike: false, + onSeekerCycleCompleted: false, }; describe('NOTIFICATION_PROVIDER_DESCRIPTORS', () => { diff --git a/code/frontend/src/app/shared/utils/notification-provider.descriptors.ts b/code/frontend/src/app/shared/utils/notification-provider.descriptors.ts index a36dd6b8..a940dfab 100644 --- a/code/frontend/src/app/shared/utils/notification-provider.descriptors.ts +++ b/code/frontend/src/app/shared/utils/notification-provider.descriptors.ts @@ -88,6 +88,7 @@ function eventFlags(m: NotificationProviderFormModel): NotificationEventFlags { onSearchItemGrabbed: m.onSearchItemGrabbed, onForceImported: m.onForceImported, onErroredStrike: m.onErroredStrike, + onSeekerCycleCompleted: m.onSeekerCycleCompleted, }; }