/// ───────────────────────────────────────────────────────────────────────── /// HOW OFTEN THE RIDER LOG BEATS /// /// ── Why this constant exists ── /// /// The heartbeat's cadence came from one place: `logseconds`, a field the /// **legacy** login returned and the app persisted at sign-in. The v1 contract /// at `api.doormile.com` does not send it — `POST /miler/verify-pin` answers /// with `{success, token, user:{…, profile:{…}}}` and nothing about logging /// cadence — so the adapter in `AuthProvider` had no value to map and wrote the /// `?? 0` fallback into prefs. /// /// Zero was then read as an instruction rather than as an absence. Both call /// sites treated it as "do not beat": /// /// • `RiderLogController.setOnDuty` started the loop only `if (interval > 0)`. /// • `startAutoCreateLoginLoop` computed `baseInterval = 0` and returned at /// `if (interval <= 0)`. /// /// So on every v1 deployment the periodic loop never started. What went with /// it was not just telemetry: `_heartbeat()` in the rider-log provider is the /// one place that writes **both** `POST /miler/logs` (the trail the console's /// Battery, Charging, Connection, GPS Accuracy and Location Service columns are /// read from) and `PUT /miler/location` (the Redis geo-index dispatch searches /// to find a rider at all). A rider on duty was reporting neither, and the /// console drew an em dash against a phone that was measuring all of it /// correctly — see [DeviceTelemetry], which was never the problem. /// /// The only reason it was not total silence is that a live pickup forces the /// interval to 30 by a separate path, so a rider mid-collection beat and a /// rider between stops did not. /// /// ── Why 30 seconds ── /// /// It is the cadence the rest of the app already assumes when nobody has told /// it otherwise: the pickup log's own `_getLogInterval` falls back to 30, and /// both controllers hard-code 30 for the live-pickup case. Matching it means a /// rider's location trail has one shape rather than two, and a hub that later /// starts sending `logseconds` still wins — this is a floor under a missing /// answer, not a replacement for a real one. /// ───────────────────────────────────────────────────────────────────────── library; /// Seconds between rider-log heartbeats when the backend has not said. const int kDefaultLogSeconds = 30; /// The heartbeat interval to actually use, given whatever the backend said. /// /// A positive value is honoured exactly. `null`, a zero, a negative, and a /// string that is none of those all mean *the backend did not answer*, and the /// answer to that is [kDefaultLogSeconds] — never zero, because zero is what /// stopped the heartbeat starting in the first place. /// /// Accepts an [Object] rather than an `int?` because the value arrives from two /// different shapes: `prefs.getInt('logseconds')` gives an `int?`, while the /// login envelope's `details['logseconds']` is untyped JSON and has been seen /// as both a number and a string. int resolveLogSeconds(Object? configured) { final int? parsed = switch (configured) { final int n => n, final num n => n.toInt(), final String s => int.tryParse(s.trim()), _ => null, }; return (parsed != null && parsed > 0) ? parsed : kDefaultLogSeconds; } /// The `status` a heartbeat reports, given whether the rider has live work. /// /// ── `active` and `idle` are not words this contract knows ── /// /// Four call sites built this string inline as `hasActivePickups ? 'active' : /// 'idle'`, and neither value appears anywhere in the API. `status` on /// `POST /miler/logs` takes an **availability** value — the set /// [MilerApi.availabilityStatuses] lists, and the same set /// `PUT /miler/availability` validates: /// /// Offline · Available · Assigned · On_Pickup · At_Customer · /// Picked_Up · On_Delivery · Break · Blocked /// /// This backend rejects an unrecognised enum **silently** — the `Break` / /// `On_Break` note in [MilerApi] records the last time the obvious guess was /// quietly dropped — so a bad `status` risks the whole row, and takes the /// battery, the connection and the location reading down with it. /// /// A rider working a counter is `On_Pickup`; a rider between stops is /// `Available`. Both are values the console already renders. String heartbeatStatus({required bool hasActiveWork}) => hasActiveWork ? 'On_Pickup' : 'Available';