dev_r_incidents — Operations Guide

p24-infra DevOps tooling table. Tracks infrastructure alerts from first fire through acknowledgement to resolution. Not the fleet/WhatsApp incidents table — that is a separate domain.


1. What This Table Tracks

dev_r_incidents is the source of truth for infra-level incidents: monitoring alert fires, audit-engine failures, exporter outages, credential expiry warnings, etc.

It sits alongside dev_r_services and dev_r_projects as a public-access DevOps tooling table (anon read/write, RLS enabled with public ALL policy).


2. Schema Reference

ColumnTypeNotes
idUUIDPK, auto-generated
service_nameTEXTe.g. 'prometheus', 'audit-engine'
titleTEXTShort alert description
severityTEXT'P1' / 'P2' / 'P3'
statusTEXTLegacy field — use resolution_status for lifecycle
sourceTEXT'alertmanager' / 'audit-engine' / 'manual'
opened_atTIMESTAMPTZWhen alert first fired
resolved_atTIMESTAMPTZWhen confirmed resolved (drives MTTR)
linked_issue_urlTEXTGitHub issue URL if one was created
notesTEXTFree-form notes
raw_payloadJSONBOriginal alert payload
created_atTIMESTAMPTZRow creation time
updated_atTIMESTAMPTZLast modification time
mttr_minutesNUMERICComputed MTTR in minutes (resolved_at - opened_at)
resolution_statusTEXTLifecycle state — see §3
acknowledged_atTIMESTAMPTZWhen first acknowledged
acknowledged_byTEXT'IO1' / 'HS1' / 'BMS4-1' / 'human' / 'auto'
reactionTEXTWhat action was taken (e.g. “restarted prometheus container”)
reaction_atTIMESTAMPTZWhen the fix was applied
resolved_byTEXTSame values as acknowledged_by
resolution_notesTEXTPost-mortem notes, root cause, prevention steps
false_positiveBOOLEANTrue if alert was a monitoring/config error, not a real outage
source_run_idUUIDFK to audit.runs.id — links incident to the run that created it

3. Resolution Lifecycle

Alert fires
    │
    ▼
INSERT (resolution_status = 'open')
    │
    ▼
Agent or human acknowledges
    → UPDATE SET resolution_status = 'acknowledged',
                 acknowledged_at = NOW(),
                 acknowledged_by = '<who>'
    │
    ▼
Fix applied
    → UPDATE SET reaction = '<what was done>',
                 reaction_at = NOW()
    │
    ▼
Confirmed resolved
    → UPDATE SET resolution_status = 'resolved',
                 resolved_at = NOW(),
                 resolved_by = '<who>',
                 resolution_notes = '<notes>'
         OR
    → UPDATE SET resolution_status = 'false_positive',
                 false_positive = TRUE,
                 resolution_notes = '<why it was a false positive>'

Valid resolution_status values (enforced by CHECK constraint):

  • open — default, alert is active and unaddressed
  • acknowledged — someone is aware and working on it
  • resolved — confirmed fixed
  • false_positive — alert fired incorrectly; no real outage

4. Who Writes Resolution Updates

ActorWhenHow
audit-engine ping_checkAuto-resolves when a monitored URL recovers after being openresolve_incident(source_run_id) via Supabase anon key
AI-Dev workerAfter fixing an infra issue and closing the GitHub issueDirect UPDATE via Supabase anon key
hourly-devops-triage skillSweeps open incidents > 1 h old, sets acknowledgedUPDATE via Supabase anon key
HumanVia Grafana panel action or direct SQLManual

5. Key Queries

Open incidents (dashboard query)

SELECT id, service_name, title, severity, source, opened_at, created_at
FROM dev_r_incidents
WHERE resolution_status = 'open'
ORDER BY created_at DESC;

MTTR last 30 days (excluding false positives)

SELECT
  AVG(EXTRACT(EPOCH FROM (resolved_at - opened_at)) / 60.0) AS avg_mttr_minutes,
  COUNT(*) AS resolved_count
FROM dev_r_incidents
WHERE resolution_status = 'resolved'
  AND false_positive = FALSE
  AND resolved_at >= NOW() - INTERVAL '30 days';

False positive rate last 30 days

SELECT
  COUNT(*) FILTER (WHERE false_positive = TRUE) AS fp_count,
  COUNT(*) AS total_count,
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE false_positive = TRUE) / NULLIF(COUNT(*), 0),
    1
  ) AS fp_rate_pct
FROM dev_r_incidents
WHERE created_at >= NOW() - INTERVAL '30 days';

Incidents by day (resolution timeline)

SELECT
  DATE_TRUNC('day', created_at) AS day,
  COUNT(*) FILTER (WHERE resolution_status = 'open') AS open_count,
  COUNT(*) FILTER (WHERE resolution_status = 'resolved') AS resolved_count
FROM dev_r_incidents
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY 1
ORDER BY 1;

6. audit-engine Integration

ping_check.py creates an incident row when a URL fails and auto-resolves it on recovery. The source_run_id column links each incident to the audit.runs row that triggered it.

Helper functions in audit-engine/db.py:

  • create_incident(action_id, service_name, title, severity, source_run_id) — INSERT with resolution_status='open'
  • resolve_incident(source_run_id) — UPDATE to resolution_status='resolved' for the matching open incident

7. Grafana Notes

grafana_readonly has SELECT access on this table (granted in migration 20260618170105).

Planned dashboard panels (issue #731):

  • Open incidents table — WHERE resolution_status = 'open' ORDER BY created_at DESC
  • MTTR gauge — AVG(resolved_at - opened_at) last 30 days, excluding false positives
  • Resolution timeline — bar chart: open vs resolved per day
  • False positive rate — percentage last 30 days

8. RLS Policy

RLS is enabled. Public anon access (read/write) — same policy as dev_r_services.

To verify:

SELECT relrowsecurity FROM pg_class WHERE relname = 'dev_r_incidents';
SELECT policyname, cmd FROM pg_policies WHERE tablename = 'dev_r_incidents';

9. Compliance

  • dev_r_services row: service_name = 'dev_r_incidents', compliance_workbook = 'yes'
  • Resolution workflow added in issue #726
  • Grafana dashboard planned in issue #731
  • This table is not subject to EU AI Act (no AI processing; it is a data store only)