Measure a reservation's real kilometres against the Traccar tracker #2
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/gps-mileage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
trip.distancewas purely declarative: whoever booked the car typed in how farthey drove, nothing checked it, and 4 of the 22 existing reservations have NULL
there. The tracker has been logging since 2026-06-19, so the real figure is
knowable. A pass now records it in new gps_* columns beside the declared one,
which is never overwritten — /api/trips/total and the ICS feed are untouched.
The measured window is deliberately NOT the declared start/end: people take the
car early and bring it back late, and measuring [12:00, 12:00] silently loses
those kilometres. So a reservation is widened to its whole days in Europe/Paris
and then clamped to the midpoint of the gap with the nearest non-overlapping
neighbour. A lone midday booking is measured 00:00–24:00; two bookings sharing a
day (08:00–12:00 and 14:00–20:00) split it at 13:00 and their windows touch
exactly, so every kilometre is counted once and none is lost.
That rests on a property of Traccar verified against the live server: its
reports/summary clamps at the window bounds, so adjacent windows partition the
distance exactly. Splitting 2026-08-17 into halves gave 100 + 156000 = 156100 m,
the full day, and the afternoon into thirds gave 72700 + 83300 + 0 = 156000.
Design notes:
haversine-summing: one small response instead of thousands of rows, the
device's own odometer instead of straight lines between sparse fixes, and no
exposure to the streamed-body hazard TraccarCarLocationService documents.
Against reports/trips: that sums 100 m lower on the 17th, because its trip
detection drops sub-threshold movement.
Accept: application/jsonis mandatory on the report endpoints. Without itTraccar answers 200 with an XLSX spreadsheet, which then fails to decode as
JSON for reasons that look nothing like the cause. The header now lives in
TraccarHttp, extracted from fetchPositions so the load-bearing "read the body
inside ZIO.scoped" constraint exists in exactly one place.
that one answers "where is the car", this one "how far did it go", and keeping
them apart leaves the parking tests' FakeCarLocationService untouched.
dark) is an answer, so it is stamped with no distance and not retried; an
unreachable Traccar writes nothing, so the next pass picks the reservation up.
widening pushes the window past midnight, so a booking that ended at noon
today is not measurable until tomorrow.
updated_at = NOW(), so ameasurement never bumps the ICS LAST-MODIFIED stamp, and updateTrip's explicit
column list means a client PUTting a Trip cannot spoof a measurement.
either side, including already-measured ones and ones before the cut-off.
Traccar outage costs one pass, not the fiber for the process lifetime.
Tests: 8 cases on the window rule (whole-day widening, same-day midpoint split
with touching seams, the existing 00:00–23:59 midnight handover losing only 30 s,
multi-day, overlapping bookings left unclamped, no neighbour, never collapsing),
10 on the pass (cut-off, unclosed window, the widened window actually being
queried, declared value left alone, no re-measure, dark tracker stamped once,
failure retried, force), and 5 on Postgres over the new test_trip_gps schema
including that updated_at does not move and updateTrip cannot overwrite.
Verified end to end against the live tracker: two bookings sharing 2026-08-17
came out 0 km and 156 km over windows meeting at 11:00Z — the day's exact total,
with both declared values untouched.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com