Archive milestone artifacts (roadmap, requirements, audit, phase directories) to .planning/milestones/. Evolve PROJECT.md with validated requirements and decision outcomes. Create MILESTONES.md and RETROSPECTIVE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.2 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-backend-services | 03 | execute | 2 |
|
|
true |
|
|
Purpose: ALRT-01 requires email alerts on service degradation/failure. ALRT-02 requires deduplication with cooldown. ALRT-04 requires the recipient to come from configuration, not hardcoded source code.
Output: alertService.ts with evaluateAndAlert() and sendAlertEmail(), and unit tests.
<execution_context> @/home/jonathan/.claude/get-shit-done/workflows/execute-plan.md @/home/jonathan/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-backend-services/02-RESEARCH.md @.planning/phases/01-data-foundation/01-01-SUMMARY.md @.planning/phases/02-backend-services/02-02-PLAN.md @backend/src/models/AlertEventModel.ts @backend/src/index.ts Task 1: Create alertService with deduplication and email backend/src/services/alertService.ts Create `backend/src/services/alertService.ts` with the following structure:Import the ProbeResult type from './healthProbeService' (created in Plan 02).
Constants:
ALERT_COOLDOWN_MINUTES = parseInt(process.env.ALERT_COOLDOWN_MINUTES ?? '60', 10)— configurable cooldown window
Private function createTransporter():
Create nodemailer transporter INSIDE function scope (not module level — PITFALL A: Firebase Secrets not available at module load). Read SMTP config from process.env:
host:process.env.EMAIL_HOST ?? 'smtp.gmail.com'port:parseInt(process.env.EMAIL_PORT ?? '587', 10)secure:process.env.EMAIL_SECURE === 'true'auth.user:process.env.EMAIL_USERauth.pass:process.env.EMAIL_PASS
Private function sendAlertEmail(serviceName, alertType, message):
- Read recipient from
process.env.EMAIL_WEEKLY_RECIPIENT(ALRT-04: NEVER hardcode the email address) - If no recipient configured, log warning and return (do not throw)
- Call
createTransporter()thentransporter.sendMail({ from, to, subject, text, html }) - Subject format:
[CIM Summary] Alert: ${serviceName} — ${alertType} - Wrap in try/catch — email failure logs error but does NOT throw (email failure must not break probe pipeline)
Exported function evaluateAndAlert(probeResults: ProbeResult[]):
For each ProbeResult where status is 'degraded' or 'down':
- Map status to alert_type: 'down' -> 'service_down', 'degraded' -> 'service_degraded'
- Call
AlertEventModel.findRecentByService(service_name, alert_type, ALERT_COOLDOWN_MINUTES) - If recent alert exists within cooldown, log suppression and skip BOTH row creation AND email (PITFALL 3: prevent alert storms)
- If no recent alert, create alert_events row via
AlertEventModel.create({ service_name, alert_type, message: error_message or status description }) - Then send email via
sendAlertEmail()
Export as: export const alertService = { evaluateAndAlert }.
Use Winston logger for all logging. Use import { AlertEventModel } from '../models/AlertEventModel'.
cd /home/jonathan/Coding/cim_summary/backend && npx tsc --noEmit --pretty 2>&1 | head -30
Verify alertService.ts exports alertService.evaluateAndAlert. Verify no hardcoded email addresses in source.
alertService.ts exports evaluateAndAlert(). Deduplication checks AlertEventModel.findRecentByService() before creating rows or sending email. Recipient read from process.env.EMAIL_WEEKLY_RECIPIENT. Transporter created lazily. Email failures caught and logged. TypeScript compiles.
Mock dependencies:
vi.mock('../../models/AlertEventModel')— mockfindRecentByServiceandcreatevi.mock('nodemailer')— mockcreateTransportreturning{ sendMail: vi.fn().mockResolvedValue({}) }vi.mock('../../utils/logger')— mock logger
Create test ProbeResult fixtures:
healthyProbe: { service_name: 'supabase', status: 'healthy', latency_ms: 50 }downProbe: { service_name: 'document_ai', status: 'down', latency_ms: 0, error_message: 'Connection refused' }degradedProbe: { service_name: 'llm_api', status: 'degraded', latency_ms: 6000 }
Test cases:
-
Healthy probes — no alerts sent — pass array of healthy ProbeResults, verify AlertEventModel.findRecentByService NOT called, sendMail NOT called
-
Down probe — creates alert_events row and sends email — pass downProbe, mock findRecentByService returning null (no recent alert), verify AlertEventModel.create called with service_name='document_ai' and alert_type='service_down', verify sendMail called
-
Degraded probe — creates alert with type 'service_degraded' — pass degradedProbe, mock findRecentByService returning null, verify AlertEventModel.create called with alert_type='service_degraded'
-
Deduplication — suppresses within cooldown — pass downProbe, mock findRecentByService returning an existing alert object (non-null), verify AlertEventModel.create NOT called, sendMail NOT called, logger.info called with 'suppress' in message
-
Recipient from env — reads process.env.EMAIL_WEEKLY_RECIPIENT — set
process.env.EMAIL_WEEKLY_RECIPIENT = 'test@example.com', pass downProbe with no recent alert, verify sendMail called withto: 'test@example.com' -
No recipient configured — skips email but still creates alert row — delete process.env.EMAIL_WEEKLY_RECIPIENT, pass downProbe with no recent alert, verify AlertEventModel.create IS called, sendMail NOT called, logger.warn called
-
Email failure — does not throw — mock sendMail to reject, verify evaluateAndAlert does not throw, verify logger.error called
-
Multiple probes — processes each independently — pass [downProbe, degradedProbe, healthyProbe], verify findRecentByService called twice (for down and degraded, not for healthy)
Use beforeEach(() => { vi.clearAllMocks(); process.env.EMAIL_WEEKLY_RECIPIENT = 'admin@test.com'; }).
cd /home/jonathan/Coding/cim_summary/backend && npx vitest run src/tests/unit/alertService.test.ts --reporter=verbose 2>&1
All alertService tests pass. Deduplication verified (suppresses within cooldown). Email sending verified with config-based recipient. Email failure verified as non-throwing. Multiple probe evaluation verified.
<success_criteria>
- alertService exports evaluateAndAlert(probeResults)
- Deduplication uses AlertEventModel.findRecentByService with configurable cooldown
- Alert rows created via AlertEventModel.create before email send
- Suppressed alerts skip BOTH row creation AND email
- Recipient from process.env, never hardcoded
- Transporter created inside function, not at module level
- Email failures caught and logged, never thrown
- All unit tests pass </success_criteria>