mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat(notifications): refactor notification system to use a fluent builder pattern
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<SyncDatabasesResult> {
|
||||
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<SyncDatabasesResult> {
|
||||
});
|
||||
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<SyncDatabasesResult> {
|
||||
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,
|
||||
|
||||
@@ -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<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
// 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(
|
||||
|
||||
89
src/lib/server/notifications/builder.ts
Normal file
89
src/lib/server/notifications/builder.ts
Normal file
@@ -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<string, unknown>): this {
|
||||
this.data.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification
|
||||
*/
|
||||
async send(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user