Compare commits

..

No commits in common. "cbae345d43f29a1e46665faaee9d72e6c89216ae" and "dc6404a1148e2720d46014ddc7ff91f94113777c" have entirely different histories.

6 changed files with 66 additions and 391 deletions

View file

@ -55,7 +55,6 @@ from ts_shared_rev import (
get_active_imeis, get_active_imeis,
get_active_imeis_by_target, get_active_imeis_by_target,
get_conn, get_conn,
get_stale_imeis,
get_token, get_token,
is_valid_fix, is_valid_fix,
log_ingestion, log_ingestion,
@ -64,10 +63,8 @@ from ts_shared_rev import (
clean_int, clean_int,
clean_ts, clean_ts,
get_logger, get_logger,
ensure_device,
safe_task, safe_task,
setup_shutdown, setup_shutdown,
upsert_live_position,
) )
log = get_logger("movement") log = get_logger("movement")
@ -220,30 +217,30 @@ def poll_live_positions():
gps_num = clean_int(p.get("gpsNum")) gps_num = clean_int(p.get("gpsNum"))
current_mileage = clean_num(p.get("currentMileage")) current_mileage = clean_num(p.get("currentMileage"))
# [FIX-M20] Time-guarded upsert via shared helper so the cur.execute("""
# 60s sweep, the alarm cross-feed, and get_device_locations INSERT INTO tracksolid.live_positions (
# all agree about freshness ordering. The sweep is normally imei, geom, lat, lng, pos_type, confidence, gps_time, hb_time,
# the freshest source so the guard rarely rejects its writes. speed, direction, acc_status, gps_signal, gps_num,
upserted += upsert_live_position( elec_quantity, power_value, battery_power_val, tracker_oil,
cur, imei, lat, lng, gps_time, temperature, current_mileage, device_status, loc_desc, recorded_at
speed=speed, direction=direction, ) VALUES (
acc_status=acc_status, %s, ST_SetSRID(ST_MakePoint(%s, %s), 4326), %s, %s, %s, %s, %s, %s,
current_mileage=current_mileage, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW()
extras={
"pos_type": clean(p.get("posType")),
"confidence": clean_int(p.get("confidence")),
"hb_time": clean_ts(p.get("hbTime")),
"gps_signal": clean_int(p.get("gpsSignal")),
"gps_num": gps_num,
"elec_quantity": clean_num(p.get("electQuantity")),
"power_value": clean_num(p.get("powerValue")),
"battery_power_val": clean_num(p.get("batteryPowerVal")),
"tracker_oil": clean(p.get("trackerOil")),
"temperature": clean_num(p.get("temperature")),
"device_status": clean(p.get("status")),
"loc_desc": clean(p.get("locDesc")),
},
) )
ON CONFLICT (imei) DO UPDATE SET
geom=EXCLUDED.geom, lat=EXCLUDED.lat, lng=EXCLUDED.lng,
gps_time=EXCLUDED.gps_time, speed=EXCLUDED.speed, direction=EXCLUDED.direction,
acc_status=EXCLUDED.acc_status, current_mileage=EXCLUDED.current_mileage,
updated_at=NOW()
""", (
imei, lng, lat, lat, lng, clean(p.get("posType")), clean_int(p.get("confidence")),
gps_time, clean_ts(p.get("hbTime")), speed,
direction, acc_status, clean_int(p.get("gpsSignal")),
gps_num, clean_num(p.get("electQuantity")), clean_num(p.get("powerValue")),
clean_num(p.get("batteryPowerVal")), clean(p.get("trackerOil")), clean_num(p.get("temperature")),
current_mileage, clean(p.get("status")), clean(p.get("locDesc"))
))
upserted += cur.rowcount
# History (Hypertable Source) # History (Hypertable Source)
if gps_time: if gps_time:
@ -513,44 +510,39 @@ def get_device_locations(imeis: list) -> int:
if not imei or not is_valid_fix(lat, lng): if not imei or not is_valid_fix(lat, lng):
continue continue
# [FIX-M20] FK guard — this path can see IMEIs the daily cur.execute("""
# sync_devices hasn't picked up yet (especially when used INSERT INTO tracksolid.live_positions (
# as the stale-IMEI rescue path). imei, geom, lat, lng, speed, direction,
ensure_device(cur, imei, clean(p.get("deviceName"))) gps_time, acc_status, current_mileage, recorded_at
) VALUES (
upserted += upsert_live_position( %s, ST_SetSRID(ST_MakePoint(%s, %s), 4326),
cur, imei, lat, lng, %s, %s, %s, %s, %s, %s, %s, NOW()
clean_ts(p.get("gpsTime")),
speed=clean_num(p.get("speed")),
direction=clean_num(p.get("direction")),
acc_status=clean(p.get("accStatus")),
current_mileage=clean_num(p.get("currentMileage")),
) )
ON CONFLICT (imei) DO UPDATE SET
geom = EXCLUDED.geom,
lat = EXCLUDED.lat,
lng = EXCLUDED.lng,
speed = EXCLUDED.speed,
direction = EXCLUDED.direction,
gps_time = EXCLUDED.gps_time,
acc_status = EXCLUDED.acc_status,
current_mileage = EXCLUDED.current_mileage,
updated_at = NOW()
""", (
imei, lng, lat, lat, lng,
clean_num(p.get("speed")),
clean_num(p.get("direction")),
clean_ts(p.get("gpsTime")),
clean(p.get("accStatus")),
clean_num(p.get("currentMileage")),
))
upserted += 1
conn.commit() conn.commit()
log.info("get_device_locations: %d positions refreshed.", upserted) log.info("get_device_locations: %d positions refreshed.", upserted)
return upserted return upserted
# ── 7. Stale-IMEI Recovery — POLL-04 ─────────────────────────────────────────
def poll_stale_locations():
"""[FIX-M20] Refresh live_positions for IMEIs whose stored gps_time is
missing or older than 30 minutes.
Complements poll_live_positions (the 60s sweep), which silently omits
devices Jimi's location.list endpoint doesn't return. jimi.device.location.get
returns *last-known* fix per IMEI, so this path can re-warm devices
the sweep has dropped.
"""
stale = get_stale_imeis(stale_minutes=30)
if not stale:
log.info("poll_stale_locations: no stale IMEIs.")
return
log.info("poll_stale_locations: refreshing %d stale IMEI(s).", len(stale))
get_device_locations(stale)
# ── Main Loop ───────────────────────────────────────────────────────────────── # ── Main Loop ─────────────────────────────────────────────────────────────────
def main(): def main():
@ -562,14 +554,12 @@ def main():
safe_task(poll_trips, log)() safe_task(poll_trips, log)()
safe_task(poll_parking, log)() safe_task(poll_parking, log)()
safe_task(poll_track_list, log)() safe_task(poll_track_list, log)()
safe_task(poll_stale_locations, log)()
# Schedule # Schedule
schedule.every(60).seconds.do(safe_task(poll_live_positions, log)) schedule.every(60).seconds.do(safe_task(poll_live_positions, log))
schedule.every(15).minutes.do(safe_task(poll_trips, log)) schedule.every(15).minutes.do(safe_task(poll_trips, log))
schedule.every(15).minutes.do(safe_task(poll_parking, log)) schedule.every(15).minutes.do(safe_task(poll_parking, log))
schedule.every(30).minutes.do(safe_task(poll_track_list, log)) # [FIX-M14] schedule.every(30).minutes.do(safe_task(poll_track_list, log)) # [FIX-M14]
schedule.every(10).minutes.do(safe_task(poll_stale_locations, log)) # [FIX-M20]
schedule.every().day.at("02:00").do(safe_task(sync_devices, log)) schedule.every().day.at("02:00").do(safe_task(sync_devices, log))
while True: while True:

View file

@ -107,27 +107,3 @@ WEBHOOK_ALARM_NULL_TYPE = {
"alarmType": None, "alarmType": None,
"gateTime": "2024-04-12 07:30:00", "gateTime": "2024-04-12 07:30:00",
} }
# Alarm with no lat/lng — cross-feed (FIX-M20) must skip live_positions
# but still write the alarm row.
WEBHOOK_ALARM_NO_POSITION = {
"deviceImei": "123456789012345",
"alarmType": "4",
"alarmName": "Speeding",
"gateTime": "2024-04-12 07:30:00",
"lat": None,
"lng": None,
"speed": 0.0,
}
# Alarm with Zero-Island (0, 0) coordinates — is_valid_fix must reject;
# alarm row still writes, live_positions cross-feed must NOT fire.
WEBHOOK_ALARM_ZERO_ISLAND = {
"deviceImei": "123456789012345",
"alarmType": "4",
"alarmName": "Speeding",
"gateTime": "2024-04-12 07:30:00",
"lat": 0,
"lng": 0,
"speed": 0.0,
}

View file

@ -20,8 +20,6 @@ import webhook_receiver_rev
from tests.fixtures.api_responses import ( from tests.fixtures.api_responses import (
WEBHOOK_ALARM_PAYLOAD, WEBHOOK_ALARM_PAYLOAD,
WEBHOOK_ALARM_NULL_TYPE, WEBHOOK_ALARM_NULL_TYPE,
WEBHOOK_ALARM_NO_POSITION,
WEBHOOK_ALARM_ZERO_ISLAND,
WEBHOOK_TRIP_BCD_PAYLOAD, WEBHOOK_TRIP_BCD_PAYLOAD,
WEBHOOK_TRIP_ISO_PAYLOAD, WEBHOOK_TRIP_ISO_PAYLOAD,
WEBHOOK_OBD_PAYLOAD, WEBHOOK_OBD_PAYLOAD,
@ -98,50 +96,6 @@ class TestPushAlarm:
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["code"] == 0 assert response.json()["code"] == 0
def test_alarm_cross_feeds_live_position(self, client, mock_db):
"""FIX-M20: a valid alarm must additionally upsert live_positions."""
mock_conn, mock_cur = mock_db
data_list = json.dumps([WEBHOOK_ALARM_PAYLOAD])
response = client.post("/pushalarm", data={"token": "", "data_list": data_list})
assert response.status_code == 200
# Exactly one INSERT INTO live_positions should have fired.
lp_inserts = [
c for c in mock_cur.execute.call_args_list
if "tracksolid.live_positions" in str(c) and "INSERT" in str(c)
]
assert len(lp_inserts) == 1, "Cross-feed must upsert live_positions exactly once"
def test_alarm_without_lat_lng_skips_cross_feed(self, client, mock_db):
"""FIX-M20: an alarm without lat/lng must NOT touch live_positions
(but must still insert the alarm row)."""
mock_conn, mock_cur = mock_db
data_list = json.dumps([WEBHOOK_ALARM_NO_POSITION])
response = client.post("/pushalarm", data={"token": "", "data_list": data_list})
assert response.status_code == 200
lp_inserts = [
c for c in mock_cur.execute.call_args_list
if "tracksolid.live_positions" in str(c) and "INSERT" in str(c)
]
assert len(lp_inserts) == 0, "No live_positions write without a valid fix"
alarm_inserts = [
c for c in mock_cur.execute.call_args_list
if "tracksolid.alarms" in str(c) and "INSERT" in str(c)
]
assert len(alarm_inserts) == 1, "Alarm row must still write"
def test_alarm_with_zero_island_skips_cross_feed(self, client, mock_db):
"""FIX-M20: a (0, 0) fix must NOT propagate to live_positions —
is_valid_fix in the shared helper guards Zero Island."""
mock_conn, mock_cur = mock_db
data_list = json.dumps([WEBHOOK_ALARM_ZERO_ISLAND])
response = client.post("/pushalarm", data={"token": "", "data_list": data_list})
assert response.status_code == 200
lp_inserts = [
c for c in mock_cur.execute.call_args_list
if "tracksolid.live_positions" in str(c) and "INSERT" in str(c)
]
assert len(lp_inserts) == 0, "Zero-Island fix must not reach live_positions"
class TestPushTripReport: class TestPushTripReport:
def test_bcd_timestamp_parsed(self, client, mock_db): def test_bcd_timestamp_parsed(self, client, mock_db):

View file

@ -1,112 +0,0 @@
"""Unit tests for the FIX-M20 stale-IMEI recovery helpers.
Covers:
- ts_shared_rev.get_stale_imeis the SQL selector
- ingest_movement_rev.poll_stale_locations the scheduler wrapper
"""
import sys
import os
import pytest
from unittest.mock import MagicMock, patch
from contextlib import contextmanager
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
os.environ.setdefault("TRACKSOLID_APP_KEY", "test_key")
os.environ.setdefault("TRACKSOLID_APP_SECRET", "test_secret")
os.environ.setdefault("TRACKSOLID_USER_ID", "test_user")
os.environ.setdefault("TRACKSOLID_PWD_MD5", "test_md5")
os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost:5432/test")
def _make_mock_conn(rows=None):
"""Cursor/connection double that returns `rows` from fetchall()."""
mock_cur = MagicMock()
mock_cur.fetchall.return_value = rows or []
mock_conn = MagicMock()
mock_conn.cursor.return_value.__enter__ = lambda s: mock_cur
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
return mock_conn, mock_cur
@contextmanager
def _conn_ctx(mock_conn):
yield mock_conn
class TestGetStaleImeis:
def test_returns_devices_with_old_gps_time(self):
from ts_shared_rev import get_stale_imeis
mock_conn, mock_cur = _make_mock_conn(rows=[("imei_a",), ("imei_b",)])
with patch("ts_shared_rev.get_conn") as mock_get_conn:
mock_get_conn.return_value = _conn_ctx(mock_conn)
result = get_stale_imeis(stale_minutes=30)
assert result == ["imei_a", "imei_b"]
sql = str(mock_cur.execute.call_args)
assert "enabled_flag = 1" in sql
assert "INTERVAL" in sql.upper() or "interval" in sql
assert "NULLS FIRST" in sql
def test_returns_empty_when_no_stale(self):
from ts_shared_rev import get_stale_imeis
mock_conn, _ = _make_mock_conn(rows=[])
with patch("ts_shared_rev.get_conn") as mock_get_conn:
mock_get_conn.return_value = _conn_ctx(mock_conn)
assert get_stale_imeis() == []
class TestPollStaleLocations:
def test_noop_when_empty(self):
"""If no IMEIs are stale, get_device_locations must NOT be called."""
import ingest_movement_rev as mod
with patch.object(mod, "get_stale_imeis", return_value=[]), \
patch.object(mod, "get_device_locations") as mock_refresh:
mod.poll_stale_locations()
mock_refresh.assert_not_called()
def test_invokes_refresh_with_stale_list(self):
"""When stale IMEIs are present, get_device_locations is called once
with the exact list returned by get_stale_imeis."""
import ingest_movement_rev as mod
stale = ["imei_a", "imei_b", "imei_c"]
with patch.object(mod, "get_stale_imeis", return_value=stale), \
patch.object(mod, "get_device_locations") as mock_refresh:
mod.poll_stale_locations()
mock_refresh.assert_called_once_with(stale)
class TestUpsertLivePositionGuards:
"""Spot-checks on the time-guard contract — covers what the dashboard
relies on (no rewinding the marker on stale alarm arrivals)."""
def test_skips_invalid_fix(self):
from ts_shared_rev import upsert_live_position
mock_cur = MagicMock()
# Zero-Island
assert upsert_live_position(mock_cur, "imei_x", 0, 0, "2026-05-21 10:00:00") == 0
mock_cur.execute.assert_not_called()
def test_skips_missing_gps_time(self):
from ts_shared_rev import upsert_live_position
mock_cur = MagicMock()
assert upsert_live_position(mock_cur, "imei_x", -1.29, 36.82, None) == 0
mock_cur.execute.assert_not_called()
def test_skips_missing_imei(self):
from ts_shared_rev import upsert_live_position
mock_cur = MagicMock()
assert upsert_live_position(mock_cur, None, -1.29, 36.82, "2026-05-21 10:00:00") == 0
mock_cur.execute.assert_not_called()
def test_executes_upsert_for_valid_fix(self):
from ts_shared_rev import upsert_live_position
mock_cur = MagicMock()
mock_cur.rowcount = 1
n = upsert_live_position(mock_cur, "imei_x", -1.29, 36.82, "2026-05-21 10:00:00",
speed=42.5)
assert n == 1
mock_cur.execute.assert_called_once()
sql = str(mock_cur.execute.call_args)
# The time-guard is the load-bearing detail — verify it's present.
assert "EXCLUDED.gps_time > tracksolid.live_positions.gps_time" in sql

View file

@ -235,138 +235,6 @@ def get_active_imeis() -> list[str]:
cur.execute("SELECT imei FROM tracksolid.devices WHERE enabled_flag = 1") cur.execute("SELECT imei FROM tracksolid.devices WHERE enabled_flag = 1")
return [r[0] for r in cur.fetchall()] return [r[0] for r in cur.fetchall()]
def get_stale_imeis(stale_minutes: int = 30) -> list[str]:
"""[FIX-M20] IMEIs whose live_positions fix is missing or older than N minutes.
Used by poll_stale_locations() to feed get_device_locations() with the
set the 60s sweep silently dropped. Ordered oldest-first (NULLs first)
so worst-offenders get the first seats in each 50-IMEI batch.
"""
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT d.imei
FROM tracksolid.devices d
LEFT JOIN tracksolid.live_positions lp USING (imei)
WHERE d.enabled_flag = 1
AND (lp.gps_time IS NULL
OR lp.gps_time < NOW() - (%s || ' minutes')::interval)
ORDER BY lp.gps_time ASC NULLS FIRST
""", (str(stale_minutes),))
return [r[0] for r in cur.fetchall()]
def ensure_device(cur, imei: str, device_name: Optional[str] = None) -> None:
"""[FIX-M20] Upsert a stub row into tracksolid.devices so FK-constrained
inserts don't fail when ingest paths see an IMEI before sync_devices does.
Lifted out of webhook_receiver_rev.py to be shareable by every writer
of live_positions / alarms / position_history. Idempotent.
"""
cur.execute(
"""
INSERT INTO tracksolid.devices (imei, device_name, status, created_at, updated_at)
VALUES (%s, %s, 'unknown', NOW(), NOW())
ON CONFLICT (imei) DO NOTHING
""",
(imei, device_name),
)
def upsert_live_position(
cur,
imei: str,
lat,
lng,
gps_time,
speed=None,
direction=None,
acc_status=None,
current_mileage=None,
extras: Optional[dict] = None,
) -> int:
"""[FIX-M20] Time-guarded upsert into tracksolid.live_positions.
Only overwrites the stored row when the incoming gps_time is strictly
newer than what's already there. NULL stored gps_time always loses
(any fix beats no fix). Returns 1 if a row was written/updated, else 0.
`extras` carries the columns only the 60s sweep emits
(pos_type, confidence, hb_time, gps_signal, gps_num, elec_quantity,
power_value, battery_power_val, tracker_oil, temperature,
device_status, loc_desc). When omitted, those columns are left alone
on update via COALESCE so a sparse caller (e.g. alarm cross-feed)
doesn't blank them out.
"""
if not imei or not gps_time or not is_valid_fix(lat, lng):
return 0
extras = extras or {}
cur.execute("""
INSERT INTO tracksolid.live_positions (
imei, geom, lat, lng, gps_time, speed, direction,
acc_status, current_mileage,
pos_type, confidence, hb_time, gps_signal, gps_num,
elec_quantity, power_value, battery_power_val,
tracker_oil, temperature, device_status, loc_desc,
recorded_at
) VALUES (
%(imei)s,
ST_SetSRID(ST_MakePoint(%(lng)s, %(lat)s), 4326),
%(lat)s, %(lng)s, %(gps_time)s, %(speed)s, %(direction)s,
%(acc_status)s, %(current_mileage)s,
%(pos_type)s, %(confidence)s, %(hb_time)s, %(gps_signal)s, %(gps_num)s,
%(elec_quantity)s, %(power_value)s, %(battery_power_val)s,
%(tracker_oil)s, %(temperature)s, %(device_status)s, %(loc_desc)s,
NOW()
)
ON CONFLICT (imei) DO UPDATE SET
geom = EXCLUDED.geom,
lat = EXCLUDED.lat,
lng = EXCLUDED.lng,
gps_time = EXCLUDED.gps_time,
speed = COALESCE(EXCLUDED.speed, tracksolid.live_positions.speed),
direction = COALESCE(EXCLUDED.direction, tracksolid.live_positions.direction),
acc_status = COALESCE(EXCLUDED.acc_status, tracksolid.live_positions.acc_status),
current_mileage = COALESCE(EXCLUDED.current_mileage, tracksolid.live_positions.current_mileage),
pos_type = COALESCE(EXCLUDED.pos_type, tracksolid.live_positions.pos_type),
confidence = COALESCE(EXCLUDED.confidence, tracksolid.live_positions.confidence),
hb_time = COALESCE(EXCLUDED.hb_time, tracksolid.live_positions.hb_time),
gps_signal = COALESCE(EXCLUDED.gps_signal, tracksolid.live_positions.gps_signal),
gps_num = COALESCE(EXCLUDED.gps_num, tracksolid.live_positions.gps_num),
elec_quantity = COALESCE(EXCLUDED.elec_quantity, tracksolid.live_positions.elec_quantity),
power_value = COALESCE(EXCLUDED.power_value, tracksolid.live_positions.power_value),
battery_power_val = COALESCE(EXCLUDED.battery_power_val, tracksolid.live_positions.battery_power_val),
tracker_oil = COALESCE(EXCLUDED.tracker_oil, tracksolid.live_positions.tracker_oil),
temperature = COALESCE(EXCLUDED.temperature, tracksolid.live_positions.temperature),
device_status = COALESCE(EXCLUDED.device_status, tracksolid.live_positions.device_status),
loc_desc = COALESCE(EXCLUDED.loc_desc, tracksolid.live_positions.loc_desc),
updated_at = NOW()
WHERE EXCLUDED.gps_time IS NOT NULL
AND (tracksolid.live_positions.gps_time IS NULL
OR EXCLUDED.gps_time > tracksolid.live_positions.gps_time)
""", {
"imei": imei,
"lat": lat,
"lng": lng,
"gps_time": gps_time,
"speed": speed,
"direction": direction,
"acc_status": acc_status,
"current_mileage": current_mileage,
"pos_type": extras.get("pos_type"),
"confidence": extras.get("confidence"),
"hb_time": extras.get("hb_time"),
"gps_signal": extras.get("gps_signal"),
"gps_num": extras.get("gps_num"),
"elec_quantity": extras.get("elec_quantity"),
"power_value": extras.get("power_value"),
"battery_power_val": extras.get("battery_power_val"),
"tracker_oil": extras.get("tracker_oil"),
"temperature": extras.get("temperature"),
"device_status": extras.get("device_status"),
"loc_desc": extras.get("loc_desc"),
})
return cur.rowcount
def get_active_imeis_by_target() -> dict[str, list[str]]: def get_active_imeis_by_target() -> dict[str, list[str]]:
"""[FIX-M19] Group active IMEIs by their Tracksolid sub-account so """[FIX-M19] Group active IMEIs by their Tracksolid sub-account so
endpoints that require an `account`/`target` param (e.g. parking) can endpoints that require an `account`/`target` param (e.g. parking) can

