updated the sheet

This commit is contained in:
2026-08-28 18:16:28 +05:30
parent 69f4f3e909
commit 074cc0eccf
43 changed files with 2617 additions and 1201 deletions

View File

@@ -11,7 +11,7 @@ import 'package:miler/data/miler_api.dart';
///
/// A `_buildSslBypassClient()` that returned an `IOClient` whose
/// `badCertificateCallback` returned `true` for every certificate, pinned a
/// hardcoded IP for `queue.workolik.com`, and hand-rolled its own
/// hardcoded IP for the retired backend's host, and hand-rolled its own
/// `SecureSocket` upgrade. It existed to work around a carrier's broken DNS on
/// a backend that is no longer deployed — and it meant every request it carried
/// could be read and rewritten by anything on the path, including a rider's GPS
@@ -33,6 +33,132 @@ String _s(dynamic v) => v == null ? '' : v.toString();
double? _d(dynamic v) => double.tryParse(_s(v));
/// ─────────────────────────────────────────────────────────────────────────
/// WHY THE CONSOLE'S DEVICE PANEL WAS EMPTY
///
/// Not the telemetry. [DeviceTelemetry] reads the handset correctly, its
/// payload carries every field the console draws, and every caller spreads it
/// into the heartbeat. The reading was right and it never left the phone.
///
/// [_heartbeat] is the only thing that posts `POST /miler/logs`, and it is also
/// the only thing that writes `PUT /miler/location`. Both were unreachable,
/// because the two entry points below gate on one locally cached number:
///
/// ```
/// if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
/// return _heartbeat(data);
/// ```
///
/// `dutylogid` is written in exactly one place — a successful
/// `POST /miler/duty/start` — and it is zeroed on going off duty. So there are
/// two ordinary ways to end up on duty with a zero, and both are permanent:
///
/// • **The server already has the rider on duty.** `duty/start` answers 400,
/// which [_startDuty] correctly treats as a reconciliation rather than a
/// failure and delegates to [_currentDuty] — which read `dutylogid` out of
/// the response and wrote it to `logid` and `logId` **and not to
/// `dutylogid`**. The gate stayed shut. Next tick: `duty/start` again, 400
/// again, reconcile again, still shut. Forever, and silently, because
/// nothing in that loop is an error. It happens after a reinstall, after
/// cleared storage, after an `endDuty` that never landed, and on any fresh
/// login for a rider the backend still has on shift.
///
/// • **The response spells the id differently.** [_startDuty] read one key on
/// one level. Anything else — `logid`, `id`, a top-level field rather than
/// one under `data` — parsed to zero and was *stored* as zero, which puts a
/// rider into the same permanent loop on his very first shift. This codebase
/// has been bitten by exactly this before; see `AuthProvider._tokenIn`,
/// which looks in nine places for the bearer for precisely this reason.
///
/// ── The design error underneath both ──
///
/// `POST /miler/logs` does not take a duty log id. Its contract is
/// `{logdate, latitude, longitude, battery, is_charging, connection,
/// location_service, accuracy, is_background}` — nothing about duty. Gating
/// telemetry on an identifier the endpoint does not want, that the backend is
/// not obliged to return, is what turned one unmapped field into total
/// silence on both writes.
///
/// So the gate is now *"has duty been established"* — a fact — rather than
/// *"do we happen to hold a non-zero id"*, which is an artefact. The id is
/// still stored when it arrives, because [_dutyEnvelope] and the legacy duty
/// readers want it; it simply no longer decides whether a rider reports.
/// ─────────────────────────────────────────────────────────────────────────
/// Set once duty is known to be on, and cleared when it goes off.
///
/// Deliberately separate from `dutylogid`: this answers "should the heartbeat
/// run", which is a question about duty, and the id answers "which duty log
/// row", which is a question about the backend's bookkeeping. Conflating them
/// is the bug above.
const String _kDutyEstablished = 'duty_established';
/// Every spelling and nesting the duty endpoints have used for the log id.
///
/// Ordered by how the current contract answers, then by what deployments have
/// been seen to send. A miss is no longer fatal — see [_dutyEstablished] — but
/// finding the id keeps the legacy `logid` readers correct.
const List<String> _dutyLogIdKeys = [
'dutylogid',
'dutyLogId',
'duty_log_id',
'logid',
'logId',
'log_id',
'id',
];
/// The duty log id anywhere in [res], or `0`.
///
/// Looks in the envelope's `data` and in the undecorated body, because a
/// handler that returns `{success, dutylogid}` and one that returns
/// `{success, data:{dutylogid}}` are both shapes this contract has worn.
int _dutyLogIdIn(ApiResult res) {
final sources = <Map>[res.map, if (res.raw is Map) res.raw as Map];
for (final source in sources) {
for (final key in _dutyLogIdKeys) {
final v = source[key];
if (v == null) continue;
final n = int.tryParse(v.toString().trim());
if (n != null && n > 0) return n;
}
}
return 0;
}
/// Records that the rider is on duty, and the id if the backend sent one.
///
/// The flag is set whether or not there is an id, which is the whole fix: a
/// rider whose duty the server has confirmed reports, even when the response
/// did not name the row.
Future<void> _rememberDuty(int dutyLogId) async {
final prefs = await SharedPreferences.getInstance();
if (dutyLogId > 0) {
await prefs.setInt('logid', dutyLogId);
await prefs.setInt('logId', dutyLogId);
await prefs.setInt('dutylogid', dutyLogId);
}
await prefs.setBool(_kDutyEstablished, true);
await prefs.setInt('onduty', 1);
}
/// Forgets duty, on the way off shift.
Future<void> _forgetDuty() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('dutylogid', 0);
await prefs.setBool(_kDutyEstablished, false);
}
/// Whether duty has been established, so the heartbeat should run.
Future<bool> _dutyEstablished() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool(_kDutyEstablished) == true) return true;
// An install that predates the flag: a stored id means duty was started by
// an earlier build, and that rider must not have to go off and on again to
// start reporting.
return (prefs.getInt('dutylogid') ?? 0) > 0;
}
/// Legacy `{status, details}` for the duty-state readers.
Map<String, dynamic> _dutyEnvelope(Map data) {
final bool on = data['onduty'] == true;
@@ -54,11 +180,11 @@ Future<Map<String, dynamic>?> _startDuty(Map data) async {
lon: _d(data['longitude']),
);
if (res.ok) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
await prefs.setInt('dutylogid', id);
// Looked at one key on one level and stored whatever it found, including
// a zero — which shut the heartbeat's gate permanently on a response that
// had merely spelled the id differently. See [_dutyLogIdIn].
final id = _dutyLogIdIn(res);
await _rememberDuty(id);
return {
'status': true,
'code': 200,
@@ -76,12 +202,19 @@ Future<Map<String, dynamic>?> _currentDuty() async {
if (!res.ok) return {'status': false, 'code': res.status};
final on = res.map['onduty'] == true;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
// ── The line that was missing ──
//
// This wrote the id to `logid` and `logId` and stopped, leaving
// `dutylogid` — the only key the heartbeat's gate reads — at zero. So the
// reconciliation that exists to recover from "the server already has you
// on duty" recovered the duty state and not the ability to report it, and
// the next tick came straight back here. See the note at the top.
await _rememberDuty(_dutyLogIdIn(res));
} else {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', 0);
await _forgetDuty();
}
return _dutyEnvelope(res.map);
}
@@ -108,11 +241,28 @@ Future<Map<String, dynamic>?> _heartbeat(Map data) async {
heading: _d(data['heading']),
);
// Deliberately not awaited into the result: a telemetry failure must never
// look like a location failure, because the caller treats the latter as duty
// state going wrong.
unawaited(
MilerApi.postLog(
// ── Awaited, but its outcome is still discarded ──
//
// This was `unawaited(...)`, for a good reason that turned out to be the
// wrong mechanism: a telemetry failure must never read as a location failure,
// because the caller treats the latter as duty state going wrong. `unawaited`
// achieves that by never looking at the result — and also by not waiting for
// the request to finish.
//
// On Android this heartbeat runs inside `flutter_foreground_task`'s **own
// Flutter engine**, spun up per tick. When the callback returns, that engine
// can be suspended before an in-flight future completes. So the awaited
// `PUT /miler/location` above landed on every tick and this `POST /miler/logs`
// was killed mid-flight — which is exactly what the backend saw: a rider with
// a live position in Redis and no new telemetry row behind it, while the app
// logged a successful heartbeat.
//
// Awaiting it costs one round trip on a call that already had to happen and
// guarantees the request outlives the tick. The isolation the `unawaited` was
// protecting is kept by the `try`: the result is logged and thrown away, and
// nothing below branches on it.
try {
final logRes = await MilerApi.postLog(
latitude: lat,
longitude: lon,
speed: _d(data['speed']),
@@ -137,8 +287,18 @@ Future<Map<String, dynamic>?> _heartbeat(Map data) async {
? null
: _s(data['location_service']),
isBackground: data['is_background'] == true,
),
);
);
// The one line that says whether telemetry reached the console. It was
// silent on success *and* on failure, which is why a dropped post took
// several rounds of backend archaeology to find.
debugPrint(
'[RIDERLOG][LOGS] POST /miler/logs -> ${logRes.status} '
'ok=${logRes.ok} battery=${_s(data['battery'])} '
'connection=${_s(data['connection'])}',
);
} catch (e) {
debugPrint('[RIDERLOG][LOGS] POST /miler/logs failed: $e');
}
return res.ok
? ApiConfig.okEnvelope()
@@ -158,13 +318,28 @@ bool _isOffDutyIntent(Map data) {
class CreateRiderLogProvider {
/// The first call of a session starts duty; every later one is a heartbeat.
///
/// ── The start is no longer a tick the rider loses ──
///
/// This returned [_startDuty]'s envelope and stopped, so the tick that opened
/// the shift carried no telemetry and no position — the console had nothing
/// until the next one, thirty seconds later. Worse, when `duty/start`
/// reconciled a rider the server already had on duty, the old gate never
/// opened at all and *every* tick ended here. See the note at the top of this
/// file.
///
/// Duty is established first, then the same payload beats immediately. The
/// heartbeat is best-effort and its result is not merged into the duty
/// envelope, because the caller reads that envelope for duty state and a
/// telemetry failure is not a duty failure.
Future<Map<String, dynamic>?> createRiderLog(
Map<String, dynamic> data,
) async {
final prefs = await SharedPreferences.getInstance();
final int existing = prefs.getInt('dutylogid') ?? 0;
if (existing <= 0) return _startDuty(data);
return _heartbeat(data);
if (await _dutyEstablished()) return _heartbeat(data);
final started = await _startDuty(data);
if (await _dutyEstablished()) await _heartbeat(data);
return started;
}
}
@@ -173,19 +348,25 @@ class UpdateRiderLogProvider {
Map<String, dynamic> data,
) async {
if (_isOffDutyIntent(data)) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('dutylogid', 0);
await _forgetDuty();
final res = await MilerApi.endDuty();
if (res.ok) await prefs.setInt('onduty', 0);
if (res.ok) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', 0);
}
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status};
}
final onduty = data['onduty'];
if (onduty == 1 || _s(onduty) == '1') {
final prefs = await SharedPreferences.getInstance();
if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
if ((onduty == 1 || _s(onduty) == '1') && !await _dutyEstablished()) {
// Same shape as [CreateRiderLogProvider.createRiderLog]: establish duty,
// then beat on the same tick rather than returning and leaving the
// console with nothing for another interval.
final started = await _startDuty(data);
if (await _dutyEstablished()) await _heartbeat(data);
return started;
}
return _heartbeat(data);
}

View File

@@ -3,6 +3,7 @@ import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:miler/Models/login/login.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/service_profile.dart';
@@ -93,7 +94,7 @@ class AuthProvider {
int? pin,
String? pinRaw,
}) async {
// The legacy `jupiter.doormile.app/.../rider/login` path that used to sit
// The legacy `rider/login` path on the retired backend that used to sit
// behind a flag here is gone with the rest of the old backend.
return _loginNew(
contactNo: contactNo,
@@ -303,6 +304,15 @@ class AuthProvider {
'logid': 0,
'partnerid': 0,
'configid': 0,
// ── Zero is not a safe default for this one ──
//
// Every other field here is an *identifier*, and an unknown identifier
// is honestly zero. `logseconds` is a **cadence**, and zero read as a
// cadence means "never beat" — which is exactly how both call sites in
// [RiderLogController] took it, so the rider-log loop never started on
// a v1 deployment and the console lost battery, connection, GPS and
// the rider's live position with it. See [kDefaultLogSeconds].
'logseconds': kDefaultLogSeconds,
// ── Which line of work this rider is on ──
//
// The server's answer first, the build's declared tenant only when
@@ -432,24 +442,6 @@ class AuthProvider {
}
}
// Convenience: send using a Login model body
Future<http.Response> loginWith(Login request) async {
final uri = Uri.parse(
'https://jupiter.doormile.app/live/api/v2/users/rider/login',
);
final body = request.toJson();
debugPrint('[AUTH][LOGIN] URL: ${uri.toString()}');
debugPrint('[AUTH][LOGIN] Body: ${json.encode(body)}');
final res = await http.post(
uri,
headers: {'Content-Type': 'application/json'},
body: json.encode(body),
);
debugPrint('[AUTH][LOGIN] Status: ${res.statusCode}');
debugPrint('[AUTH][LOGIN] Response: ${res.body}');
return res;
}
// Convenience: parsed response as Login model
Future<Login> loginParsed({
required String contactNo,
@@ -498,7 +490,14 @@ class AuthProvider {
await prefs.setInt('partnerid', details['partnerid'] ?? 0);
await prefs.setInt('partnerId', details['partnerid'] ?? 0);
await prefs.setInt('configid', details['configid'] ?? 0);
await prefs.setInt('logseconds', details['logseconds'] ?? 0);
// Not `?? 0`. This is read back as the heartbeat's interval, and a zero
// there stops the loop starting at all — see [resolveLogSeconds]. A
// legacy backend that sends a real cadence still wins; anything else
// lands on the 30s floor rather than on silence.
await prefs.setInt(
'logseconds',
resolveLogSeconds(details['logseconds']),
);
await prefs.setInt('locationid', details['locationid'] ?? 0);
await prefs.setInt('tenantid', details['tenantid'] ?? 0);

View File

@@ -15,7 +15,7 @@ import 'package:miler/data/accepted_store.dart';
/// ── What changed ──
///
/// This file used to carry two complete implementations of every call: a
/// legacy one that spoke to `queue.workolik.com` through a hand-built
/// legacy one that spoke to the retired backend through a hand-built
/// `IOClient` — one that disabled certificate checking, pinned a hardcoded IP
/// and did its own TLS upgrade to work around a carrier's broken DNS — and a
/// v1 one behind `if (ApiConfig.useNewApi)`. The legacy backend is gone, and

View File

@@ -2,12 +2,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:miler/Models/summary/pickupstats.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:flutter/foundation.dart';
class SummaryProvider {
final String baseUrl = ApiConstants.summaryApiLive;
Future<PickupStats?> fetchSummaryStats(int userId) => _fetchSummaryStatsNew();
/// `GET /miler/earnings?period=daily|weekly|monthly`