119 lines
5.9 KiB
MySQL
119 lines
5.9 KiB
MySQL
|
|
-- 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$;
|