diff --git a/src/lib/server/jobs/definitions/upgradeManager.ts b/src/lib/server/jobs/definitions/upgradeManager.ts index 35b2881..42b9a41 100644 --- a/src/lib/server/jobs/definitions/upgradeManager.ts +++ b/src/lib/server/jobs/definitions/upgradeManager.ts @@ -1,6 +1,7 @@ import { logger } from '$logger/logger.ts'; import { runUpgradeManager } from '../logic/upgradeManager.ts'; -import { notificationManager } from '$notifications/NotificationManager.ts'; +import { notify } from '$notifications/builder.ts'; +import { NotificationTypes } from '$notifications/types.ts'; import type { JobDefinition, JobResult } from '../types.ts'; /** @@ -80,25 +81,26 @@ export const upgradeManagerJob: JobDefinition = { messageLines.push(''); } - let notificationType: string; - let title: string; + const notificationType = + result.failureCount === 0 + ? NotificationTypes.UPGRADE_SUCCESS + : result.successCount === 0 + ? NotificationTypes.UPGRADE_FAILED + : NotificationTypes.UPGRADE_PARTIAL; - if (result.failureCount === 0) { - notificationType = 'upgrade.success'; - title = hasDryRun ? 'Upgrade Completed (Dry Run)' : 'Upgrade Completed'; - } else if (result.successCount === 0) { - notificationType = 'upgrade.failed'; - title = 'Upgrade Failed'; - } else { - notificationType = 'upgrade.partial'; - title = 'Upgrade Partially Completed'; - } + const title = + result.failureCount === 0 + ? hasDryRun + ? 'Upgrade Completed (Dry Run)' + : 'Upgrade Completed' + : result.successCount === 0 + ? 'Upgrade Failed' + : 'Upgrade Partially Completed'; - await notificationManager.notify({ - type: notificationType, - title, - message: messageLines.join('\n').trim(), - metadata: { + await notify(notificationType) + .title(title) + .lines(messageLines) + .meta({ successCount: result.successCount, failureCount: result.failureCount, dryRun: hasDryRun, @@ -108,8 +110,8 @@ export const upgradeManagerJob: JobDefinition = { searched: i.itemsSearched, items: i.items })) - } - }); + }) + .send(); } // Consider job failed only if all configs failed @@ -132,12 +134,11 @@ export const upgradeManagerJob: JobDefinition = { meta: { error: errorMessage } }); - await notificationManager.notify({ - type: 'upgrade.failed', - title: 'Upgrade Failed', - message: `Upgrade manager encountered an error: ${errorMessage}`, - metadata: { error: errorMessage } - }); + await notify(NotificationTypes.UPGRADE_FAILED) + .title('Upgrade Failed') + .message(`Upgrade manager encountered an error: ${errorMessage}`) + .meta({ error: errorMessage }) + .send(); return { success: false, diff --git a/src/lib/server/jobs/logic/syncDatabases.ts b/src/lib/server/jobs/logic/syncDatabases.ts index 110f495..6acb82b 100644 --- a/src/lib/server/jobs/logic/syncDatabases.ts +++ b/src/lib/server/jobs/logic/syncDatabases.ts @@ -4,7 +4,8 @@ */ import { pcdManager } from '$pcd/pcd.ts'; -import { notificationManager } from '$notifications/NotificationManager.ts'; +import { notify } from '$notifications/builder.ts'; +import { NotificationTypes } from '$notifications/types.ts'; export interface DatabaseSyncStatus { id: number; @@ -56,17 +57,11 @@ export async function syncDatabases(): Promise { const syncResult = await pcdManager.sync(db.id); if (syncResult.success) { - // Send success notification - await notificationManager.notify({ - type: 'pcd.sync_success', - title: 'Database Synced Successfully', - message: `Database "${db.name}" has been updated (${syncResult.commitsBehind} commit${syncResult.commitsBehind === 1 ? '' : 's'} pulled)`, - metadata: { - databaseId: db.id, - databaseName: db.name, - commitsPulled: syncResult.commitsBehind - } - }); + await notify(NotificationTypes.PCD_SYNC_SUCCESS) + .title('Database Synced Successfully') + .message(`Database "${db.name}" has been updated (${syncResult.commitsBehind} commit${syncResult.commitsBehind === 1 ? '' : 's'} pulled)`) + .meta({ databaseId: db.id, databaseName: db.name, commitsPulled: syncResult.commitsBehind }) + .send(); statuses.push({ id: db.id, @@ -76,17 +71,11 @@ export async function syncDatabases(): Promise { }); successCount++; } else { - // Send failure notification - await notificationManager.notify({ - type: 'pcd.sync_failed', - title: 'Database Sync Failed', - message: `Failed to sync database "${db.name}": ${syncResult.error}`, - metadata: { - databaseId: db.id, - databaseName: db.name, - error: syncResult.error - } - }); + await notify(NotificationTypes.PCD_SYNC_FAILED) + .title('Database Sync Failed') + .message(`Failed to sync database "${db.name}": ${syncResult.error}`) + .meta({ databaseId: db.id, databaseName: db.name, error: syncResult.error }) + .send(); statuses.push({ id: db.id, @@ -98,17 +87,12 @@ export async function syncDatabases(): Promise { failureCount++; } } else { - // Auto-pull is disabled, send notification - await notificationManager.notify({ - type: 'pcd.updates_available', - title: 'Database Updates Available', - message: `Updates are available for database "${db.name}" (${updateInfo.commitsBehind} commit${updateInfo.commitsBehind === 1 ? '' : 's'} behind)`, - metadata: { - databaseId: db.id, - databaseName: db.name, - commitsBehind: updateInfo.commitsBehind - } - }); + // Auto-pull is disabled, notify about available updates + await notify(NotificationTypes.PCD_UPDATES_AVAILABLE) + .title('Database Updates Available') + .message(`Updates are available for database "${db.name}" (${updateInfo.commitsBehind} commit${updateInfo.commitsBehind === 1 ? '' : 's'} behind)`) + .meta({ databaseId: db.id, databaseName: db.name, commitsBehind: updateInfo.commitsBehind }) + .send(); statuses.push({ id: db.id, diff --git a/src/lib/server/jobs/runner.ts b/src/lib/server/jobs/runner.ts index 5a7a160..c10d472 100644 --- a/src/lib/server/jobs/runner.ts +++ b/src/lib/server/jobs/runner.ts @@ -1,7 +1,8 @@ import { jobRegistry } from './registry.ts'; import { jobsQueries, jobRunsQueries } from '$db/queries/jobs.ts'; import { logger } from '$logger/logger.ts'; -import { notificationManager } from '../notifications/NotificationManager.ts'; +import { notify } from '$notifications/builder.ts'; +import { NotificationTypes } from '$notifications/types.ts'; import type { Job, JobResult } from './types.ts'; /** @@ -93,26 +94,16 @@ export async function runJob(job: Job): Promise { }); } - // Send notifications - const notificationType = result.success ? `job.${job.name}.success` : `job.${job.name}.failed`; - const notificationTitle = result.success - ? `${definition.description} - Success` - : `${definition.description} - Failed`; - const notificationMessage = result.success - ? result.output ?? 'Job completed successfully' - : result.error ?? 'Unknown error'; + // Send notification + const notificationType = result.success + ? NotificationTypes.jobSuccess(job.name) + : NotificationTypes.jobFailed(job.name); - await notificationManager.notify({ - type: notificationType, - title: notificationTitle, - message: notificationMessage, - metadata: { - jobId: job.id, - jobName: job.name, - durationMs, - timestamp: finishedAt - } - }); + await notify(notificationType) + .title(`${definition.description} - ${result.success ? 'Success' : 'Failed'}`) + .message(result.success ? (result.output ?? 'Job completed successfully') : (result.error ?? 'Unknown error')) + .meta({ jobId: job.id, jobName: job.name, durationMs, timestamp: finishedAt }) + .send(); // Save job run to database jobRunsQueries.create( diff --git a/src/lib/server/notifications/builder.ts b/src/lib/server/notifications/builder.ts new file mode 100644 index 0000000..40afdb6 --- /dev/null +++ b/src/lib/server/notifications/builder.ts @@ -0,0 +1,89 @@ +/** + * Fluent notification builder + * Provides a chainable API for constructing and sending notifications + */ + +import { notificationManager } from './NotificationManager.ts'; +import type { Notification } from './types.ts'; + +/** + * Builder class for constructing notifications + */ +class NotificationBuilder { + private data: Notification; + + constructor(type: string) { + this.data = { + type, + title: '', + message: '' + }; + } + + /** + * Set the notification title + */ + title(t: string): this { + this.data.title = t; + return this; + } + + /** + * Set the notification message + */ + message(m: string): this { + this.data.message = m; + return this; + } + + /** + * Build message from multiple lines + * Automatically filters out null/undefined/empty values + */ + lines(messageLines: (string | null | undefined | false)[]): this { + this.data.message = messageLines.filter(Boolean).join('\n').trim(); + return this; + } + + /** + * Set metadata + */ + meta(metadata: Record): this { + this.data.metadata = metadata; + return this; + } + + /** + * Send the notification + */ + async send(): Promise { + await notificationManager.notify(this.data); + } +} + +/** + * Create a new notification builder + * + * @example + * // Simple notification + * await notify('pcd.linked') + * .title('Database Linked') + * .message('Database "MyDB" has been linked successfully') + * .meta({ databaseId: 1 }) + * .send(); + * + * @example + * // Multi-line notification + * await notify('upgrade.success') + * .title('Upgrade Completed') + * .lines([ + * 'Filter: 50 matched → 30 after cooldown', + * 'Selection: 10/10 items', + * hasItems ? `Items: ${items.join(', ')}` : null + * ]) + * .meta({ instanceId: 1 }) + * .send(); + */ +export function notify(type: string): NotificationBuilder { + return new NotificationBuilder(type); +} diff --git a/src/lib/server/notifications/types.ts b/src/lib/server/notifications/types.ts index 08464f8..909291f 100644 --- a/src/lib/server/notifications/types.ts +++ b/src/lib/server/notifications/types.ts @@ -2,11 +2,32 @@ * Core notification types and interfaces */ +/** + * Type-safe notification type constants + */ +export const NotificationTypes = { + // Jobs (dynamic - constructed with job name) + jobSuccess: (jobName: string) => `job.${jobName}.success` as const, + jobFailed: (jobName: string) => `job.${jobName}.failed` as const, + + // PCD / Databases + PCD_LINKED: 'pcd.linked', + PCD_UNLINKED: 'pcd.unlinked', + PCD_UPDATES_AVAILABLE: 'pcd.updates_available', + PCD_SYNC_SUCCESS: 'pcd.sync_success', + PCD_SYNC_FAILED: 'pcd.sync_failed', + + // Upgrades + UPGRADE_SUCCESS: 'upgrade.success', + UPGRADE_PARTIAL: 'upgrade.partial', + UPGRADE_FAILED: 'upgrade.failed' +} as const; + /** * Notification payload sent to services */ export interface Notification { - type: string; // e.g., 'job.backup.success', 'job.cleanup.failed' + type: string; title: string; message: string; metadata?: Record; diff --git a/src/lib/server/pcd/pcd.ts b/src/lib/server/pcd/pcd.ts index e452e3a..190e60a 100644 --- a/src/lib/server/pcd/pcd.ts +++ b/src/lib/server/pcd/pcd.ts @@ -8,7 +8,8 @@ import type { DatabaseInstance } from '$db/queries/databaseInstances.ts'; import { loadManifest, type Manifest } from './manifest.ts'; import { getPCDPath } from './paths.ts'; import { processDependencies, syncDependencies } from './deps.ts'; -import { notificationManager } from '$notifications/NotificationManager.ts'; +import { notify } from '$notifications/builder.ts'; +import { NotificationTypes } from '$notifications/types.ts'; import { compile, invalidate, startWatch, getCache } from './cache.ts'; import { logger } from '$logger/logger.ts'; @@ -90,17 +91,11 @@ class PCDManager { } } - // Send notification - await notificationManager.notify({ - type: 'pcd.linked', - title: 'Database Linked', - message: `Database "${options.name}" has been linked successfully`, - metadata: { - databaseId: id, - databaseName: options.name, - repositoryUrl: options.repositoryUrl - } - }); + await notify(NotificationTypes.PCD_LINKED) + .title('Database Linked') + .message(`Database "${options.name}" has been linked successfully`) + .meta({ databaseId: id, databaseName: options.name, repositoryUrl: options.repositoryUrl }) + .send(); return instance; } catch (error) { @@ -140,17 +135,11 @@ class PCDManager { console.error(`Failed to remove PCD directory ${instance.local_path}:`, error); } - // Send notification - await notificationManager.notify({ - type: 'pcd.unlinked', - title: 'Database Unlinked', - message: `Database "${name}" has been removed`, - metadata: { - databaseId: id, - databaseName: name, - repositoryUrl: repository_url - } - }); + await notify(NotificationTypes.PCD_UNLINKED) + .title('Database Unlinked') + .message(`Database "${name}" has been removed`) + .meta({ databaseId: id, databaseName: name, repositoryUrl: repository_url }) + .send(); } /** diff --git a/src/routes/arr/[id]/upgrades/+page.server.ts b/src/routes/arr/[id]/upgrades/+page.server.ts index c3ca3de..b38ea9c 100644 --- a/src/routes/arr/[id]/upgrades/+page.server.ts +++ b/src/routes/arr/[id]/upgrades/+page.server.ts @@ -3,7 +3,8 @@ import type { Actions, ServerLoad } from '@sveltejs/kit'; import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; import { upgradeConfigsQueries } from '$db/queries/upgradeConfigs.ts'; import { logger } from '$logger/logger.ts'; -import { notificationManager } from '$notifications/NotificationManager.ts'; +import { notify } from '$notifications/builder.ts'; +import { NotificationTypes } from '$notifications/types.ts'; import type { FilterConfig, FilterMode } from '$lib/shared/filters.ts'; import { processUpgradeConfig } from '$lib/server/upgrades/processor.ts'; @@ -227,16 +228,15 @@ export const actions: Actions = { const dryRunLabel = result.config.dryRun ? ' [DRY RUN]' : ''; const itemsList = result.selection.items.map((i) => i.title).join(', '); - await notificationManager.notify({ - type: isSuccess ? 'upgrade.success' : 'upgrade.failed', - title: `${instance.name}: ${result.config.selectedFilter}${dryRunLabel}`, - message: [ + await notify(isSuccess ? NotificationTypes.UPGRADE_SUCCESS : NotificationTypes.UPGRADE_FAILED) + .title(`${instance.name}: ${result.config.selectedFilter}${dryRunLabel}`) + .lines([ `Filter: ${result.filter.matchedCount} matched → ${result.filter.afterCooldown} after cooldown`, `Selection: ${result.selection.actualCount}/${result.selection.requestedCount} items`, `Results: ${result.results.searchesTriggered} searches, ${result.results.successful} successful`, itemsList ? `Items: ${itemsList}` : null - ].filter(Boolean).join('\n'), - metadata: { + ]) + .meta({ instanceId: id, instanceName: instance.name, filterName: result.config.selectedFilter, @@ -244,8 +244,8 @@ export const actions: Actions = { matchedCount: result.filter.matchedCount, dryRun: result.config.dryRun, items: result.selection.items.map((i) => i.title) - } - }); + }) + .send(); return { success: true, @@ -267,17 +267,11 @@ export const actions: Actions = { meta: { instanceId: id, error: err } }); - await notificationManager.notify({ - type: 'upgrade.failed', - title: 'Upgrade Failed', - message: `${instance.name}: ${errorMessage}`, - metadata: { - instanceId: id, - instanceName: instance.name, - error: errorMessage, - dryRun: true - } - }); + await notify(NotificationTypes.UPGRADE_FAILED) + .title('Upgrade Failed') + .message(`${instance.name}: ${errorMessage}`) + .meta({ instanceId: id, instanceName: instance.name, error: errorMessage, dryRun: true }) + .send(); return fail(500, { error: 'Upgrade run failed. Check logs for details.' }); }