Repository: cityofaustin/vision-zero
Pull request #1791: v2.12.1 - Update crash-EMS trigger
Author: login: johnclary
Created: 2025-05-16T14:42:26Z | Merged: 2025-05-16T16:21:14Z

## Associated issues

* https://github.com/cityofaustin/atd-data-tech/issues/22612

I will PR this against prod as well once we have approvals.

## Testing

**URL to test:** Local

1. Apply migrations and metadata
2. Visit this crash's details: `http://localhost:3002/editor/crashes/17357278`
3. Scroll down to **EMS Patient care** and observe that there is a single EMS record with status of **Matched automatically**
4. Edit the crash's **Longitude** to be `-98.12642144`. Scroll down to the EMS table and notice that there are no EMS records listed.
5. Edit crash's **Longitude** back to the original value of `-97.676720393005`. Notice that the matched EMS record is listed in the EMS table. Use it's hyperlinked incident # to visit the incident details page.
6. Use the match UI to match the EMS record to any person record. Notice that it's status changes to **Matched by review/QA**
7. Navigate back to the crash details page, and again edit the longitude to  `-98.12642144`. Notice that the EMS record remains listed in the table.
8. Navigate back to the EMS incident details page, and use the falafel menu to "Reset" the record's status.
9. Notice that the EMS record now has a status of **Unmatched**
10. Finally, go back to the crash details and once again restore the crash to it's original **Longitude**: `-97.676720393005`. Notice that the EMS record is again listed in the table with a status of **Matched automatically**

---

#### Ship list

- [ ] Check migrations for any conflicts with latest migrations in `main` branch
- [ ] Confirm Hasura role permissions for necessary access
- [ ] Code reviewed
- [ ] Product manager approved