View file

@ -55,8 +55,6 @@ from ts_shared_rev import (
clean_ts, clean_ts,
is_valid_fix, is_valid_fix,
get_logger, get_logger,
ensure_device,
upsert_live_position,
) )
log = get_logger("webhook") log = get_logger("webhook")
@ -180,11 +178,22 @@ def _make_geom_params(lat, lng):
return (lng, lat, lng, lat) return (lng, lat, lng, lat)
# Backwards-compat shim. The implementation was relocated to ts_shared_rev def _ensure_device(cur, imei: str, device_name: Optional[str] = None) -> None:
# (as `ensure_device`) so ingest_movement_rev and any future writer can share """Upsert a stub row into tracksolid.devices so FK-constrained inserts don't fail.
# the FK-guard without re-defining it. Existing call sites in this file
# continue to use the underscore-prefixed name. Jimi pushes alarms/GPS for devices that may not yet be in our devices table
_ensure_device = ensure_device (neither API-sync'd nor in the onboarding CSV). Rather than drop the event,
register the IMEI with whatever context the push carried; the nightly
`sync_devices()` and the CSV import fill in the remaining fields later.
"""
cur.execute(
"""
INSERT INTO tracksolid.devices (imei, device_name, status, created_at, updated_at)
VALUES (%s, %s, 'unknown', NOW(), NOW())
ON CONFLICT (imei) DO NOTHING
""",
(imei, device_name),
)
# ── Health Check ────────────────────────────────────────────────────────────── # ── Health Check ──────────────────────────────────────────────────────────────
@ -383,16 +392,6 @@ async def push_alarm(request: Request):
lat, lng, lat, lng,
clean_num(item.get("speed")), clean_num(item.get("speed")),
)) ))
# [FIX-M20] Cross-feed: every Jimi alarm carries lat/lng.
# Refresh live_positions so dashboard markers don't have to
# wait up to 60s for the next polled sweep. Time-guarded
# inside the helper — alarms older than the current fix lose.
upsert_live_position(
cur, imei, lat, lng, alarm_time,
speed=clean_num(item.get("speed")),
)
cur.execute("RELEASE SAVEPOINT sp") cur.execute("RELEASE SAVEPOINT sp")
inserted += 1 inserted += 1
except Exception: except Exception: