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>
183 lines
9.2 KiB
Markdown
183 lines
9.2 KiB
Markdown
---
|
|
phase: 02-backend-services
|
|
plan: 03
|
|
type: execute
|
|
wave: 2
|
|
depends_on: [02-02]
|
|
files_modified:
|
|
- backend/src/services/alertService.ts
|
|
- backend/src/__tests__/unit/alertService.test.ts
|
|
autonomous: true
|
|
requirements: [ALRT-01, ALRT-02, ALRT-04]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "An alert email is sent when a probe returns 'degraded' or 'down'"
|
|
- "A second probe failure within the cooldown period does NOT send a duplicate email"
|
|
- "Alert recipient is read from process.env.EMAIL_WEEKLY_RECIPIENT, never hardcoded"
|
|
- "Email failure does not throw or break the probe pipeline"
|
|
- "Nodemailer transporter is created inside the function call, not at module level (Firebase Secret timing)"
|
|
- "An alert_events row is created before sending the email"
|
|
artifacts:
|
|
- path: "backend/src/services/alertService.ts"
|
|
provides: "Alert deduplication, email sending, and alert event creation"
|
|
exports: ["alertService"]
|
|
- path: "backend/src/__tests__/unit/alertService.test.ts"
|
|
provides: "Unit tests for alert deduplication, email sending, recipient config"
|
|
min_lines: 80
|
|
key_links:
|
|
- from: "backend/src/services/alertService.ts"
|
|
to: "backend/src/models/AlertEventModel.ts"
|
|
via: "findRecentByService() for deduplication, create() for alert row"
|
|
pattern: "AlertEventModel\\.(findRecentByService|create)"
|
|
- from: "backend/src/services/alertService.ts"
|
|
to: "nodemailer"
|
|
via: "createTransport + sendMail for email delivery"
|
|
pattern: "nodemailer\\.createTransport"
|
|
- from: "backend/src/services/alertService.ts"
|
|
to: "process.env.EMAIL_WEEKLY_RECIPIENT"
|
|
via: "Config-based recipient (ALRT-04)"
|
|
pattern: "process\\.env\\.EMAIL_WEEKLY_RECIPIENT"
|
|
---
|
|
|
|
<objective>
|
|
Create the alert service with deduplication logic, SMTP email sending via nodemailer, and config-based recipient.
|
|
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jonathan/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jonathan/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create alertService with deduplication and email</name>
|
|
<files>
|
|
backend/src/services/alertService.ts
|
|
</files>
|
|
<action>
|
|
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_USER`
|
|
- `auth.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()` then `transporter.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':
|
|
1. Map status to alert_type: 'down' -> 'service_down', 'degraded' -> 'service_degraded'
|
|
2. Call `AlertEventModel.findRecentByService(service_name, alert_type, ALERT_COOLDOWN_MINUTES)`
|
|
3. If recent alert exists within cooldown, log suppression and skip BOTH row creation AND email (PITFALL 3: prevent alert storms)
|
|
4. If no recent alert, create alert_events row via `AlertEventModel.create({ service_name, alert_type, message: error_message or status description })`
|
|
5. Then send email via `sendAlertEmail()`
|
|
|
|
Export as: `export const alertService = { evaluateAndAlert }`.
|
|
|
|
Use Winston logger for all logging. Use `import { AlertEventModel } from '../models/AlertEventModel'`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jonathan/Coding/cim_summary/backend && npx tsc --noEmit --pretty 2>&1 | head -30</automated>
|
|
<manual>Verify alertService.ts exports alertService.evaluateAndAlert. Verify no hardcoded email addresses in source.</manual>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create alertService unit tests</name>
|
|
<files>
|
|
backend/src/__tests__/unit/alertService.test.ts
|
|
</files>
|
|
<action>
|
|
Create `backend/src/__tests__/unit/alertService.test.ts` using the Vitest mock pattern.
|
|
|
|
Mock dependencies:
|
|
- `vi.mock('../../models/AlertEventModel')` — mock `findRecentByService` and `create`
|
|
- `vi.mock('nodemailer')` — mock `createTransport` returning `{ 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:
|
|
|
|
1. **Healthy probes — no alerts sent** — pass array of healthy ProbeResults, verify AlertEventModel.findRecentByService NOT called, sendMail NOT called
|
|
|
|
2. **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
|
|
|
|
3. **Degraded probe — creates alert with type 'service_degraded'** — pass degradedProbe, mock findRecentByService returning null, verify AlertEventModel.create called with alert_type='service_degraded'
|
|
|
|
4. **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
|
|
|
|
5. **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 with `to: 'test@example.com'`
|
|
|
|
6. **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
|
|
|
|
7. **Email failure — does not throw** — mock sendMail to reject, verify evaluateAndAlert does not throw, verify logger.error called
|
|
|
|
8. **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'; })`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jonathan/Coding/cim_summary/backend && npx vitest run src/__tests__/unit/alertService.test.ts --reporter=verbose 2>&1</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `npx tsc --noEmit` passes
|
|
2. `npx vitest run src/__tests__/unit/alertService.test.ts` — all tests pass
|
|
3. `grep -r 'jpressnell\|bluepoint' backend/src/services/alertService.ts` returns nothing (no hardcoded emails)
|
|
4. alertService reads recipient from `process.env.EMAIL_WEEKLY_RECIPIENT`
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-backend-services/02-03-SUMMARY.md`
|
|
</output>
|