Diff:
diff --git a/database/migrations/default/1747404534187_patch_crash_ems_trigger/down.sql b/database/migrations/default/1747404534187_patch_crash_ems_trigger/down.sql
new file mode 100644
index 0000000000..fa952d0ed6
--- /dev/null
+++ b/database/migrations/default/1747404534187_patch_crash_ems_trigger/down.sql
@@ -0,0 +1,111 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the automated match column
+        IF matching_ems.crash_match_status === 'matched_by_manual_qa' THEN
+          IF match_count = 0 THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = NULL
+            WHERE id = matching_ems.id;
+          ELSE 
+            -- if the match count is more than one we will update the matched_crash_pks
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+          END IF;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    --
+    -- nullify the crash_pk for EMS records that no longer match this crash
+    --
+    IF TG_OP = 'UPDATE' THEN
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches: these should not be modified
+            crash_match_status != 'matched_by_manual_qa';
+    END IF;
+    RETURN NEW;
+END;
+$function$;
diff --git a/database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql b/database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
new file mode 100644
index 0000000000..cf21d7db51
--- /dev/null
+++ b/database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
@@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            crash_match_status = 'matched_by_manual_qa';
+    END IF;
+    RETURN NEW;
+END;
+$function$;
diff --git a/editor/package-lock.json b/editor/package-lock.json
index 91bfba777f..af94c4dce1 100644
--- a/editor/package-lock.json
+++ b/editor/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "vision-zero-editor",
-  "version": "2.12",
+  "version": "2.12.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "vision-zero-editor",
-      "version": "2.12",
+      "version": "2.12.1",
       "license": "MIT",
       "dependencies": {
         "@auth0/auth0-react": "^2.3.0",
diff --git a/editor/package.json b/editor/package.json
index c83a1d9a05..c0808fce88 100644
--- a/editor/package.json
+++ b/editor/package.json
@@ -1,6 +1,6 @@
 {
   "name": "vision-zero-editor",
-  "version": "2.12",
+  "version": "2.12.1",
   "description": "ATD Vision Zero Editor",
   "author": "TPW Data & Technology Services",
   "license": "MIT",
diff --git a/viewer/package-lock.json b/viewer/package-lock.json
index cc8f533fe7..f2427918c1 100644
--- a/viewer/package-lock.json
+++ b/viewer/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "viewer",
-  "version": "2.8.1",
+  "version": "2.12.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "viewer",
-      "version": "2.8.1",
+      "version": "2.12.1",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
diff --git a/viewer/package.json b/viewer/package.json
index 3dbc0970f8..deaf851d89 100644
--- a/viewer/package.json
+++ b/viewer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "viewer",
-  "version": "2.11",
+  "version": "2.12.1",
   "homepage": "/viewer",
   "description": "Vision Zero Viewer",
   "author": "Austin Transportation & Public Works - Data & Technology Services",


Reviews:
user: login: johnclary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T14:57:24Z
user: login: johnclary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T14:58:56Z
user: login: mddilley
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:25:04Z
user: login: frankhereford
body: Great instructions, and everything worked as expected. This is ready to ship -- thank you for such a quick fix.

Maybe it's something I have setup funny, but I get the crash URL in my local stack at http://localhost:3002/crashes/17357278 without the `editor` portion of the path.

The match UI is great, and this is a nothing, but FWIW, I was unable to figure out how to scroll the matching UI table to the right without having to open up the mac and use the trackpad. I wonder if anyone else has any issue when using just a traditional mouse -- I'm going to try to remember to ask in the dev sync.

Also, it'd be neat to get the moped view bot in effect in VZ to help see the diffs of these more complex views. Maybe that will help in the future for this sort of thing.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T15:34:03Z
user: login: roseeichelmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:37:13Z
user: login: chiaberry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T15:41:21Z
user: login: mddilley
body: All the test steps check out, and the updates to the function make sense to me! 🚢
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:47:33Z
user: login: roseeichelmann
body: this looks great, super thorough test instructions, thank u for fixing my oopsie :heavy_equals_sign: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:51:25Z
user: login: johnclary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T15:51:49Z
user: login: johnclary
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T15:53:00Z
user: login: chiaberry
body: :ship: !
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T15:53:05Z

Review comments:
diff_hunk: @@ -0,0 +1,111 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the automated match column
+        IF matching_ems.crash_match_status === 'matched_by_manual_qa' THEN
+          IF match_count = 0 THEN
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/down.sql
user: login: johnclary
body: This is another bug I uncovered while testing. This block never ran because match count is never `0` here. I moved this handling down to the `IF TG_OP = 'UPDATE' THEN` section.
created_at: 2025-05-16T14:57:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: johnclary
body: This update statement is new, and replaces the part of the trigger that was under `IF match_count = 0 THEN`
created_at: 2025-05-16T14:58:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: mddilley
body: @johnclary I follow this whole function, but I have one question about setting `matched_crash_pks` to `null` here but not in the block on lines 50-53 since they are both looking at the `matched_by_manual_qa` status. 

I think that I'm getting lost on when we want to use `matched_crash_pks` as a holding place for recovering previous matches and when we don't want to.
created_at: 2025-05-16T15:25:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: roseeichelmann
body: i believe the logic here is that `matched_crash_pks` should always stay up to date with what our automated matching finds, and in this case this incident doesnt have any matches, so `matched_crash_pks` would update to null. but this wouldnt affect the manual value in `crash_pk` that the VZ team manually matched. in lines 50-53 the record did have matching crash ids, so thats why `matched_crash_pks` was updated there.
created_at: 2025-05-16T15:37:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,6 @@
 {
   "name": "viewer",
-  "version": "2.11",
path: viewer/package.json
user: login: chiaberry
body: sorry i missed this reviewing the release yesterday
created_at: 2025-05-16T15:41:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: mddilley
body: ohhh now i see it. the WHERE conditions in this update statement are how we know there are no automated matches - that is what I was missing. 🙏 Thanks for helping me through this because I was looking earlier in the code and not seeing that kind of check.
created_at: 2025-05-16T15:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: johnclary
body: > when we want to use `matched_crash_pks` as a holding place for recovering previous matches and when we don't want to.

@mddilley Our intention is that `matched_crash_pks` _always_ holds the automatically matched crash PKs regardless of any user edits. This way we can reset to the original/automatic state of the EMS record. So the trigger is keeping this column in sync with any edits that are happening to the crash.
created_at: 2025-05-16T15:51:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+-- Updating this trigger to always keep the matched_crash_pks up to date with automatic matching results
+-- regardless of crash match status
+CREATE OR REPLACE FUNCTION public.update_crash_ems_match()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+    matching_ems RECORD;
+    ems_record RECORD;
+    match_count INTEGER;
+    matched_crash_ids INTEGER[];
+    meters_threshold INTEGER := 1200;
+    -- 30 min threshold equates to a 60 minute window (+/- 30 minutes of crash timestamp)
+    time_threshold INTERVAL := '30 minutes';
+BEGIN
+    -- Find all EMS records near the crash location + time
+    FOR matching_ems IN (
+        SELECT 
+            e.id,
+            e.incident_received_datetime,
+            e.geometry,
+            e.crash_match_status
+        FROM 
+            ems__incidents e
+        WHERE
+            e.incident_received_datetime  >= (NEW.crash_timestamp - time_threshold)
+            AND e.incident_received_datetime  <= (NEW.crash_timestamp + time_threshold)
+            AND e.geometry IS NOT NULL
+            AND NEW.position IS NOT NULL
+            AND ST_DWithin(e.geometry::geography, NEW.position::geography, meters_threshold)
+    ) LOOP
+        -- Find all crashes which match this EMS record location + time
+        SELECT 
+            ARRAY(
+                SELECT c.id
+                FROM crashes c
+                WHERE 
+                    c.is_deleted = false
+                    and not c.is_temp_record
+                    AND matching_ems.incident_received_datetime  >= (c.crash_timestamp - time_threshold)
+                    AND matching_ems.incident_received_datetime  <= (c.crash_timestamp + time_threshold)
+                    AND c.position IS NOT NULL
+                    AND ST_DWithin(matching_ems.geometry::geography, c.position::geography, meters_threshold)
+            ) INTO matched_crash_ids;
+            
+        -- Get the count from the array length (handling when array_length is null as 0)
+        SELECT COALESCE(array_length(matched_crash_ids, 1), 0) INTO match_count;
+
+        -- For records that have been manually matched we want to only update the matched_crash_pks column
+        IF matching_ems.crash_match_status = 'matched_by_manual_qa' THEN
+            UPDATE ems__incidents 
+            SET matched_crash_pks = matched_crash_ids
+            WHERE id = matching_ems.id;
+        -- If the record has not been manually matched
+        ELSE
+          IF match_count = 0 THEN
+              UPDATE ems__incidents 
+              SET crash_pk = NULL,
+                  crash_match_status = 'unmatched',
+                  matched_crash_pks = NULL
+              WHERE id = matching_ems.id;
+          ELSIF match_count = 1 THEN
+              -- this EMS record is only matched to one crash - we can assign the crash_pk and matched_crash_pks
+              UPDATE ems__incidents 
+              SET crash_pk = NEW.id,
+                  crash_match_status = 'matched_by_automation',
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          ELSE
+              -- multiple matching crashes found - can assign the matched_crash_pks but not crash_pk
+              UPDATE ems__incidents 
+              SET crash_match_status = 'multiple_matches_by_automation',
+                  crash_pk = NULL,
+                  matched_crash_pks = matched_crash_ids
+              WHERE id = matching_ems.id;
+          END IF;
+        END IF;
+    END LOOP;
+
+    IF TG_OP = 'UPDATE' THEN
+        -- update incidents which have not been manually matched
+        UPDATE ems__incidents
+        SET crash_pk = NULL,
+            crash_match_status = 'unmatched',
+            matched_crash_pks = NULL
+        WHERE crash_pk = NEW.id
+          AND (
+              incident_received_datetime  < (NEW.crash_timestamp - time_threshold)
+              OR incident_received_datetime  > (NEW.crash_timestamp + time_threshold)
+              OR geometry IS NULL
+              OR NEW.position IS NULL
+              OR NEW.is_temp_record is TRUE
+              or NEW.is_deleted is TRUE
+              OR (NOT ST_DWithin(geometry::geography, NEW.position::geography, meters_threshold))
+          )
+          AND 
+            -- ignore manual qa matches
+            crash_match_status != 'matched_by_manual_qa';
+       
+        -- update the matched_crash_pks column only for manually qa matches
+        UPDATE ems__incidents
+        SET matched_crash_pks = NULL
path: database/migrations/default/1747404534187_patch_crash_ems_trigger/up.sql
user: login: johnclary
body: I added this thread to https://github.com/cityofaustin/atd-data-tech/issues/22006
created_at: 2025-05-16T15:53:00Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-05-16T14:44:55Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *atd-vze-staging* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 324cdc28acada2b333ee1dadaa3875ba26fd9c12 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/atd-vze-staging/deploys/68274ed568b6880008c8d13c |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1791--atd-vze-staging.netlify.app](https://deploy-preview-1791--atd-vze-staging.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE3OTEtLWF0ZC12emUtc3RhZ2luZy5uZXRsaWZ5LmFwcCJ9.VzsWewjb4bp7VtXTRR8fb1SzXHEI_yifQHKQk3hYaMQ)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [atd-vze-staging Preview](https://deploy-preview-1791--atd-vze-staging.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/atd-vze-staging/configuration/notifications#deploy-notifications)._
author_association: NONE
user: login: johnclary
created_at: 2025-05-16T14:59:46Z
body: 👋 I didn't add this to the test instructions, but I'm going to run the latest CRIS import locally to make sure that's working as expected

Edit 👇 
```
❯ docker compose run cris_import
root@docker-desktop:/app# ./cris_import.py --csv
INFO: 1 extract(s) to process
INFO: Unzipping extracts/extract_2023_20250303080326188_107768_20250515_HAYSTRAVISWILLIAMSON.zip to extracts/extract_2023_20250303080326188_107768_20250515_HAYSTRAVISWILLIAMSON
INFO: Processing CSVs for extract ID: 20250303080326
INFO: Uploading 31 crashes
INFO: Uploading 59 units
INFO: Uploading 77 persons
INFO: Uploading 17 charges
INFO: Uploading 85 crashes
INFO: Uploading 174 units
INFO: Uploading 217 persons
INFO: Uploading 29 charges
INFO: ✅ Done in 0.01 minutes
``
author_association: MEMBER
user: login: chiaberry
created_at: 2025-05-16T15:53:47Z
body: That's a good idea Frank, to also see if we could get that dev experience not only on views like in moped but also on triggers
https://github.com/cityofaustin/atd-data-tech/issues/18791
author_association: MEMBER
user: login: johnclary
created_at: 2025-05-16T16:00:52Z
body: > Maybe it's something I have setup funny, but I get the crash URL in my local stack at http://localhost:3002/crashes/17357278 without the editor portion of the path.

@frankhereford - sometime during the VZE upgrades we added a custom `/editor` base path to [local env](https://github.com/cityofaustin/vision-zero/blob/3aaac9b7234cbd7b0f5848074866bc9bf56febd7/editor/env_template#L9) so that it mimics prod. the app works normally locally without so it's a bit confusing 🤪 

>  using just a traditional mouse [to scroll right]

I am curious if any of these work for you:
1. Hold down the Shift key while scrolling the mouse wheel - this will convert vertical scrolling to horizontal scrolling
2. If you have a mouse with a scroll wheel that tilts sideways (like some Logitech mice), you can use that feature directly for horizontal scrolling
3. For Apple's Magic Mouse, swiping left or right on the surface works for horizontal scrolling, though that's technically using its touch surface

> diffs of complex views

💯 https://github.com/cityofaustin/atd-data-tech/issues/18791 (thanks Chia for the link)

author_association: MEMBER
user: login: frankhereford
created_at: 2025-05-16T16:10:40Z
body: @johnclary wrote:

> 1. Hold down the Shift key while scrolling the mouse wheel - this will convert vertical scrolling to horizontal scrolling

😲 **I've sat in front of these computers for 30 years, and today I learn that shift+scroll → horizontal-scroll. What other deep magic do have?!?!**

My mouse is a [Corsair Nightswoard RGB Gaming Mouse](https://www.amazon.com/dp/B07QX9C9WH) and I wouldn't buy it again because it's meh at best. It lacks that Logitech wheel-tilt feature. I've used mice with that before though, I totally know what you mean. It'd do the trick.

I don't think it's a big deal since it appears that I may be the only user who doesn't always have a track-pad handy, but I did have to puzzle for a second where the UI's button when matching an EMS record because my window was just wide enough to have the edge of the table land between columns. The button was well hidden in my particular, isolated case.

Thank you also for the `/editor` tip; I've made that configuration here now too. 🙏 
author_association: MEMBER
user: login: johnclary
created_at: 2025-05-16T16:24:13Z
body: Yeah, I'm not really sure how to get away from side-scrolling in this app. It would be nice if the buttons were stuck the right and always visible. We could also move them to the left 🤔 

author_association: MEMBER