From e1b22e5309472dca583ee816917ac7f4b81643a2 Mon Sep 17 00:00:00 2001 From: Max Mayfield Date: Sun, 1 Mar 2026 04:14:26 +0000 Subject: [PATCH] Wire up remaining TODO stubs: P3 test notifications, P2 drift notification trigger - P3: test notification endpoint now instantiates real Slack/Email/Webhook notifiers - P2: drift processor triggers notification service when drift_score > 0 (non-fatal on failure) --- .../saas/src/processor/routes.ts | 17 ++++++++-- .../src/api/notifications.ts | 33 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/products/02-iac-drift-detection/saas/src/processor/routes.ts b/products/02-iac-drift-detection/saas/src/processor/routes.ts index 858a1f9..20bb147 100644 --- a/products/02-iac-drift-detection/saas/src/processor/routes.ts +++ b/products/02-iac-drift-detection/saas/src/processor/routes.ts @@ -56,8 +56,21 @@ export async function registerProcessorRoutes(app: FastifyInstance) { ); }); - // TODO: Trigger notification if drift_score > threshold - // TODO: Trigger remediation workflow if auto-remediate enabled + // Trigger notification if drift score exceeds threshold + if (report.drift_score > 0) { + try { + const { DriftNotificationService } = await import('../notifications/service.js'); + const notifService = new DriftNotificationService(pool); + await notifService.notifyDrift(tenantId, { + stackName: report.stack_name, + driftScore: report.drift_score, + totalResources: report.total_resources, + driftedResources: report.drifted_resources?.length ?? 0, + }); + } catch (err) { + app.log.warn({ tenantId, error: (err as Error).message }, 'Notification dispatch failed (non-fatal)'); + } + } return reply.status(201).send({ status: 'accepted' }); }); diff --git a/products/03-alert-intelligence/src/api/notifications.ts b/products/03-alert-intelligence/src/api/notifications.ts index 31fadcf..5c26758 100644 --- a/products/03-alert-intelligence/src/api/notifications.ts +++ b/products/03-alert-intelligence/src/api/notifications.ts @@ -50,8 +50,35 @@ export function registerNotificationRoutes(app: FastifyInstance) { const { channel } = req.params as { channel: string }; const tenantId = (req as any).tenantId; - // TODO: Send test notification via the configured channel - logger.info({ tenantId, channel }, 'Test notification sent'); - return { status: 'sent', channel }; + const configResult = await withTenant(tenantId, async (client) => { + return client.query('SELECT * FROM notification_configs WHERE channel = $1', [channel]); + }); + + const cfg = configResult.rows[0]; + if (!cfg) return reply.status(404).send({ error: `No ${channel} config found` }); + + const { SlackNotifier, EmailNotifier, WebhookNotifier } = await import('../notifications/dispatcher.js'); + const testIncident = { + incidentId: 'test-000', + title: '[TEST] dd0c/alert notification test', + severity: 'info', + service: 'test-service', + alertCount: 1, + firstAlertAt: new Date(), + fingerprint: 'test', + dashboardUrl: `https://alert.dd0c.dev/incidents/test-000`, + }; + + let sent = false; + if (channel === 'slack' && cfg.config?.slack_webhook_url) { + sent = await new SlackNotifier(cfg.config.slack_webhook_url).send(testIncident); + } else if (channel === 'email' && cfg.config?.email_to) { + sent = await new EmailNotifier(process.env.RESEND_API_KEY ?? '', 'alerts@dd0c.dev', cfg.config.email_to).send(testIncident); + } else if (channel === 'webhook' && cfg.config?.webhook_url) { + sent = await new WebhookNotifier(cfg.config.webhook_url).send(testIncident); + } + + logger.info({ tenantId, channel, sent }, 'Test notification sent'); + return { status: sent ? 'sent' : 'failed', channel }; }); }