feat(rpc): type and gate alarm trigger conditions
Summary
Addresses part of #958 — the "typed trigger conditions plus RPC validation" slice from the delivery plan, not the full issue. Evaluators, alarm_states, scheduling, and the dashboard picker/preview stay out of scope, pending the maintainer's answer to the issue's open architecture question ("where should the evaluator run?").
- Replaced
triggerConditions: z.record(z.string(), z.unknown())with a discriminated union ontriggerType, following the same pattern already used fordestinations(z.discriminatedUnion("type", [...])) in this file:uptime→{ monitorIds: string[] }(defaults to[]), matching what the code already expected informally (alarmMonitorIdsin the dashboard anduptime-transition-alerts.tsalready just checkArray.isArray(...)).traffic_spike/error_rate→ typed per the shapes proposed in the issue, so the contract is documented, but see below — they're not creatable yet.
alarms.createandalarms.updatenow reject any trigger type other thanuptimewith aBAD_REQUESTexplaining there's no evaluator for it yet, instead of silently accepting and persisting a dead alarm.updateresolves the effective trigger type/conditions against the existing row when only one of the two fields is sent (e.g. the monitor-linking flow inmonitor-sheet.tsx, which patchestriggerConditionswithouttriggerType).- Read paths (
alarms.list, thealarms.create/updateoutput schema) are unchanged and stay permissive (z.record), so any row written before this change — regardless of trigger type or conditions shape — stays listable. Only writes are tightened. - Added
packages/rpc/src/routers/alarms.test.tscovering: valid/defaulteduptimeconditions, malformeduptimeconditions, and rejection oftraffic_spike/error_rate/unknown types.
Verification
- Checked the dashboard:
alarm-sheet.tsxalways hardcodestriggerType: "uptime"on create and has no UI for selectingtraffic_spike/error_rate— there's nothing to remove, the tightened API doesn't break any existing UI flow. - Checked the create→link-monitor flow (
alarm-sheet.tsxcreates withtriggerConditions: {}, thenmonitor-sheet.tsxlater patches inmonitorIdsviaalarms.update): still works becausemonitorIdsdefaults to[]. - Ran
bun run check-types,bun run lint, andpackages/rpc's fullbun run test(223 pass) locally against Postgres/Redis/ClickHouse. The pre-push hook additionally ran the full monorepobun run test(1101+ pass across all packages) — all green. - Local dev DB was empty, so I couldn't inspect real stored alarm rows directly; reasoned through the code paths above instead (schema defaults handle the only row shape the app itself ever writes for
uptime).
Out of scope (left for follow-up PRs, per the issue's delivery plan)
- The actual
traffic_spike/error_rateevaluators,alarm_statestable, and BullMQ scheduling (step 2/3 in the issue). - The dashboard trigger-type picker and "would have fired" preview (step 4) — moot until an evaluator exists.
Disclosure: implemented with AI assistance (Claude Code); verified and tested by hand before submitting.
Summary by cubic
Types and gates alarm trigger conditions so alarms.create/update reject alarms nothing can evaluate. Previously any trigger type was accepted with untyped conditions, so traffic_spike and error_rate alarms saved but could never fire. Writes now validate triggerConditions against triggerType: uptime requires monitorIds: string[] (defaulting to []), and other types get a BAD_REQUEST. Webhook header blocking now reuses the shared FORBIDDEN_WEBHOOK_HEADERS export so alarms reject the headers delivery actually drops. Read paths stay permissive, so alarms written before this change remain listable. update resolves the effective trigger type and conditions against the stored row via a pure resolveTriggerUpdate helper, keeping single-field updates like the monitor-linking flow working. Adds packages/rpc/src/routers/alarms.test.ts covering defaulted and malformed uptime conditions, single-field resolution, and rejection of unsupported trigger types.