-- migrate:up -- 12-colour palette (stable across runs via hashtext). -- Used when a vehicle has a cost_centre; null cost_centre → default blue. CREATE OR REPLACE FUNCTION serve._cost_centre_color(name text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN name IS NULL OR name = '' THEN '#3b82f6' ELSE (ARRAY[ '#10b981', '#3b82f6', '#f59e0b', '#ec4899', '#8b5cf6', '#14b8a6', '#f97316', '#06b6d4', '#a855f7', '#84cc16', '#e11d48', '#6366f1' ])[mod(abs(hashtext(name)), 12) + 1] END $$; DROP FUNCTION IF EXISTS serve.fn_live_view(jsonb); CREATE OR REPLACE FUNCTION serve.fn_live_view(filters jsonb) RETURNS jsonb LANGUAGE plpgsql STABLE AS $$ DECLARE fresh_window interval := COALESCE((filters->>'fresh_window')::interval, interval '24 hours'); offline_after interval := COALESCE((filters->>'offline_after')::interval, interval '5 minutes'); move_speed_kmh numeric := COALESCE((filters->>'move_speed_kmh')::numeric, 5); p_cost_centre text := filters->>'cost_centre'; p_assigned_city text := filters->>'assigned_city'; p_vehicle_numbers text[] := CASE WHEN filters ? 'vehicle_numbers' THEN ARRAY(SELECT jsonb_array_elements_text(filters->'vehicle_numbers')) ELSE NULL END; result jsonb; BEGIN WITH candidates AS ( SELECT lp.imei, lp.occurred_at, lp.geom, lp.speed_kmh, lp.direction_deg, lp.mc_type, lp.current_mileage_km, lp.gps_signal, lp.satellites, lp.device_name, lp.pos_type, d.device_type, d.activation_at, v.vehicle_id, v.plate, v.cost_centre, v.assigned_city FROM state.live_positions lp JOIN domain.devices d ON d.imei = lp.imei JOIN domain.vehicles v ON v.vehicle_id = d.vehicle_id WHERE d.lifecycle = 'active' AND (p_cost_centre IS NULL OR v.cost_centre = p_cost_centre) AND (p_assigned_city IS NULL OR v.assigned_city = p_assigned_city) AND (p_vehicle_numbers IS NULL OR v.plate = ANY (p_vehicle_numbers)) ), ranked AS ( SELECT c.*, ROW_NUMBER() OVER ( PARTITION BY c.vehicle_id ORDER BY CASE c.device_type WHEN 'tracker' THEN 0 ELSE 1 END, CASE WHEN c.occurred_at > now() - fresh_window THEN 0 ELSE 1 END, c.occurred_at DESC, c.activation_at DESC NULLS LAST ) AS rn FROM candidates c ), deduped AS (SELECT * FROM ranked WHERE rn = 1), enriched AS ( SELECT d.*, CASE WHEN d.occurred_at <= now() - offline_after THEN 'offline' WHEN d.speed_kmh IS NOT NULL AND d.speed_kmh > move_speed_kmh THEN 'moving' ELSE 'parked' END AS operational_state, serve._cost_centre_color(d.cost_centre) AS cost_centre_color, EXTRACT(EPOCH FROM (now() - d.occurred_at))::int AS age_sec FROM deduped d ), summary AS ( SELECT jsonb_build_object( 'total_active', count(*), 'moving', count(*) FILTER (WHERE operational_state = 'moving'), 'parked', count(*) FILTER (WHERE operational_state = 'parked'), 'offline', count(*) FILTER (WHERE operational_state = 'offline'), 'below_freshness_slo', count(*) FILTER ( WHERE occurred_at <= now() - interval '90 seconds' ), 'as_of', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') ) AS s FROM enriched ), features AS ( SELECT COALESCE(jsonb_agg( jsonb_build_object( 'type', 'Feature', 'geometry', ST_AsGeoJSON(e.geom)::jsonb, 'properties', jsonb_build_object( 'vehicle_id', e.vehicle_id, 'plate', e.plate, 'plate_short', right(e.plate, 4), 'imei', e.imei, 'device_type', e.device_type, 'device_name', e.device_name, 'mc_type', e.mc_type, 'pos_type', e.pos_type, 'cost_centre', e.cost_centre, 'cost_centre_color', e.cost_centre_color, 'assigned_city', e.assigned_city, 'occurred_at', to_char(e.occurred_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'), 'age_sec', e.age_sec, 'speed_kmh', e.speed_kmh, 'heading_deg', e.direction_deg, 'gps_signal', e.gps_signal, 'satellites', e.satellites, 'current_mileage_km', e.current_mileage_km, 'operational_state', e.operational_state, 'style_class', 'vehicle-' || e.operational_state, 'marker_color', CASE WHEN e.operational_state = 'moving' THEN e.cost_centre_color ELSE '#9ca3af' END, 'show_arrow', (e.operational_state = 'moving' AND e.direction_deg IS NOT NULL) ) ) ), '[]'::jsonb) AS feats FROM enriched e ), slo_block AS ( SELECT COALESCE(jsonb_object_agg( metric, jsonb_build_object( 'threshold', threshold, 'current', current_value, 'status', status ) ), '{}'::jsonb) AS ss FROM slo.v_current_status ) SELECT jsonb_build_object( 'summary', (SELECT s FROM summary), 'geojson', jsonb_build_object( 'type', 'FeatureCollection', 'features', (SELECT feats FROM features) ), 'slo_status', (SELECT ss FROM slo_block) ) INTO result; RETURN result; END; $$; -- migrate:down DROP FUNCTION IF EXISTS serve.fn_live_view(jsonb); -- restore v1 (keep prior live behavior) CREATE OR REPLACE FUNCTION serve.fn_live_view(filters jsonb) RETURNS jsonb LANGUAGE plpgsql STABLE AS $$ BEGIN RETURN '{}'::jsonb; END; $$; DROP FUNCTION IF EXISTS serve._cost_centre_color(text);