-- 10_inc_history_capture.sql — fleettickets · history capture for time-series -- ───────────────────────────────────────────────────────────────────────────── -- tickets.inc is current-state (upsert), so it can't answer "how many were OPEN on -- day X". This adds two durable history artifacts, populated by the ingest via -- tickets.capture_history(): -- -- tickets.closure_events — append-only log of observed ticket closures -- (keyed (ticket_id, closed_at); survives row churn, -- records observed_at = when WE first saw the closure). -- tickets.inc_daily_snapshot— one row per EAT day: open backlog (+ SLA split, -- by cluster/status) and the day's created/closed flow. -- Upserted each run → backlog-over-time accrues going -- forward (past backlog can't be reconstructed). -- -- Idempotent: safe on a fresh DB and re-appliable. The first capture backfills -- closure_events from existing closed rows (observed_at = first-run time). -- ───────────────────────────────────────────────────────────────────────────── SET search_path = tickets, public; CREATE TABLE IF NOT EXISTS tickets.closure_events ( ticket_id text NOT NULL, closed_at timestamptz NOT NULL, cluster text, region text, assigned_team text, owner text, mttr numeric, -- minutes sla_status text, observed_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (ticket_id, closed_at) ); CREATE INDEX IF NOT EXISTS ix_closure_events_closed_at ON tickets.closure_events (closed_at); CREATE INDEX IF NOT EXISTS ix_closure_events_team ON tickets.closure_events (assigned_team); CREATE INDEX IF NOT EXISTS ix_closure_events_cluster ON tickets.closure_events (cluster); CREATE TABLE IF NOT EXISTS tickets.inc_daily_snapshot ( snapshot_date date PRIMARY KEY, -- EAT calendar day open_total integer, open_breached integer, open_at_risk integer, open_ok integer, open_unknown integer, created_today integer, -- created_at_service on this EAT day closed_today integer, -- closed_at on this EAT day by_cluster_open jsonb, by_status_open jsonb, captured_at timestamptz NOT NULL DEFAULT now() ); -- capture: append new closures + upsert today's backlog snapshot. Called by the -- ingest after each --apply run (and available standalone). CREATE OR REPLACE FUNCTION tickets.capture_history() RETURNS jsonb LANGUAGE plpgsql AS $fn$ DECLARE v_today date := (now() AT TIME ZONE 'Africa/Nairobi')::date; v_events integer; v_open integer; BEGIN -- 1) append newly-observed closures (closed_at is authoritative; dedup by id+closed_at) INSERT INTO tickets.closure_events (ticket_id, closed_at, cluster, region, assigned_team, owner, mttr, sla_status) SELECT ticket_id, closed_at, cluster, region, assigned_team, owner, mttr, sla_status FROM tickets.inc WHERE closed_at IS NOT NULL ON CONFLICT (ticket_id, closed_at) DO NOTHING; GET DIAGNOSTICS v_events = ROW_COUNT; -- 2) upsert today's backlog snapshot (open metrics from inc_open_sla) INSERT INTO tickets.inc_daily_snapshot (snapshot_date, open_total, open_breached, open_at_risk, open_ok, open_unknown, created_today, closed_today, by_cluster_open, by_status_open, captured_at) SELECT v_today, (SELECT count(*) FROM tickets.inc_open_sla), (SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'breached'), (SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'at_risk'), (SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'ok'), (SELECT count(*) FROM tickets.inc_open_sla WHERE sla_state = 'unknown'), (SELECT count(*) FROM tickets.inc WHERE (created_at_service AT TIME ZONE 'Africa/Nairobi')::date = v_today), (SELECT count(*) FROM tickets.inc WHERE (closed_at AT TIME ZONE 'Africa/Nairobi')::date = v_today), (SELECT COALESCE(jsonb_object_agg(cl, c), '{}'::jsonb) FROM (SELECT COALESCE(cluster, '(none)') cl, count(*) c FROM tickets.inc_open_sla GROUP BY 1) z), (SELECT COALESCE(jsonb_object_agg(st, c), '{}'::jsonb) FROM (SELECT COALESCE(normalized_status, '(none)') st, count(*) c FROM tickets.inc_open_sla GROUP BY 1) z), now() ON CONFLICT (snapshot_date) DO UPDATE SET open_total = EXCLUDED.open_total, open_breached = EXCLUDED.open_breached, open_at_risk = EXCLUDED.open_at_risk, open_ok = EXCLUDED.open_ok, open_unknown = EXCLUDED.open_unknown, created_today = EXCLUDED.created_today, closed_today = EXCLUDED.closed_today, by_cluster_open = EXCLUDED.by_cluster_open, by_status_open = EXCLUDED.by_status_open, captured_at = now(); SELECT open_total INTO v_open FROM tickets.inc_daily_snapshot WHERE snapshot_date = v_today; RETURN jsonb_build_object('closure_events_added', v_events, 'snapshot_date', v_today, 'open_total', v_open); END $fn$; COMMENT ON FUNCTION tickets.capture_history() IS 'Append new closures to tickets.closure_events + upsert today''s tickets.inc_daily_snapshot. fleettickets 10.'; -- grants (guarded: roles may not exist on a fresh DB) DO $grants$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tracksolid_owner') THEN GRANT SELECT, INSERT, UPDATE, DELETE ON tickets.closure_events, tickets.inc_daily_snapshot TO tracksolid_owner; GRANT EXECUTE ON FUNCTION tickets.capture_history() TO tracksolid_owner; END IF; IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN GRANT SELECT ON tickets.closure_events, tickets.inc_daily_snapshot TO dashboard_ro; END IF; IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN GRANT SELECT ON tickets.closure_events, tickets.inc_daily_snapshot TO grafana_ro; END IF; END $grants$;