From a8e1327aa81872ed6095b9f28d1de07e25d596f4 Mon Sep 17 00:00:00 2001 From: david kiania Date: Mon, 8 Jun 2026 14:33:21 +0300 Subject: [PATCH] feat(reporting): add vehicle_type + fleet_segment to live map feed (migration 16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fn_live_positions now emits 'vehicle_type' (devices.vehicle_models) and 'fleet_segment' (reporting.fn_fleet_segment) in each GeoJSON feature so FleetNow can give specialist vehicles (Crane/Motorbike/Pick-Up) their own marker icons. Additive only — no signature change, STABLE function read immediately by dashboard_api (no redeploy). Function body reproduced verbatim from prod via pg_get_functiondef plus the two new properties. Co-Authored-By: Claude Opus 4.8 --- migrations/16_live_feed_vehicle_type.sql | 99 ++++++++++++++++++++++++ run_migrations.py | 1 + 2 files changed, 100 insertions(+) create mode 100644 migrations/16_live_feed_vehicle_type.sql diff --git a/migrations/16_live_feed_vehicle_type.sql b/migrations/16_live_feed_vehicle_type.sql new file mode 100644 index 0000000..6cc357d --- /dev/null +++ b/migrations/16_live_feed_vehicle_type.sql @@ -0,0 +1,99 @@ +-- 16_live_feed_vehicle_type.sql +-- Expose vehicle_type + fleet_segment on the live-map GeoJSON feed so FleetNow can give the +-- specialist vehicles (Crane / Motorbike / Pick-Up) their own marker icons. All other vehicles +-- (field-service + unassigned) keep their current marker — FleetNow only overrides icons when +-- vehicle_type is one of the specialist types. +-- +-- reporting.fn_live_positions is reproduced verbatim from the live prod definition +-- (== migrations/11_reporting_schema.sql, captured via pg_get_functiondef) with TWO added +-- feature properties: +-- 'vehicle_type' = devices.vehicle_models (authoritative API type, surfaced by v_live_positions) +-- 'fleet_segment' = reporting.fn_fleet_segment(vehicle_models) (field_service|specialist|unassigned) +-- No signature change, so dependents are unaffected; STABLE function, read immediately by +-- dashboard_api (no redeploy/restart). Safe to re-apply (CREATE OR REPLACE). + +SET search_path = tracksolid, reporting, public; + +CREATE OR REPLACE FUNCTION reporting.fn_live_positions(p_cost_centre text DEFAULT NULL::text, p_acc_status text DEFAULT NULL::text) + RETURNS jsonb + LANGUAGE plpgsql + STABLE +AS $function$ +DECLARE + v_result jsonb; +BEGIN + p_cost_centre := NULLIF(p_cost_centre, ''); + p_acc_status := NULLIF(p_acc_status, ''); + WITH filtered AS ( + SELECT * FROM reporting.v_live_positions + WHERE (p_cost_centre IS NULL OR cost_centre = p_cost_centre) + AND (p_acc_status IS NULL OR acc_status = p_acc_status) + ) + SELECT jsonb_build_object( + 'summary', jsonb_build_object( + 'vehicle_count', COUNT(*), + -- "moving" and "parked" both restrict to devices that have reported + -- within the OFFLINE_THRESHOLD (24 h) so they represent the live + -- fleet, not equipment-failure stragglers. "offline" is its own + -- counter for the > 24 h tail. + 'moving', COUNT(*) FILTER (WHERE acc_status = '1' + AND source_age_hours < 24), + 'parked', COUNT(*) FILTER (WHERE acc_status = '0' + AND source_age_hours < 24), + 'offline', COUNT(*) FILTER (WHERE source_age_hours >= 24), + 'median_speed_moving', percentile_cont(0.5) WITHIN GROUP (ORDER BY speed) + FILTER (WHERE acc_status = '1' + AND source_age_hours < 24 + AND speed > 0), + 'last_batch_at', to_char(MAX(updated_at) AT TIME ZONE 'Africa/Nairobi', + 'YYYY-MM-DD HH24:MI:SS'), + 'oldest_fix_at', to_char(MIN(gps_time) AT TIME ZONE 'Africa/Nairobi', + 'YYYY-MM-DD HH24:MI:SS'), + 'newest_fix_at', to_char(MAX(gps_time) AT TIME ZONE 'Africa/Nairobi', + 'YYYY-MM-DD HH24:MI:SS'), + 'last_batch_utc', MAX(updated_at), + 'newest_fix_utc', MAX(gps_time) + ), + 'geojson', jsonb_build_object( + 'type', 'FeatureCollection', + 'features', COALESCE(jsonb_agg( + jsonb_build_object( + 'type', 'Feature', + 'properties', jsonb_build_object( + 'imei', imei, + 'vehicle_number', vehicle_number, + 'driver', assigned_driver, + 'cost_centre', cost_centre, + 'assigned_city', assigned_city, + 'vehicle_category', vehicle_category, + 'vehicle_type', vehicle_models, + 'fleet_segment', reporting.fn_fleet_segment(vehicle_models), + 'mc_type', mc_type, + 'device_kind', device_kind, + 'source_age_hours', source_age_hours, + 'speed', speed, + 'direction', direction, + 'acc_status', acc_status, + 'device_status', device_status, + 'gps_signal', gps_signal, + 'gps_num', gps_num, + 'current_mileage', current_mileage, + 'loc_desc', loc_desc, + 'gps_time', to_char(gps_time AT TIME ZONE 'Africa/Nairobi', + 'YYYY-MM-DD HH24:MI:SS'), + 'updated_at', to_char(updated_at AT TIME ZONE 'Africa/Nairobi', + 'YYYY-MM-DD HH24:MI:SS'), + 'gps_time_utc', gps_time, + 'updated_at_utc', updated_at + ), + 'geometry', jsonb_build_object( + 'type', 'Point', + 'coordinates', jsonb_build_array(lng, lat) + ) + ) + ), '[]'::jsonb) + ) + ) INTO v_result FROM filtered; + + RETURN v_result; +END $function$; diff --git a/run_migrations.py b/run_migrations.py index a56b900..8e1e939 100644 --- a/run_migrations.py +++ b/run_migrations.py @@ -39,6 +39,7 @@ MIGRATIONS = [ "13_drop_dwh_gold.sql", # purge dormant dwh_gold schema + v_utilisation_daily "14_fleet_segment_and_vehicles_view.sql", # reporting.fn_fleet_segment + reporting.v_vehicles roster "15_map_exclude_cost_centres.sql", # hide personal/management/mtn vehicles from the live map + "16_live_feed_vehicle_type.sql", # add vehicle_type + fleet_segment to fn_live_positions feed ] # ── Tables that must exist before the service is allowed to start ─────────────