Migration: serve._label_short() extracts plate-tail (handles 'Driver - KDW 573B_cam' patterns)
This commit is contained in:
parent
45974b3810
commit
2b428e8058
1 changed files with 167 additions and 0 deletions
167
db/migrations/20260601000012_label_short_from_plate.sql
Normal file
167
db/migrations/20260601000012_label_short_from_plate.sql
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- migrate:up
|
||||
--
|
||||
-- device_name from Tracksolid encodes both driver and plate in one field,
|
||||
-- with patterns like:
|
||||
-- "John Mbugua - KDW 573B_cam" → driver "John Mbugua", plate "KDW 573B"
|
||||
-- "Parked - KMGK 596V" → no driver, plate "KMGK 596V"
|
||||
-- "JC400P-92732" → no driver/plate, device serial
|
||||
-- "865135061562722" → bare IMEI fallback
|
||||
--
|
||||
-- For the marker label we want the last 4 chars of the *plate* portion, not
|
||||
-- "_cam" / "_CAM" suffix or driver-name tail. Build a helper that:
|
||||
-- 1. drops trailing "_cam" / "_CAM"
|
||||
-- 2. strips everything up to and including the last " - " separator
|
||||
-- 3. takes the last 4 chars
|
||||
|
||||
CREATE OR REPLACE FUNCTION serve._label_short(device_name text, plate text)
|
||||
RETURNS text
|
||||
LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT right(
|
||||
regexp_replace(
|
||||
regexp_replace(
|
||||
COALESCE(NULLIF(device_name, ''), plate, ''),
|
||||
'_(cam|CAM)$', ''
|
||||
),
|
||||
'^.* - ', ''
|
||||
),
|
||||
4
|
||||
)
|
||||
$$;
|
||||
|
||||
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,
|
||||
round(ST_Y(d.geom)::numeric, 4) AS lat_rounded,
|
||||
round(ST_X(d.geom)::numeric, 4) AS lng_rounded
|
||||
FROM deduped d
|
||||
),
|
||||
with_addr AS (
|
||||
SELECT e.*, g.address, g.address_short
|
||||
FROM enriched e
|
||||
LEFT JOIN state.geocoded_positions g
|
||||
ON g.lat_rounded = e.lat_rounded
|
||||
AND g.lng_rounded = e.lng_rounded
|
||||
),
|
||||
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 with_addr
|
||||
),
|
||||
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', serve._label_short(e.device_name, e.plate),
|
||||
'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,
|
||||
'address', e.address,
|
||||
'address_short', e.address_short,
|
||||
'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 with_addr 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);
|
||||
DROP FUNCTION IF EXISTS serve._label_short(text, text);
|
||||
Loading…
Reference in a new issue