Files
doormile_milderapp/lib/data/heartbeat.dart
2026-08-28 18:16:28 +05:30

90 lines
4.7 KiB
Dart

/// ─────────────────────────────────────────────────────────────────────────
/// 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';