-- 02_import_meta.sql — fleettickets · snapshot metadata + map freshness -- ───────────────────────────────────────────────────────────────────────────── -- The n8n S3 export now wraps each dataset in a metadata envelope -- ({ "metadata": {...}, "records": [...] }; see n8n-s3-export-workflows.md). -- We capture that envelope per dataset at ingest so the map can show how fresh -- the snapshot is, and re-define reporting.fn_tickets_for_map (same signature — -- dashboard_api unchanged) to expose it under summary.freshness. -- -- Idempotent: safe on a fresh DB and re-appliable on the live DB. -- ───────────────────────────────────────────────────────────────────────────── SET search_path = tickets, public; -- ── per-dataset snapshot metadata (one row per dataset; upserted each ingest) ─ CREATE TABLE IF NOT EXISTS tickets.import_meta ( dataset text PRIMARY KEY, -- 'inc' | 'crq' export_type text, -- 'delta' | 'full' exported_at timestamptz, -- metadata.exported_at (source) snapshot_date date, -- metadata.snapshot_date (full runs) source_schema text, source_table text, row_count integer, -- metadata.row_count (source count) records_ingested integer, -- rows we actually read/upserted n8n_execution_id text, metadata jsonb, -- full envelope metadata (audit) ingested_at timestamptz NOT NULL DEFAULT now() ); -- ── read function — add summary.freshness (signature unchanged) ────────────── CREATE OR REPLACE FUNCTION reporting.fn_tickets_for_map( p_service_type text DEFAULT NULL, p_status text DEFAULT NULL, p_open_only boolean DEFAULT true ) RETURNS jsonb LANGUAGE plpgsql STABLE AS $fn$ DECLARE v_result jsonb; BEGIN p_service_type := lower(NULLIF(p_service_type, '')); p_status := NULLIF(p_status, ''); WITH filtered AS ( SELECT 'inc'::text AS service_type, raw, geom, geo_source FROM tickets.inc WHERE geom IS NOT NULL AND (p_service_type IS NULL OR p_service_type = 'inc') AND (p_status IS NULL OR raw->>'normalized_status' = p_status) AND (NOT p_open_only OR (raw->>'is_actionable')::boolean IS TRUE) UNION ALL SELECT 'crq'::text AS service_type, raw, geom, geo_source FROM tickets.crq WHERE geom IS NOT NULL AND (p_service_type IS NULL OR p_service_type = 'crq') AND (p_status IS NULL OR raw->>'normalized_status' = p_status) AND (NOT p_open_only OR (raw->>'is_actionable')::boolean IS TRUE) ) SELECT jsonb_build_object( 'summary', jsonb_build_object( 'ticket_count', COUNT(*), 'inc', COUNT(*) FILTER (WHERE service_type = 'inc'), 'crq', COUNT(*) FILTER (WHERE service_type = 'crq'), 'open', COUNT(*) FILTER (WHERE (raw->>'is_actionable')::boolean IS TRUE), 'by_status', (SELECT jsonb_object_agg(s, c) FROM (SELECT raw->>'normalized_status' AS s, COUNT(*) AS c FROM filtered GROUP BY raw->>'normalized_status') z), 'freshness', (SELECT jsonb_object_agg(dataset, jsonb_build_object( 'export_type', export_type, 'exported_at', exported_at, 'snapshot_date', snapshot_date, 'row_count', row_count, 'records_ingested', records_ingested, 'ingested_at', ingested_at)) FROM tickets.import_meta WHERE p_service_type IS NULL OR dataset = p_service_type) ), 'geojson', jsonb_build_object( 'type', 'FeatureCollection', 'features', COALESCE(jsonb_agg( jsonb_build_object( 'type', 'Feature', 'properties', jsonb_build_object( 'ticket_id', raw->>'ticket_id', 'service_type', service_type, 'status', raw->>'normalized_status', 'raw_status', raw->>'raw_status', 'cluster', raw->>'cluster', 'region', raw->>'region', 'location_name', raw->>'location_name', 'department', raw->>'department', 'owner', raw->>'owner', 'assigned_team', raw->>'assigned_team', 'sla_status', raw->>'sla_status', 'is_actionable', (raw->>'is_actionable')::boolean, 'geo_source', geo_source, 'created_at', raw->>'created_at_service', 'scheduled_at', raw->>'scheduled_at' ), 'geometry', ST_AsGeoJSON(geom)::jsonb ) ), '[]'::jsonb) ) ) INTO v_result FROM filtered; RETURN v_result; END $fn$; COMMENT ON FUNCTION reporting.fn_tickets_for_map(text, text, boolean) IS 'INC/CRQ tickets (tickets.inc + tickets.crq, raw-jsonb-first) as GeoJSON, with ' 'summary.freshness from tickets.import_meta. fleettickets 02.'; -- ── 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.import_meta TO tracksolid_owner; END IF; IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN GRANT SELECT ON tickets.import_meta TO dashboard_ro; END IF; IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN GRANT SELECT ON tickets.import_meta TO grafana_ro; END IF; END $grants$;