-- 19_v_ingest_health.sql -- reporting.v_ingest_health — per-endpoint ingest freshness for FleetOps. -- -- CONTEXT: Grafana was removed (2026-06-10) as redundant with the FleetOps SPA. -- The one signal only Grafana surfaced was pipeline freshness. This view replaces -- it with a read-API-friendly surface derived from the existing -- tracksolid.ingestion_log (every poll already writes a row via log_ingestion()), -- so FleetOps can show "is the ingest pipeline alive / stale / erroring" per -- endpoint without a separate dashboard product. Exposed by dashboard_api as -- GET /health/ingest. -- -- One row per endpoint: the latest run, how long ago, last success/error, 1-hour -- run + failure counts, and a coarse freshness verdict. Each endpoint's expected -- cadence mirrors the ingest_worker schedule (ingest_worker_rev.py); 'stale' fires -- only past 3x that cadence so daily/low-frequency jobs aren't false-flagged. -- Guarded + idempotent -> safe to re-apply. CREATE OR REPLACE VIEW reporting.v_ingest_health AS WITH last_run AS ( SELECT DISTINCT ON (endpoint) endpoint, run_at, success, error_code, error_message, rows_inserted, rows_upserted, imei_count, duration_ms FROM tracksolid.ingestion_log ORDER BY endpoint, run_at DESC ), agg AS ( SELECT endpoint, count(*) FILTER (WHERE run_at > now() - interval '1 hour') AS runs_1h, count(*) FILTER (WHERE run_at > now() - interval '1 hour' AND NOT success) AS failures_1h FROM tracksolid.ingestion_log GROUP BY endpoint ) SELECT lr.endpoint, lr.run_at AS last_run_at, EXTRACT(EPOCH FROM (now() - lr.run_at))::int AS seconds_ago, lr.success AS last_success, lr.error_code, lr.error_message, lr.rows_inserted, lr.rows_upserted, COALESCE(a.runs_1h, 0) AS runs_1h, COALESCE(a.failures_1h, 0) AS failures_1h, ex.expected_interval_s, CASE WHEN EXTRACT(EPOCH FROM (now() - lr.run_at)) > 3 * ex.expected_interval_s THEN 'stale' WHEN NOT lr.success THEN 'error' ELSE 'ok' END AS freshness FROM last_run lr LEFT JOIN agg a USING (endpoint) CROSS JOIN LATERAL ( SELECT CASE lr.endpoint WHEN 'jimi.user.device.location.list' THEN 60 -- live sweep (60s) WHEN 'jimi.device.alarm.list' THEN 300 -- alarms (5m) WHEN 'jimi.device.track.mileage' THEN 900 -- trips (15m) WHEN 'jimi.open.platform.report.parking' THEN 900 -- parking (15m) WHEN 'jimi.device.track.list' THEN 1800 -- high-res trail (30m) ELSE 3600 -- default (1h) END AS expected_interval_s ) ex ORDER BY seconds_ago DESC; COMMENT ON VIEW reporting.v_ingest_health IS 'Per-endpoint ingest freshness from tracksolid.ingestion_log. Replaces the ' 'Grafana pipeline-health panels (Grafana removed 2026-06-10). Surfaced by ' 'dashboard_api GET /health/ingest. freshness = ok|stale|error (stale = past 3x ' 'the ingest_worker_rev.py cadence).'; -- Read-only access for the dashboard roles (guarded; idempotent). DO $grants$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN GRANT SELECT ON reporting.v_ingest_health TO grafana_ro; END IF; IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN GRANT SELECT ON reporting.v_ingest_health TO dashboard_ro; END IF; END $grants$;