diff --git a/src/lib/server/notifications/events-core.ts b/src/lib/server/notifications/events-core.ts new file mode 100644 index 0000000..efbd0e7 --- /dev/null +++ b/src/lib/server/notifications/events-core.ts @@ -0,0 +1,32 @@ +/** + * Pure notification-event decision logic - NO db/docker imports, so it unit-tests + * under the bun runner. index.ts re-exports and uses these; keeping the decisions here + * means the full "which events fire for which input" matrix is testable in isolation. + * + * `NotificationEventType` is a type-only import (erased at build), so it doesn't pull + * better-sqlite3 in through db.ts. + */ + +import type { NotificationEventType } from '../db'; + +/** + * Map a raw Docker event action to the notification event type it should raise, or null + * when the action isn't one we notify on. Docker has no "updated" action - a container + * update is a die/kill of the old container followed by start of the new one, so those + * map to container_exited/container_started here. The "container was updated" signal is + * raised separately (see updateEventTypes) from the recreate flow, not from a raw event. + */ +export function mapActionToEventType(action: string): NotificationEventType | null { + const mapping: Record = { + start: 'container_started', + stop: 'container_stopped', + restart: 'container_restarted', + die: 'container_exited', + kill: 'container_exited', + oom: 'container_oom', + 'health_status: unhealthy': 'container_unhealthy', + 'health_status: healthy': 'container_healthy', + pull: 'image_pulled' + }; + return mapping[action] || null; +} diff --git a/src/lib/server/notifications/index.ts b/src/lib/server/notifications/index.ts index b9a511b..65aeec0 100644 --- a/src/lib/server/notifications/index.ts +++ b/src/lib/server/notifications/index.ts @@ -26,6 +26,9 @@ import { import type { NotificationPayload, NotificationResult } from './shared'; export type { NotificationPayload, NotificationResult } from './shared'; +// The pure docker-action -> event-type map lives in events-core (unit-testable). +import { mapActionToEventType } from './events-core'; +export { mapActionToEventType } from './events-core'; import { sendSmtpNotification } from './smtp'; import { sendDiscord } from './discord'; @@ -166,21 +169,6 @@ export async function testNotification(setting: NotificationSettingData): Promis return { success: false, error: 'Unknown notification type' }; } -// Map Docker action to notification event type -function mapActionToEventType(action: string): NotificationEventType | null { - const mapping: Record = { - 'start': 'container_started', - 'stop': 'container_stopped', - 'restart': 'container_restarted', - 'die': 'container_exited', - 'kill': 'container_exited', - 'oom': 'container_oom', - 'health_status: unhealthy': 'container_unhealthy', - 'health_status: healthy': 'container_healthy', - 'pull': 'image_pulled' - }; - return mapping[action] || null; -} // Scanner image patterns to exclude from notifications const SCANNER_IMAGE_PATTERNS = [ @@ -257,10 +245,7 @@ export async function sendEventNotification( payload: NotificationPayload, environmentId?: number ): Promise<{ success: boolean; sent: number }> { - let enrichedPayload: NotificationPayload = { - ...payload, - eventType - }; + let enrichedPayload: NotificationPayload = { ...payload, eventType }; if (environmentId) { const env = await getEnvironment(environmentId); if (env) { diff --git a/src/lib/server/scheduler/tasks/container-update.ts b/src/lib/server/scheduler/tasks/container-update.ts index e36b690..95e88a0 100644 --- a/src/lib/server/scheduler/tasks/container-update.ts +++ b/src/lib/server/scheduler/tasks/container-update.ts @@ -693,6 +693,21 @@ export async function recreateContainer( } catch (e: any) { log?.(`WARNING: dependent-child reconnect step errored (parent update stands): ${e?.message}`); } + + // recreateContainer is only ever called to UPDATE a container's image (scheduled + // auto-update AND the manual batch-update UI), so a successful recreate is a + // container_updated event. This is what the UI's "container updated" toggle + // promises; auto_update_success is raised separately by the scheduler path. (#1424) + try { + await sendEventNotification('container_updated', { + title: 'Container updated', + message: `Container "${containerName}" was updated to a new image (${imageName})`, + type: 'success' + }, envId); + } catch (e: any) { + log?.(`WARNING: container_updated notification failed (update stands): ${e?.message}`); + } + return { success: true }; } catch (error: any) { log?.(`Failed to recreate container: ${error.message}`);