updated the sheet
This commit is contained in:
@@ -3,15 +3,15 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
import 'package:miler/data/device_telemetry.dart';
|
||||
import 'package:miler/providers/notifications/notificationservce.dart';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/utils/kalman_filter.dart';
|
||||
|
||||
/// Background service for managing active pickup logs
|
||||
@@ -20,7 +20,6 @@ class BackgroundCollectionLog {
|
||||
static DateTime? _lastUpdateTime;
|
||||
|
||||
static final CreatePickupLogProvider _logProvider = CreatePickupLogProvider();
|
||||
static final http.Client _httpClient = http.Client();
|
||||
static final FlutterTts _tts = FlutterTts();
|
||||
static final AudioPlayer _proximityPlayer = AudioPlayer();
|
||||
|
||||
@@ -60,38 +59,39 @@ class BackgroundCollectionLog {
|
||||
final now = DateTime.now();
|
||||
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
|
||||
|
||||
// Build API URL using v1 endpoint (getpickup)
|
||||
final bool isLive = ApiConstants.mainRoute == 'live';
|
||||
final baseUrl = isLive
|
||||
? ApiConstants.currentPickupLive
|
||||
: ApiConstants.currentPickupDev;
|
||||
|
||||
final uri = Uri.parse(baseUrl).replace(
|
||||
queryParameters: {
|
||||
'userid': userId.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[ACTIVE_COLLECTION_LOG][BG] Fetching Pickup: $uri');
|
||||
|
||||
// Fetch pickup from API using v1 endpoint
|
||||
final pickups = await _fetchPickupsFromApi(uri);
|
||||
// ── This asked the pre-v1 backend, with the wrong question ──
|
||||
//
|
||||
// It built a `pickups/getpickups` URL on the retired backend by hand
|
||||
// and fetched it with a bare `_httpClient.get` — **no bearer token**, and
|
||||
// `userid` in the query string, so the rider's day was readable by id
|
||||
// from an unauthenticated call.
|
||||
//
|
||||
// It could never have worked anyway. The filter below looked for
|
||||
// `orderstatus == 'active'`, and `active` is not a value this app's own
|
||||
// vocabulary contains: [ApiConfig.legacyStatusFromNew] maps the v1
|
||||
// statuses to `assigned` / `accepted` / `arrived` / `picked`, and never to
|
||||
// `active`. So on every tick this returned 0 items and fell through to
|
||||
// the shift-end check — which is exactly what the device log shows.
|
||||
//
|
||||
// `GET /miler/bookings` is the v1 question, asked with the token, about
|
||||
// the rider the token names. The filter is the real vocabulary: work the
|
||||
// rider has taken on and not yet collected.
|
||||
final res = await MilerApi.bookings(date: today);
|
||||
final pickups = res.list;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_COLLECTION_LOG][BG] API returned ${pickups.length} items',
|
||||
'[ACTIVE_COLLECTION_LOG][BG] /miler/bookings -> ${res.status}, '
|
||||
'${pickups.length} items',
|
||||
);
|
||||
|
||||
// Filter for active bookings only
|
||||
const inProgress = {'accepted', 'arrived'};
|
||||
final activeOrders = pickups.whereType<Map<String, dynamic>>().where((
|
||||
Booking,
|
||||
) {
|
||||
final status = (Booking['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isActive = status == 'active';
|
||||
return isActive;
|
||||
final status = ApiConfig.legacyStatusFromNew(
|
||||
(Booking['status'] ?? Booking['orderstatus'])?.toString(),
|
||||
);
|
||||
return inProgress.contains(status);
|
||||
}).toList();
|
||||
|
||||
if (activeOrders.isEmpty) {
|
||||
@@ -189,7 +189,9 @@ class BackgroundCollectionLog {
|
||||
// so every per-stop breadcrumb reached the console with a position and
|
||||
// nothing to explain a gap in the trail. Read from the same place the
|
||||
// rider heartbeat reads it. See [DeviceTelemetry].
|
||||
final telemetry = await DeviceTelemetry.read(locationService: coords['status']);
|
||||
final telemetry = await DeviceTelemetry.read(
|
||||
locationService: coords['status'],
|
||||
);
|
||||
|
||||
// Build payload with coordinates and timestamp
|
||||
payloadWithCoords = <String, dynamic>{
|
||||
@@ -211,11 +213,6 @@ class BackgroundCollectionLog {
|
||||
'logstatus': await _resolveLogStatus(orderId, coords),
|
||||
};
|
||||
|
||||
// Determine API endpoint
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createPickupLogLive
|
||||
: ApiConstants.createPickupLogDev;
|
||||
|
||||
// Post the log
|
||||
debugPrint('[ACTIVE_COLLECTION_LOG][BG] Posting pickup log for $orderId');
|
||||
|
||||
@@ -427,33 +424,6 @@ class BackgroundCollectionLog {
|
||||
}
|
||||
|
||||
/// Fetch pickup from API using v1 endpoint
|
||||
static Future<List<dynamic>> _fetchPickupsFromApi(Uri uri) async {
|
||||
try {
|
||||
final res = await _httpClient
|
||||
.get(uri)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) {
|
||||
return data['items'] as List;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to pad numbers with leading zero
|
||||
static String _pad(int n) => n.toString().padLeft(2, '0');
|
||||
@@ -533,10 +503,6 @@ class BackgroundCollectionLog {
|
||||
final List<String> remaining = [];
|
||||
bool anySuccess = false;
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createPickupLogLive
|
||||
: ApiConstants.createPickupLogDev;
|
||||
|
||||
for (final itemStr in queue) {
|
||||
try {
|
||||
final Map<String, dynamic> item = jsonDecode(itemStr);
|
||||
@@ -761,14 +727,18 @@ class BackgroundCollectionLog {
|
||||
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
|
||||
);
|
||||
|
||||
// 1. Get Required IDs
|
||||
// ── The four ids and the fix are no longer this function's business ──
|
||||
//
|
||||
// `userid`, `partnerid`, `shiftid`, `logid` and a position were assembled
|
||||
// here to fill a legacy break-log body. On v1 the break and the duty end
|
||||
// are both keyed on the **token**: `POST /miler/breaks/start` opens a
|
||||
// break for whoever the session names, and `PUT /miler/duty/end` closes
|
||||
// whichever duty log is open for them. Sending an id would not make it
|
||||
// more correct; it is what let the old call be made unauthenticated.
|
||||
//
|
||||
// `userid` is still read, as the one guard worth keeping: no signed-in
|
||||
// rider, nothing to end.
|
||||
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
final int partnerid =
|
||||
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
|
||||
final int shiftid =
|
||||
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
|
||||
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
|
||||
|
||||
if (userid == 0) {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
|
||||
@@ -776,61 +746,28 @@ class BackgroundCollectionLog {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Get Location
|
||||
final Map<String, String>? coords = await _getCoordinatesWithFallback();
|
||||
final String lat = coords?['lat'] ?? '0';
|
||||
final String lng = coords?['lng'] ?? '0';
|
||||
|
||||
// 3. Prepare Break Log Payload
|
||||
final now = DateTime.now();
|
||||
final int localBreakId =
|
||||
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
|
||||
final String breakdate =
|
||||
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
final String breakstart =
|
||||
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
|
||||
final payload = <String, dynamic>{
|
||||
"breakid": localBreakId,
|
||||
"logid": logid,
|
||||
"breakdate": breakdate,
|
||||
"userid": userid,
|
||||
"partnerid": partnerid,
|
||||
"shiftid": shiftid,
|
||||
"breakstart": breakstart,
|
||||
"breakend": "",
|
||||
"breakhours": 0.0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
|
||||
// 4. Call API to Create Break
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createBreakRiderLogLive
|
||||
: ApiConstants.createBreakRiderLogDev;
|
||||
|
||||
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
|
||||
|
||||
// We use a separate provider instance or http call if needed,
|
||||
// but _logProvider is for pickup logs. We need a generic post or use http directly.
|
||||
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
|
||||
// 4. Close the shift with a break, on v1.
|
||||
//
|
||||
// This hand-posted the retired backend's `partners/createbreaklog`
|
||||
// with a bare `_httpClient.post` and no bearer
|
||||
// token — an unauthenticated write keyed on a `userid` in the
|
||||
// body. `POST /miler/breaks/start` is the v1 endpoint, and
|
||||
// [MilerApi] attaches the session.
|
||||
debugPrint('[AUTO_SHIFT_END] Creating break log (v1)');
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(payload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
final res = await MilerApi.startBreak('Shift_End');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
if (res.ok) {
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
|
||||
|
||||
// Parse response to get server break ID if needed, but mainly we just need to go offline
|
||||
final body = jsonDecode(response.body);
|
||||
final det = (body['details'] is Map) ? body['details'] : body;
|
||||
final serverBreakId = det['breakid'];
|
||||
final serverBreakId = res.map['breaklogid'] ?? res.map['breakid'];
|
||||
|
||||
if (serverBreakId != null) {
|
||||
await prefs.setInt(
|
||||
@@ -842,7 +779,7 @@ class BackgroundCollectionLog {
|
||||
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
|
||||
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${res.status}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -854,27 +791,15 @@ class BackgroundCollectionLog {
|
||||
await prefs.setInt('onduty', 0);
|
||||
await prefs.setBool('online', false);
|
||||
|
||||
// 6. Update Rider Log (Set Duty = 0)
|
||||
// We should also update the main rider log to say onduty=0
|
||||
final updateUrl = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateRiderLogLive
|
||||
: ApiConstants.updateRiderLogDev;
|
||||
|
||||
final updatePayload = {
|
||||
"userid": userid,
|
||||
"onduty": 0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
// 6. End duty, on v1.
|
||||
//
|
||||
// Was an unauthenticated POST of `{userid, onduty: 0}` to the
|
||||
// legacy `updateriderlog`. `PUT /miler/duty/end` closes whichever
|
||||
// duty log is open for the token's rider, which is the same fact
|
||||
// without the id or the open door.
|
||||
|
||||
try {
|
||||
await _httpClient
|
||||
.post(
|
||||
Uri.parse(updateUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(updatePayload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
await MilerApi.endDuty();
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
|
||||
} catch (_) {}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/providers/Riderlog/riderlog_provider.dart';
|
||||
import 'package:miler/background/backgroundservice.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:miler/data/device_telemetry.dart';
|
||||
import 'package:miler/data/heartbeat.dart';
|
||||
import 'package:miler/utils/kalman_filter.dart';
|
||||
import 'package:miler/utils/mqtt_service.dart';
|
||||
import 'package:miler/views/helpers/constants/mqtt_constants.dart';
|
||||
@@ -149,7 +149,7 @@ class _BackgroundRiderLog {
|
||||
|
||||
// ✅ Check if there are active pickup to set status
|
||||
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
|
||||
final String riderStatus = hasActivePickups ? 'active' : 'idle';
|
||||
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
|
||||
|
||||
final now = DateTime.now();
|
||||
final iso = _formatDateTimeFull(now);
|
||||
@@ -226,9 +226,6 @@ class _BackgroundRiderLog {
|
||||
payload['lastname'] = lastName.trim();
|
||||
}
|
||||
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
|
||||
final provider = CreateRiderLogProvider();
|
||||
final resp = await provider.createRiderLog(payload);
|
||||
@@ -304,7 +301,7 @@ class _BackgroundRiderLog {
|
||||
|
||||
// 5. Lane: Status
|
||||
mqttService.updateStatus(
|
||||
riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline,
|
||||
riderStatus == 'On_Pickup' ? 'Active' : MqttConstants.statusOnline,
|
||||
);
|
||||
|
||||
// 6. Lane: Periodic Log (Comprehensive Snapshot)
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/background/foreground_service.dart' as fg;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:miler/data/accepted_store.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
|
||||
import 'dart:io';
|
||||
import 'package:minio/minio.dart';
|
||||
@@ -1203,12 +1202,6 @@ class PickupsController extends GetxController
|
||||
return "$y-$m-$d $hh:$mm:$ss";
|
||||
}
|
||||
|
||||
String _resolveUpdateUrl() {
|
||||
return ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updatePickupLive
|
||||
: ApiConstants.updatePickupDev;
|
||||
}
|
||||
|
||||
// ---------------- SharedPrefs helpers ----------------
|
||||
Future<void> setNotificationSent(bool value) async {
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:miler/data/miler_api.dart';
|
||||
///
|
||||
/// ── Two things this used to do and no longer does ──
|
||||
///
|
||||
/// It called a second, legacy backend (`jupiter.doormile.app/.../
|
||||
/// It called a second, legacy backend (the retired `.../
|
||||
/// getuserbonussummary`) when the new-API flag was off, and it filled an empty
|
||||
/// balance with seeded demo points in debug builds. Both are gone: there is one
|
||||
/// backend now, and a points balance is a number about somebody's pay — the
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/Models/riders/riders_models.dart';
|
||||
import 'package:miler/providers/Riderlog/riderlog_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -16,6 +15,7 @@ import 'package:miler/background/foreground_service.dart' as fg;
|
||||
import 'package:miler/utils/kalman_filter.dart';
|
||||
import 'package:miler/utils/mqtt_service.dart';
|
||||
import 'package:miler/data/device_telemetry.dart';
|
||||
import 'package:miler/data/heartbeat.dart';
|
||||
import 'package:miler/views/helpers/constants/mqtt_constants.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:miler/controllers/connectivity_mixin.dart';
|
||||
@@ -235,7 +235,7 @@ class RiderLogController extends GetxController
|
||||
|
||||
// ✅ Check if there are active pickup to set status
|
||||
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
|
||||
final String riderStatus = hasActivePickups ? 'active' : 'idle';
|
||||
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
|
||||
final String orderId = prefs.getString('current_riding_order_id') ?? '';
|
||||
debugPrint(
|
||||
'[RIDERLOG][CREATE LOGIN NOW] Active pickup: $hasActivePickups -> status: $riderStatus',
|
||||
@@ -315,7 +315,7 @@ class RiderLogController extends GetxController
|
||||
|
||||
// 1. Lane: Status
|
||||
mqttService.updateStatus(
|
||||
riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline,
|
||||
riderStatus == 'On_Pickup' ? 'Active' : MqttConstants.statusOnline,
|
||||
);
|
||||
|
||||
// 2. Lane: Profile (Send only if significantly changed or first time)
|
||||
@@ -448,7 +448,7 @@ class RiderLogController extends GetxController
|
||||
// Check status for offline fallback too
|
||||
final bool hasActivePickups =
|
||||
prefs.getBool('has_live_pickup') ?? false;
|
||||
final String riderStatus = hasActivePickups ? 'active' : 'idle';
|
||||
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
|
||||
|
||||
final Map<String, dynamic> payload = {
|
||||
...baseTemplate,
|
||||
@@ -520,10 +520,19 @@ class RiderLogController extends GetxController
|
||||
await prefs.setInt('onduty', on ? 1 : 0);
|
||||
if (on) {
|
||||
await createLoginNowV2();
|
||||
final int interval = prefs.getInt('logseconds') ?? 0;
|
||||
if (interval > 0) {
|
||||
startAutoCreateLoginLoop(seconds: interval);
|
||||
}
|
||||
// ── Going on duty always starts the heartbeat ──
|
||||
//
|
||||
// This was gated on `interval > 0`, reading a stored zero as a
|
||||
// decision not to beat. The v1 login has no `logseconds` to store, so
|
||||
// the gate was shut on every rider: duty went on, one log was posted
|
||||
// by the call above, and nothing followed it — no telemetry trail and
|
||||
// no location writes for the rest of the shift.
|
||||
//
|
||||
// There is no cadence at which a rider on duty should not report, so
|
||||
// there is no branch here any more. [resolveLogSeconds] supplies the
|
||||
// floor when the hub has not set one.
|
||||
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
|
||||
startAutoCreateLoginLoop(seconds: interval);
|
||||
|
||||
// Ensure foreground logging notification is started when going on-duty
|
||||
try {
|
||||
@@ -590,15 +599,45 @@ class RiderLogController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the periodic heartbeat is currently running on the stream path.
|
||||
///
|
||||
/// ── Why a test needs to be able to see this ──
|
||||
///
|
||||
/// The `logseconds` bug was invisible from outside the class. Duty went on,
|
||||
/// the API accepted it, one log was posted by the immediate call — and the
|
||||
/// loop that was supposed to follow it simply never started. Nothing threw,
|
||||
/// nothing logged an error, and the only symptom was a console panel that had
|
||||
/// been blank for so long it read as a backend problem.
|
||||
///
|
||||
/// Android normally runs the heartbeat as a foreground service instead, and
|
||||
/// leaves this null; the test host is never Android, so the stream path is
|
||||
/// the one under test. See [startAutoCreateLoginLoop].
|
||||
@visibleForTesting
|
||||
bool get autoLoopRunning => _autoLoginSubscription != null;
|
||||
|
||||
// Start periodic createRiderLog calls based on seconds (or prefs 'logseconds')
|
||||
// ✅ CRITICAL: When there are active pickup, use 30 seconds (same as pickup logs)
|
||||
// Otherwise, use the configured logseconds interval
|
||||
void startAutoCreateLoginLoop({int? seconds}) async {
|
||||
/// `Future<void>`, not `void`. It was fire-and-forget, which meant no caller
|
||||
/// could wait for the loop to actually be up — and the one place that most
|
||||
/// wanted to, [setOnDuty], had no way to tell whether the heartbeat it had
|
||||
/// just asked for existed. Every existing call site ignores the future and is
|
||||
/// unchanged by this.
|
||||
Future<void> startAutoCreateLoginLoop({int? seconds}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// ✅ Check if there are active pickup - if yes, use 30 seconds (same as pickup logs)
|
||||
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
|
||||
final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0);
|
||||
// ── The configured cadence, or the floor under it ──
|
||||
//
|
||||
// `?? 0` here was the second half of the same bug as the one in
|
||||
// [setOnDuty]: a caller that passes no `seconds` — the app-resume path in
|
||||
// `main.dart` is one — fell through to a stored zero and then returned at
|
||||
// the `interval <= 0` guard below, so the loop the resume was trying to
|
||||
// revive silently did not start. See [resolveLogSeconds].
|
||||
final int baseInterval = resolveLogSeconds(
|
||||
seconds ?? prefs.getInt('logseconds'),
|
||||
);
|
||||
|
||||
// When there are active pickup, post rider logs every 30 seconds (matching pickup logs)
|
||||
// Otherwise, use the configured interval
|
||||
@@ -610,12 +649,20 @@ class RiderLogController extends GetxController
|
||||
);
|
||||
}
|
||||
|
||||
await stopAutoCreateLoginLoop(); // Cancel existing subscription
|
||||
// Stop before start, which is what keeps this method safe to call from the
|
||||
// three places that do — going on duty, resuming the app, and a pickup
|
||||
// going live. Whichever arrives last wins, and there is never a second
|
||||
// loop: the subscription is cancelled and the foreground service stopped
|
||||
// before either is created again.
|
||||
await stopAutoCreateLoginLoop();
|
||||
|
||||
// Attempt to flush offline logs on loop start
|
||||
flushOfflineLogs();
|
||||
|
||||
if (interval <= 0) return;
|
||||
// No `interval <= 0` guard. [resolveLogSeconds] cannot return one, and the
|
||||
// guard that used to be here is what turned a missing `logseconds` into a
|
||||
// rider who never reported. Duty state decides whether the loop runs; the
|
||||
// cadence only decides how often.
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
if (onduty != 1) {
|
||||
debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty');
|
||||
@@ -1049,9 +1096,6 @@ class RiderLogController extends GetxController
|
||||
final breakhoursDouble = duration.inSeconds / 3600.0;
|
||||
|
||||
// Debug logs for terminal visibility
|
||||
debugPrint(
|
||||
'[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}',
|
||||
);
|
||||
debugPrint(
|
||||
'[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude',
|
||||
);
|
||||
@@ -1107,7 +1151,7 @@ class RiderLogController extends GetxController
|
||||
|
||||
// ✅ Check if there are active pickup to set status
|
||||
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
|
||||
final String riderStatus = hasActivePickups ? 'active' : 'idle';
|
||||
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
|
||||
|
||||
// Resolve rider display name (username) from prefs to include in log
|
||||
String? username = prefs.getString('username');
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
/// ── The flag is gone ──
|
||||
///
|
||||
/// This class used to carry `useNewApi`, a `--dart-define` switch between the
|
||||
/// v1 backend and a legacy one (`jupiter.doormile.app` / `queue.workolik.com`).
|
||||
/// v1 backend and a legacy one, on two now-retired hosts.
|
||||
/// Every provider branched on it, so the app shipped two implementations of
|
||||
/// every call and only one of them was ever exercised.
|
||||
///
|
||||
|
||||
@@ -46,10 +46,11 @@ class DeviceTelemetry {
|
||||
/// problem as a rider at 12% and riding.
|
||||
final bool? isCharging;
|
||||
|
||||
/// `wifi`, `mobile`, `none` … as the platform names it.
|
||||
/// `WiFi`, `4G` or `none` — the contract's vocabulary, not the plugin's.
|
||||
/// See [_connectionName].
|
||||
final String? connection;
|
||||
|
||||
/// `enabled`, `disabled`, `denied`, `denied_forever`, `unknown`.
|
||||
/// `enabled` or `disabled`. See [_locationServiceName].
|
||||
///
|
||||
/// The one field here that is about a *choice the rider made*, which is why
|
||||
/// it is worth a column of its own on the console: a disabled location
|
||||
@@ -86,9 +87,7 @@ class DeviceTelemetry {
|
||||
String? connection;
|
||||
try {
|
||||
final result = await Connectivity().checkConnectivity();
|
||||
connection = result.isNotEmpty
|
||||
? result.first.toString().split('.').last
|
||||
: 'none';
|
||||
connection = _connectionName(result);
|
||||
} catch (e) {
|
||||
debugPrint('[TELEMETRY] connectivity unavailable: $e');
|
||||
}
|
||||
@@ -104,6 +103,7 @@ class DeviceTelemetry {
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
service = _locationServiceName(service);
|
||||
|
||||
return DeviceTelemetry(
|
||||
battery: level,
|
||||
@@ -113,6 +113,48 @@ class DeviceTelemetry {
|
||||
);
|
||||
}
|
||||
|
||||
/// `WiFi` / `4G` / `none`, which is the vocabulary `/miler/logs` documents.
|
||||
///
|
||||
/// ── The plugin's own spelling is not the contract's ──
|
||||
///
|
||||
/// This was `result.first.toString().split('.').last`, which is the *enum
|
||||
/// constant* — `wifi`, `mobile`, `ethernet`, `vpn`, `bluetooth`, `other`.
|
||||
/// Two of those happen to look right in lower case and the rest do not
|
||||
/// appear in the contract at all, so the console's Connection column was
|
||||
/// rendering whatever `connectivity_plus` happened to call the transport
|
||||
/// this release.
|
||||
///
|
||||
/// `first` was also the wrong pick: the list is every active transport, so a
|
||||
/// phone on Wi-Fi with mobile data up could report either depending on the
|
||||
/// order the platform returned them. Wi-Fi wins where both are present,
|
||||
/// because it is the one that explains a rider whose data has run out.
|
||||
static String _connectionName(List<ConnectivityResult> results) {
|
||||
if (results.isEmpty) return 'none';
|
||||
if (results.contains(ConnectivityResult.wifi)) return 'WiFi';
|
||||
if (results.contains(ConnectivityResult.mobile)) return '4G';
|
||||
if (results.contains(ConnectivityResult.ethernet)) return 'ethernet';
|
||||
if (results.every((r) => r == ConnectivityResult.none)) return 'none';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/// `enabled` or `disabled` — the two values the contract names.
|
||||
///
|
||||
/// The richer answers the fix lookup produces — `denied`, `denied_forever`,
|
||||
/// `unknown` — are more useful to a dispatcher, and they are not what this
|
||||
/// field accepts. On a backend that validates its enums (and this one does
|
||||
/// silently: see the `Break` / `On_Break` note in [MilerApi]) an unrecognised
|
||||
/// value risks the whole row, which costs the battery and the connection
|
||||
/// alongside it.
|
||||
///
|
||||
/// So anything that is not `enabled` is `disabled` here, which is the fact
|
||||
/// the column exists to report — this rider's location is not reaching us.
|
||||
/// The distinction between *off* and *denied* is not lost: it still rides on
|
||||
/// the MQTT `location_turned_off` alert as `error_type`.
|
||||
static String? _locationServiceName(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return null;
|
||||
return raw == 'enabled' ? 'enabled' : 'disabled';
|
||||
}
|
||||
|
||||
/// The heartbeat payload's own field names, ready to spread into it.
|
||||
///
|
||||
/// Only what was actually read: an absent key is a field the console draws as
|
||||
|
||||
89
lib/data/heartbeat.dart
Normal file
89
lib/data/heartbeat.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// 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';
|
||||
@@ -135,9 +135,32 @@ class MilerApi {
|
||||
/// Telemetry wants strings for values that are numbers everywhere else.
|
||||
static String _str(dynamic v) => v == null ? '' : v.toString();
|
||||
|
||||
/// `YYYY-MM-DD HH:MM:SS`, IST wall-clock, for telemetry `logdate`.
|
||||
/// `YYYY-MM-DD HH:MM:SS`, the phone's **local wall clock**, for `logdate`.
|
||||
///
|
||||
/// ── This was briefly changed to UTC, and that was wrong ──
|
||||
///
|
||||
/// The reasoning looked sound: the field carries no zone suffix, so one side
|
||||
/// has to name the convention, and UTC is the usual answer. It is not the
|
||||
/// answer here. The Doormile backend's read range — `DBNow` / `DBToday` — and
|
||||
/// the database itself run on **IST wall-clock**, so a UTC stamp from an
|
||||
/// Indian handset lands 5h30m *behind* the window the console asks for, and
|
||||
/// after 18:30 local it files the evening's work under the previous day.
|
||||
///
|
||||
/// Confirmed against the live backend on 28 Aug 2026, after the UTC change
|
||||
/// had already shipped to a test build. It went unnoticed there because that
|
||||
/// handset's own clock was running on UTC, so local and UTC agreed and the
|
||||
/// rows landed in the window anyway.
|
||||
///
|
||||
/// So: local, and the phone's timezone is assumed to be the fleet's. That is
|
||||
/// true for every Doormile Miler today. The durable fix is an offset on the
|
||||
/// wire — an ISO-8601 stamp the backend parses with its zone — and until the
|
||||
/// contract carries one, this is the convention both sides have agreed.
|
||||
static String logStamp([DateTime? at]) {
|
||||
final t = at ?? DateTime.now();
|
||||
// `.toLocal()` because a caller can hand this a UTC `DateTime` — the
|
||||
// consignment breadcrumb does — and reading `.hour` off one of those
|
||||
// renders the UTC wall clock, which is the bug this method just came back
|
||||
// from. A local `DateTime` passes through unchanged.
|
||||
final t = (at ?? DateTime.now()).toLocal();
|
||||
String p(int n) => n.toString().padLeft(2, '0');
|
||||
return '${t.year}-${p(t.month)}-${p(t.day)} '
|
||||
'${p(t.hour)}:${p(t.minute)}:${p(t.second)}';
|
||||
|
||||
@@ -12,10 +12,9 @@ import 'dart:io';
|
||||
/// day's cash figures without a word.
|
||||
///
|
||||
/// It is the kind of thing that gets added once to get past a staging server
|
||||
/// and then ships. Both live hosts were checked before removing it and both
|
||||
/// present valid, publicly-trusted certificates — `api.doormile.com` and
|
||||
/// `queue.workolik.com` verify strictly under TLS 1.3 against Let's Encrypt.
|
||||
/// Nothing needed the bypass.
|
||||
/// and then ships. The live host was checked before removing it:
|
||||
/// `api.doormile.com` verifies strictly under TLS 1.3 against a publicly
|
||||
/// trusted certificate. Nothing needed the bypass.
|
||||
///
|
||||
/// ── What is left, and why the class is still here ──
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -17,7 +17,6 @@ import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:miler/providers/pickup/pickup_provider.dart';
|
||||
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
|
||||
import 'package:miler/views/Dashboard/orders/orderstaus_button.dart';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/miler_surface.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
@@ -59,6 +58,7 @@ import 'package:miler/views/Dashboard/pickups/pickups.dart' as pickups;
|
||||
import 'package:miler/background/live_tracking_service.dart';
|
||||
import 'package:miler/data/accepted_store.dart';
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/heartbeat.dart';
|
||||
import 'package:miler/data/consignment_state.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/collect_payment.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
||||
@@ -1699,11 +1699,13 @@ class _HomepageState extends State<Homepage>
|
||||
Future<Duration> _getLogInterval() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final secs = prefs.getInt('logseconds');
|
||||
final int interval = (secs != null && secs > 0) ? secs : 30;
|
||||
// Already correct — routed through the shared resolver so the 30 lives in
|
||||
// one place. See [kDefaultLogSeconds] for what a zero here used to cost
|
||||
// the rider-log loop, which read the same pref and did not guard it.
|
||||
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
|
||||
return Duration(seconds: interval);
|
||||
} catch (_) {
|
||||
return const Duration(seconds: 30);
|
||||
return const Duration(seconds: kDefaultLogSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -932,7 +932,15 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
|
||||
final top = media.padding.top + 72;
|
||||
await _camera.fit(
|
||||
<LatLng>[_routeStart, _routeEnd],
|
||||
padding: EdgeInsets.fromLTRB(48, top, 48, viewport * _sheetWorking + 24),
|
||||
// The sheet is lifted by the gesture bar, so the space it covers is
|
||||
// taller than the bare fraction — see [_sheetLift]. Framing against the
|
||||
// old figure would tuck the route's near end under the sheet.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
48,
|
||||
top,
|
||||
48,
|
||||
viewport * (_sheetWorking + _sheetLift(media)) + 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1102,18 +1110,49 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
|
||||
/// Three stops and no more: a glance, the working height, and the detail
|
||||
/// view. A drag with many arbitrary rest points feels loose and gives the
|
||||
/// rider nothing to aim at.
|
||||
/// The system's own bar at the foot of the screen, as a fraction of the
|
||||
/// viewport — so the three snap fractions below can be lifted clear of it.
|
||||
///
|
||||
/// The snaps are fractions and the bar is an absolute height, so the two only
|
||||
/// meet through the viewport. Clamped because a fraction derived from a
|
||||
/// measurement should never be able to swallow the sheet on a device that
|
||||
/// reports something strange.
|
||||
double _sheetLift(MediaQueryData media) {
|
||||
final double height = media.size.height;
|
||||
if (height <= 0) return 0;
|
||||
final double bar = math.max(
|
||||
media.viewInsets.bottom,
|
||||
media.viewPadding.bottom,
|
||||
);
|
||||
return (bar / height).clamp(0.0, 0.15);
|
||||
}
|
||||
|
||||
static const double _sheetGlance = 0.35;
|
||||
static const double _sheetWorking = 0.45;
|
||||
static const double _sheetDetail = 0.75;
|
||||
|
||||
Widget _buildDraggableBottomSheet() {
|
||||
// ── Every stop is lifted by the height of the gesture bar ──
|
||||
//
|
||||
// Padding the content alone would have kept the sheet the same height and
|
||||
// simply moved everything in it up, costing the rider a bar's worth of the
|
||||
// stop he is reading. Growing the sheet by the same amount puts the glass
|
||||
// *behind* the bar and leaves the visible content area exactly the size it
|
||||
// was — which is what "the sheet is touching the navigation" actually asks
|
||||
// for. The two work together: this makes room, the list's bottom padding
|
||||
// keeps the CTA out of it.
|
||||
final double lift = _sheetLift(MediaQuery.of(context));
|
||||
final double glance = (_sheetGlance + lift).clamp(0.0, 1.0);
|
||||
final double working = (_sheetWorking + lift).clamp(0.0, 1.0);
|
||||
final double detail = (_sheetDetail + lift).clamp(0.0, 1.0);
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: _sheetWorking,
|
||||
minChildSize: _sheetGlance,
|
||||
maxChildSize: _sheetDetail,
|
||||
initialChildSize: working,
|
||||
minChildSize: glance,
|
||||
maxChildSize: detail,
|
||||
// Snap to the three, so the sheet always settles somewhere meant.
|
||||
snap: true,
|
||||
snapSizes: const [_sheetGlance, _sheetWorking, _sheetDetail],
|
||||
snapSizes: [glance, working, detail],
|
||||
// ── Frosted, not opaque ──
|
||||
//
|
||||
// A solid white slab over a map takes away 45% of the one thing this
|
||||
@@ -1157,7 +1196,32 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
|
||||
// has, and it is why that sheet reads at arm's length.
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.fromLTRB(20.w, 16.h, 20.w, 8.h),
|
||||
// ── The gesture bar sat on top of the CTA ──
|
||||
//
|
||||
// Eight points of bottom padding, on a sheet anchored to
|
||||
// the bottom of the screen: the last thing in this list is
|
||||
// [_buildPrimaryAction] — the slide control that closes the
|
||||
// stop — and on a gesture-navigation phone the system's own
|
||||
// bar was drawn across it. The rider's swipe either missed
|
||||
// the control or went to the OS.
|
||||
//
|
||||
// The glass still bleeds to the screen edge, which is what
|
||||
// it is for; what has to clear the bar is the *content*.
|
||||
// Same rule the sheet kit applies to every modal sheet —
|
||||
// `max(keyboard, gesture bar)`, never the sum, because the
|
||||
// keyboard replaces the bar rather than stacking with it.
|
||||
// This sheet is hand-built rather than presented through
|
||||
// [MilerSheetScaffold], so it never inherited that rule.
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20.w,
|
||||
16.h,
|
||||
20.w,
|
||||
8.h +
|
||||
math.max(
|
||||
MediaQuery.viewInsetsOf(context).bottom,
|
||||
MediaQuery.viewPaddingOf(context).bottom,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_LiveEtaHeader(
|
||||
meters: _liveMetersToStop,
|
||||
@@ -1635,13 +1699,11 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
|
||||
if (_isNavigating || !mounted) return;
|
||||
final outcome = await DeliveryProofPage.askOutcome(context);
|
||||
if (!mounted || outcome == null) return;
|
||||
await _closeDelivery(
|
||||
switch (outcome) {
|
||||
ProofOutcome.delivered => DeliveryOutcome.delivered,
|
||||
ProofOutcome.skipped => DeliveryOutcome.skipped,
|
||||
ProofOutcome.cancelled => DeliveryOutcome.cancelled,
|
||||
},
|
||||
);
|
||||
await _closeDelivery(switch (outcome) {
|
||||
ProofOutcome.delivered => DeliveryOutcome.delivered,
|
||||
ProofOutcome.skipped => DeliveryOutcome.skipped,
|
||||
ProofOutcome.cancelled => DeliveryOutcome.cancelled,
|
||||
});
|
||||
}
|
||||
|
||||
/// The end of the stop, whichever of the three it is.
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
class ApiConstants {
|
||||
static String mainDev = "dev";
|
||||
static String mainRoute = "live";
|
||||
|
||||
// ── There is no maps key any more ──
|
||||
//
|
||||
// A `googleMapsApiKey` used to live here, feeding `flutter_polyline_points`
|
||||
// and both native projects. It was revoked, so every tile request and every
|
||||
// Directions call came back `REQUEST_DENIED` and the app looked broken rather
|
||||
// than unconfigured.
|
||||
//
|
||||
// The maps no longer need one: tiles come from OpenStreetMap and routes from
|
||||
// OSRM, both keyless. See `lib/views/helpers/widgets/miler_map.dart` and
|
||||
// `lib/helpers/miler_router.dart` — including the note on what those free
|
||||
// services are and are not suitable for.
|
||||
|
||||
//Collection Queue - v2
|
||||
static String pickupQueueDev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v2/pickups/getpickupqueues";
|
||||
static String pickupQueueLive =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v2/pickups/getpickupqueues";
|
||||
|
||||
//Current Collection - v1
|
||||
static String currentPickupDev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v1/pickups/getpickups";
|
||||
static String currentPickupLive =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v1/pickups/getpickups";
|
||||
|
||||
//Current Collection V3 - v3 (date-bounded)
|
||||
static String currentPickupV3Dev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v3/pickups/getpickups";
|
||||
static String currentPickupV3Live =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v3/pickups/getpickups";
|
||||
|
||||
//Update Collection - v1
|
||||
static String updatePickupDev =
|
||||
"https://queue.workolik.com/live/api/v1/pickups/updatepickup";
|
||||
static String updatePickupLive =
|
||||
"https://queue.workolik.com/live/api/v1/pickups/updatepickup";
|
||||
|
||||
//Get Rider Log - v1
|
||||
static String getRiderLogDev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v1/partners/getriderlog";
|
||||
static String getRiderLogLive =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/getriderlog";
|
||||
|
||||
//Create Rider Log - v2
|
||||
static String createRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
static String createRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
|
||||
//Update Rider Log - v1
|
||||
static String updateRiderLogDev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v1/partners/updateriderlog";
|
||||
static String updateRiderLogLive =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/updateriderlog";
|
||||
|
||||
//Get Rider Count - v1
|
||||
static String getRiderCountDev =
|
||||
"https://jupiter.doormile.app/$mainDev/api/v1/partners/getridercount";
|
||||
static String getRiderCountLive =
|
||||
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/getridercount";
|
||||
|
||||
//Create Break Rider Log - v2
|
||||
static String createBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
static String createBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
|
||||
//Update Break Rider Log - v2
|
||||
static String updateBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
static String updateBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
|
||||
//Create Collection Log - v2
|
||||
static String createPickupLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/pickups/createpickuplog";
|
||||
static String createPickupLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/pickups/createpickuplog";
|
||||
|
||||
//Summary API - v2
|
||||
static String summaryApiLive =
|
||||
'https://jupiter.doormile.app/$mainRoute/api/v2/partners';
|
||||
|
||||
//Summary Rider Weekly KMs - v1
|
||||
static String summaryriderkmLive =
|
||||
'https://jupiter.doormile.app/$mainRoute/api/v1/partners';
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'dart:math' as math;
|
||||
@@ -20,7 +21,6 @@ class BackgroundDeliveryLog {
|
||||
|
||||
static final CreateDeliveryLogProvider _logProvider =
|
||||
CreateDeliveryLogProvider();
|
||||
static final http.Client _httpClient = http.Client();
|
||||
static final FlutterTts _tts = FlutterTts();
|
||||
static final AudioPlayer _proximityPlayer = AudioPlayer();
|
||||
|
||||
@@ -60,38 +60,37 @@ class BackgroundDeliveryLog {
|
||||
final now = DateTime.now();
|
||||
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
|
||||
|
||||
// Build API URL using v1 endpoint (getdeliveries)
|
||||
final bool isLive = ApiConstants.mainRoute == 'live';
|
||||
final baseUrl = isLive
|
||||
? ApiConstants.currentDeliveryLive
|
||||
: ApiConstants.currentDeliveryDev;
|
||||
|
||||
final uri = Uri.parse(baseUrl).replace(
|
||||
queryParameters: {
|
||||
'userid': userId.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Fetching deliveries: $uri');
|
||||
|
||||
// Fetch deliveries from API using v1 endpoint
|
||||
final deliveries = await _fetchDeliveriesFromApi(uri);
|
||||
// ── The delivery twin of the pickup fetch, with the same two faults ──
|
||||
//
|
||||
// It built a `deliveries/getdeliveries` URL by hand and fetched it with a
|
||||
// bare `_httpClient.get` — **no bearer token** — and put `userid` in the
|
||||
// query string. The URL itself already resolved to a v1 route through the
|
||||
// [DeliveryApi] facade, so the call could only ever be answered 401.
|
||||
//
|
||||
// And the filter looked for `orderstatus == 'active'`, which is not a
|
||||
// value this app's vocabulary contains: [ApiConfig.legacyStatusFromNew]
|
||||
// maps the v1 statuses to `assigned` / `accepted` / `arrived` / `picked`,
|
||||
// never to `active`. So it returned nothing on every tick and fell
|
||||
// through to the shift-end check, which is what the device log shows.
|
||||
//
|
||||
// `GET /miler/bookings` through [MilerApi], asked with the session, about
|
||||
// the rider the session names.
|
||||
final res = await MilerApi.bookings(date: today);
|
||||
final deliveries = res.list;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG] API returned ${deliveries.length} items',
|
||||
'[ACTIVE_DELIVERY_LOG][BG] /miler/bookings -> ${res.status}, '
|
||||
'${deliveries.length} items',
|
||||
);
|
||||
|
||||
// Filter for active orders only
|
||||
const inProgress = {'accepted', 'arrived', 'picked'};
|
||||
final activeOrders = deliveries.whereType<Map<String, dynamic>>().where((
|
||||
order,
|
||||
) {
|
||||
final status = (order['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isActive = status == 'active';
|
||||
return isActive;
|
||||
final status = ApiConfig.legacyStatusFromNew(
|
||||
(order['status'] ?? order['orderstatus'])?.toString(),
|
||||
);
|
||||
return inProgress.contains(status);
|
||||
}).toList();
|
||||
|
||||
if (activeOrders.isEmpty) {
|
||||
@@ -202,11 +201,8 @@ class BackgroundDeliveryLog {
|
||||
'logstatus': await _resolveLogStatus(orderId, coords),
|
||||
};
|
||||
|
||||
// Determine API endpoint
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
|
||||
final url = ApiConstants.createDeliveryLogLive;
|
||||
// Post the log
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Posting log for $orderId');
|
||||
|
||||
@@ -434,33 +430,6 @@ class BackgroundDeliveryLog {
|
||||
}
|
||||
|
||||
/// Fetch deliveries from API using v1 endpoint
|
||||
static Future<List<dynamic>> _fetchDeliveriesFromApi(Uri uri) async {
|
||||
try {
|
||||
final res = await _httpClient
|
||||
.get(uri)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) {
|
||||
return data['items'] as List;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to pad numbers with leading zero
|
||||
static String _pad(int n) => n.toString().padLeft(2, '0');
|
||||
@@ -540,10 +509,6 @@ class BackgroundDeliveryLog {
|
||||
final List<String> remaining = [];
|
||||
bool anySuccess = false;
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
for (final itemStr in queue) {
|
||||
try {
|
||||
final Map<String, dynamic> item = jsonDecode(itemStr);
|
||||
@@ -554,6 +519,8 @@ class BackgroundDeliveryLog {
|
||||
|
||||
if (payload.isEmpty) continue;
|
||||
|
||||
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
|
||||
final url = ApiConstants.createDeliveryLogLive;
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Retrying for orderId: $orderId',
|
||||
);
|
||||
@@ -770,14 +737,18 @@ class BackgroundDeliveryLog {
|
||||
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
|
||||
);
|
||||
|
||||
// 1. Get Required IDs
|
||||
// ── The four ids and the fix are no longer this function's business ──
|
||||
//
|
||||
// `userid`, `partnerid`, `shiftid`, `logid` and a position were assembled
|
||||
// here to fill a legacy break-log body. On v1 the break and the duty end
|
||||
// are both keyed on the **token**: `POST /miler/breaks/start` opens a
|
||||
// break for whoever the session names, and `PUT /miler/duty/end` closes
|
||||
// whichever duty log is open for them. Sending an id would not make it
|
||||
// more correct; it is what let the old call be made unauthenticated.
|
||||
//
|
||||
// `userid` is still read, as the one guard worth keeping: no signed-in
|
||||
// rider, nothing to end.
|
||||
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
final int partnerid =
|
||||
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
|
||||
final int shiftid =
|
||||
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
|
||||
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
|
||||
|
||||
if (userid == 0) {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
|
||||
@@ -785,61 +756,28 @@ class BackgroundDeliveryLog {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Get Location
|
||||
final Map<String, String>? coords = await _getCoordinatesWithFallback();
|
||||
final String lat = coords?['lat'] ?? '0';
|
||||
final String lng = coords?['lng'] ?? '0';
|
||||
|
||||
// 3. Prepare Break Log Payload
|
||||
final now = DateTime.now();
|
||||
final int localBreakId =
|
||||
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
|
||||
final String breakdate =
|
||||
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
final String breakstart =
|
||||
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
|
||||
final payload = <String, dynamic>{
|
||||
"breakid": localBreakId,
|
||||
"logid": logid,
|
||||
"breakdate": breakdate,
|
||||
"userid": userid,
|
||||
"partnerid": partnerid,
|
||||
"shiftid": shiftid,
|
||||
"breakstart": breakstart,
|
||||
"breakend": "",
|
||||
"breakhours": 0.0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
|
||||
// 4. Call API to Create Break
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createBreakRiderLogLive
|
||||
: ApiConstants.createBreakRiderLogDev;
|
||||
|
||||
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
|
||||
|
||||
// We use a separate provider instance or http call if needed,
|
||||
// but _logProvider is for delivery logs. We need a generic post or use http directly.
|
||||
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
|
||||
// 4. Close the shift with a break, on v1.
|
||||
//
|
||||
// This hand-posted the retired backend's `partners/createbreaklog`
|
||||
// with a bare `_httpClient.post` and no bearer
|
||||
// token — an unauthenticated write keyed on a `userid` in the
|
||||
// body. `POST /miler/breaks/start` is the v1 endpoint, and
|
||||
// [MilerApi] attaches the session.
|
||||
debugPrint('[AUTO_SHIFT_END] Creating break log (v1)');
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(payload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
final res = await MilerApi.startBreak('Shift_End');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
if (res.ok) {
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
|
||||
|
||||
// Parse response to get server break ID if needed, but mainly we just need to go offline
|
||||
final body = jsonDecode(response.body);
|
||||
final det = (body['details'] is Map) ? body['details'] : body;
|
||||
final serverBreakId = det['breakid'];
|
||||
final serverBreakId = res.map['breaklogid'] ?? res.map['breakid'];
|
||||
|
||||
if (serverBreakId != null) {
|
||||
await prefs.setInt(
|
||||
@@ -851,7 +789,7 @@ class BackgroundDeliveryLog {
|
||||
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
|
||||
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${res.status}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -863,27 +801,15 @@ class BackgroundDeliveryLog {
|
||||
await prefs.setInt('onduty', 0);
|
||||
await prefs.setBool('online', false);
|
||||
|
||||
// 6. Update Rider Log (Set Duty = 0)
|
||||
// We should also update the main rider log to say onduty=0
|
||||
final updateUrl = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateRiderLogLive
|
||||
: ApiConstants.updateRiderLogDev;
|
||||
|
||||
final updatePayload = {
|
||||
"userid": userid,
|
||||
"onduty": 0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
// 6. End duty, on v1.
|
||||
//
|
||||
// Was an unauthenticated POST of `{userid, onduty: 0}` to the
|
||||
// legacy `updateriderlog`. `PUT /miler/duty/end` closes whichever
|
||||
// duty log is open for the token's rider, which is the same fact
|
||||
// without the id or the open door.
|
||||
|
||||
try {
|
||||
await _httpClient
|
||||
.post(
|
||||
Uri.parse(updateUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(updatePayload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
await MilerApi.endDuty();
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
|
||||
} catch (_) {}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
|
||||
import 'package:miler/xpress/background/backgroundservice.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
@@ -354,12 +353,9 @@ class _BackgroundRiderLog {
|
||||
payload['lastname'] = lastName.trim();
|
||||
}
|
||||
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
|
||||
final provider = CreateRiderLogProvider();
|
||||
final resp = await provider.createRiderLog(base, payload);
|
||||
final resp = await provider.createRiderLog(payload);
|
||||
|
||||
if (resp == null || resp.isEmpty) return;
|
||||
final det = (resp['details'] is Map<String, dynamic>)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:miler/xpress/Models/riders/riders_models.dart';
|
||||
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -15,6 +14,7 @@ import 'package:miler/xpress/controllers/logcontroller.dart';
|
||||
import 'package:miler/xpress/background/foreground_service.dart' as fg;
|
||||
import 'package:miler/xpress/utils/kalman_filter.dart';
|
||||
import 'package:miler/xpress/utils/mqtt_service.dart';
|
||||
import 'package:miler/data/heartbeat.dart';
|
||||
import 'package:miler/xpress/views/helpers/constants/mqtt_constants.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
|
||||
@@ -40,11 +40,9 @@ class RiderLogController extends GetxController {
|
||||
Future<void> fetchRiderLogs({required String userId}) async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.getRiderLogLive
|
||||
: ApiConstants.getRiderLogDev;
|
||||
final url = "$base?userid=$userId";
|
||||
final response = await _getProvider.getRiderLog(url);
|
||||
// v1 answers duty for the *token's* rider — `userid` is neither sent
|
||||
// nor needed. See [GetRiderLogProvider].
|
||||
final response = await _getProvider.getRiderLog();
|
||||
|
||||
// Handle either {details: {...}} or {data: [...]} shapes
|
||||
if (response == null) {
|
||||
@@ -122,11 +120,7 @@ class RiderLogController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> fetchRiderCount({required String userId}) async {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.getRiderCountLive
|
||||
: ApiConstants.getRiderCountDev;
|
||||
final url = "$base?userid=$userId";
|
||||
final response = await _getProvider.getRiderCount(url);
|
||||
final response = await _getProvider.getRiderCount();
|
||||
final dynamic cnt = response != null ? response['count'] : null;
|
||||
riderCount.value = cnt is num
|
||||
? cnt.toInt()
|
||||
@@ -134,46 +128,30 @@ class RiderLogController extends GetxController {
|
||||
}
|
||||
|
||||
Future<bool> createLogin(RiderLogin login) async {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
final ok = await _createProvider.createRiderLog(
|
||||
base,
|
||||
_CreateRiderLogRequestCompat.fromRiderLogin(login),
|
||||
);
|
||||
return ok != null && ok.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<bool> updateLog(RiderUpdate update) async {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateRiderLogLive
|
||||
: ApiConstants.updateRiderLogDev;
|
||||
final ok = await _updateProvider.updateRiderLog(
|
||||
base,
|
||||
_UpdateRiderLogRequestCompat.fromRiderUpdate(update),
|
||||
);
|
||||
return ok != null && ok.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<bool> createBreak(RiderBreak brk) async {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createBreakRiderLogLive
|
||||
: ApiConstants.createBreakRiderLogDev;
|
||||
final ok = await _breakProvider.createBreakRiderLog(
|
||||
base,
|
||||
_BreakLogRequestCompat.fromRiderBreak(brk),
|
||||
);
|
||||
return ok != null && ok.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<bool> updateBreak(RiderBreak brk) async {
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateBreakRiderLogLive
|
||||
: ApiConstants.updateBreakRiderLogDev;
|
||||
final ok = await _breakProvider.updateBreakRiderLog(
|
||||
base,
|
||||
_BreakLogRequestCompat.fromRiderBreak(brk),
|
||||
);
|
||||
// `PUT /miler/breaks/end` takes no body and closes whichever break is
|
||||
// open, which the server knows and the app was only guessing at.
|
||||
final ok = await _breakProvider.updateBreakRiderLog();
|
||||
return ok != null && ok.isNotEmpty;
|
||||
}
|
||||
|
||||
@@ -190,10 +168,6 @@ class RiderLogController extends GetxController {
|
||||
required String latitude,
|
||||
required String longitude,
|
||||
}) async {
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateBreakRiderLogLive
|
||||
: ApiConstants.updateBreakRiderLogDev;
|
||||
|
||||
final payload = {
|
||||
"breakid": breakid,
|
||||
"logid": logid,
|
||||
@@ -209,11 +183,9 @@ class RiderLogController extends GetxController {
|
||||
};
|
||||
|
||||
try {
|
||||
// Debug: show exact URL and JSON body
|
||||
debugPrint('[BREAK][UPDATE] URL: $url');
|
||||
debugPrint('[BREAK][UPDATE] Body: $payload');
|
||||
|
||||
final response = await _breakProvider.updateBreakRiderLog(url, payload);
|
||||
final response = await _breakProvider.updateBreakRiderLog();
|
||||
|
||||
// Debug: show raw response
|
||||
debugPrint(
|
||||
@@ -375,13 +347,9 @@ class RiderLogController extends GetxController {
|
||||
payload.remove('lastname');
|
||||
}
|
||||
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
debugPrint('[RIDERLOG][CREATE LOGIN NOW] URL: $base');
|
||||
debugPrint('[RIDERLOG][CREATE LOGIN NOW] Payload: $payload');
|
||||
|
||||
final resp = await _createProvider.createRiderLog(base, payload);
|
||||
final resp = await _createProvider.createRiderLog(payload);
|
||||
debugPrint(
|
||||
'[RIDERLOG][CREATE LOGIN NOW] Response: ${resp == null ? 'null' : resp.toString()}',
|
||||
);
|
||||
@@ -391,7 +359,7 @@ class RiderLogController extends GetxController {
|
||||
debugPrint(
|
||||
'[RIDERLOG][CREATE LOGIN NOW] Failed, saving to offline queue',
|
||||
);
|
||||
await _saveToOfflineQueue(base, payload);
|
||||
await _saveToOfflineQueue('/miler/logs', payload);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -500,11 +468,7 @@ class RiderLogController extends GetxController {
|
||||
payload.remove('lastname');
|
||||
}
|
||||
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
|
||||
await _saveToOfflineQueue(base, payload);
|
||||
await _saveToOfflineQueue('/miler/logs', payload);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[RIDERLOG] Error saving offline log: $e');
|
||||
@@ -532,10 +496,15 @@ class RiderLogController extends GetxController {
|
||||
await prefs.setInt('onduty', on ? 1 : 0);
|
||||
if (on) {
|
||||
await createLoginNowV2();
|
||||
final int interval = prefs.getInt('logseconds') ?? 0;
|
||||
if (interval > 0) {
|
||||
startAutoCreateLoginLoop(seconds: interval);
|
||||
}
|
||||
// ── Going on duty always starts the heartbeat ──
|
||||
//
|
||||
// Gated on `interval > 0`, this read a stored zero as a decision not
|
||||
// to beat. The v1 login has no `logseconds` to store, so the gate was
|
||||
// shut on every rider: duty went on, one log was posted by the call
|
||||
// above, and nothing followed it. Mirrors the logistics controller —
|
||||
// see [resolveLogSeconds].
|
||||
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
|
||||
startAutoCreateLoginLoop(seconds: interval);
|
||||
|
||||
// Ensure foreground logging notification is started when going on-duty
|
||||
try {
|
||||
@@ -604,16 +573,43 @@ class RiderLogController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the periodic heartbeat is currently running on the stream path.
|
||||
///
|
||||
/// ── Why a test needs to be able to see this ──
|
||||
///
|
||||
/// The `logseconds` bug was invisible from outside the class. Duty went on,
|
||||
/// the API accepted it, one log was posted by the immediate call — and the
|
||||
/// loop that was supposed to follow it simply never started. Nothing threw,
|
||||
/// nothing logged an error, and the only symptom was a console panel that had
|
||||
/// been blank for so long it read as a backend problem.
|
||||
///
|
||||
/// Android normally runs the heartbeat as a foreground service instead, and
|
||||
/// leaves this null; the test host is never Android, so the stream path is
|
||||
/// the one under test. See [startAutoCreateLoginLoop].
|
||||
@visibleForTesting
|
||||
bool get autoLoopRunning => _autoLoginSubscription != null;
|
||||
|
||||
// Start periodic createRiderLog calls based on seconds (or prefs 'logseconds')
|
||||
// ✅ CRITICAL: When there are active deliveries, use 30 seconds (same as delivery logs)
|
||||
// Otherwise, use the configured logseconds interval
|
||||
void startAutoCreateLoginLoop({int? seconds}) async {
|
||||
/// `Future<void>`, not `void`. It was fire-and-forget, which meant no caller
|
||||
/// could wait for the loop to actually be up — and the one place that most
|
||||
/// wanted to, [setOnDuty], had no way to tell whether the heartbeat it had
|
||||
/// just asked for existed. Every existing call site ignores the future and is
|
||||
/// unchanged by this.
|
||||
Future<void> startAutoCreateLoginLoop({int? seconds}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// ✅ Check if there are active deliveries - if yes, use 30 seconds (same as delivery logs)
|
||||
final bool hasActiveDeliveries =
|
||||
prefs.getBool('has_live_deliveries') ?? false;
|
||||
final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0);
|
||||
// The configured cadence, or the floor under it. `?? 0` here was the
|
||||
// second half of the same bug as the one in [setOnDuty]: a caller passing
|
||||
// no `seconds` fell through to a stored zero and returned at the
|
||||
// `interval <= 0` guard below. See [resolveLogSeconds].
|
||||
final int baseInterval = resolveLogSeconds(
|
||||
seconds ?? prefs.getInt('logseconds'),
|
||||
);
|
||||
|
||||
// When there are active deliveries, post rider logs every 30 seconds (matching delivery logs)
|
||||
// Otherwise, use the configured interval
|
||||
@@ -625,12 +621,18 @@ class RiderLogController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
await stopAutoCreateLoginLoop(); // Cancel existing subscription
|
||||
// Stop before start, which is what keeps this method safe to call from
|
||||
// every place that does. Whichever call arrives last wins, and there is
|
||||
// never a second loop: the subscription is cancelled and the foreground
|
||||
// service stopped before either is created again.
|
||||
await stopAutoCreateLoginLoop();
|
||||
|
||||
// Attempt to flush offline logs on loop start
|
||||
flushOfflineLogs();
|
||||
|
||||
if (interval <= 0) return;
|
||||
// No `interval <= 0` guard. [resolveLogSeconds] cannot return one, and the
|
||||
// guard that used to be here is what turned a missing `logseconds` into a
|
||||
// rider who never reported.
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
if (onduty != 1) {
|
||||
debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty');
|
||||
@@ -921,10 +923,6 @@ class RiderLogController extends GetxController {
|
||||
final breakdate = _formatDateTimeFull(now); // e.g. 2025-10-16 16:36:16
|
||||
final breakstart = _formatTime(now); // e.g. 16:36:16
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createBreakRiderLogLive
|
||||
: ApiConstants.createBreakRiderLogDev;
|
||||
|
||||
// Build payload with all required fields in the exact format the API expects
|
||||
final payload = <String, dynamic>{
|
||||
"breakid": localBreakId,
|
||||
@@ -941,13 +939,12 @@ class RiderLogController extends GetxController {
|
||||
};
|
||||
|
||||
// Debug logs for terminal visibility
|
||||
debugPrint('[BREAK][CREATE] URL: $url');
|
||||
debugPrint('[BREAK][CREATE] Payload: $payload');
|
||||
|
||||
// Persist local break id immediately to ensure update can reference it
|
||||
await prefs.setInt('breakId', localBreakId);
|
||||
|
||||
final resp = await _breakProvider.createBreakRiderLog(url, payload);
|
||||
final resp = await _breakProvider.createBreakRiderLog(payload);
|
||||
|
||||
debugPrint(
|
||||
'[BREAK][CREATE] Response: ${resp == null ? 'null' : resp.toString()}',
|
||||
@@ -1045,9 +1042,6 @@ class RiderLogController extends GetxController {
|
||||
final breakhoursDouble = duration.inSeconds / 3600.0;
|
||||
|
||||
// Debug logs for terminal visibility
|
||||
debugPrint(
|
||||
'[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}',
|
||||
);
|
||||
debugPrint(
|
||||
'[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude',
|
||||
);
|
||||
@@ -1101,10 +1095,6 @@ class RiderLogController extends GetxController {
|
||||
final iso = _formatDateTimeFull(now);
|
||||
final loginTime = _formatTime(now);
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
|
||||
// ✅ Check if there are active deliveries to set status
|
||||
final bool hasActiveDeliveries =
|
||||
prefs.getBool('has_live_deliveries') ?? false;
|
||||
@@ -1141,10 +1131,9 @@ class RiderLogController extends GetxController {
|
||||
userfcmtoken: userfcmtoken ?? '',
|
||||
).toJson();
|
||||
|
||||
debugPrint('[RIDERLOG][CREATE LOGIN] URL: $url');
|
||||
debugPrint('[RIDERLOG][CREATE LOGIN] Payload: $payload');
|
||||
|
||||
final resp = await _createProvider.createRiderLog(url, payload);
|
||||
final resp = await _createProvider.createRiderLog(payload);
|
||||
debugPrint(
|
||||
'[RIDERLOG][CREATE LOGIN] Response: ${resp == null ? 'null' : resp.toString()}',
|
||||
);
|
||||
@@ -1205,7 +1194,7 @@ class RiderLogController extends GetxController {
|
||||
// We can use _createProvider generic call
|
||||
|
||||
debugPrint('[RIDERLOG][OFFLINE] Retrying: $payload');
|
||||
final resp = await _createProvider.createRiderLog(url, payload);
|
||||
final resp = await _createProvider.createRiderLog(payload);
|
||||
|
||||
if (resp != null && resp.isNotEmpty) {
|
||||
debugPrint('[RIDERLOG][OFFLINE] Success!');
|
||||
|
||||
@@ -107,7 +107,7 @@ class DeliveryApi {
|
||||
/// **ABSENT** — the generic status write the ported flow uses for every
|
||||
/// transition (accepted → active → arrived → delivered → skipped).
|
||||
///
|
||||
/// Was `queue.workolik.com/live/api/v1/deliveries/updatedelivery`. There is no
|
||||
/// Was `deliveries/updatedelivery` on the retired backend. There is no
|
||||
/// single status-write route on Doormile; the transitions above are separate
|
||||
/// endpoints. Call sites that still post a bare status change land here and
|
||||
/// will 404 until either they are split or the route is built.
|
||||
@@ -117,7 +117,7 @@ class DeliveryApi {
|
||||
|
||||
/// **LIVE** — periodic location/telemetry log.
|
||||
///
|
||||
/// Was `queue.workolik.com/live/api/v2/partners/createriderlog`.
|
||||
/// Was `partners/createriderlog` on the retired backend.
|
||||
static String get createRiderLog => url('/miler/logs');
|
||||
|
||||
/// **LIVE** — read them back.
|
||||
@@ -142,7 +142,7 @@ class DeliveryApi {
|
||||
|
||||
/// **ABSENT** — per-delivery event log.
|
||||
///
|
||||
/// Was `queue.workolik.com/live/api/v2/deliveries/createdeliverylog`. The
|
||||
/// Was `deliveries/createdeliverylog` on the retired backend. The
|
||||
/// closest real thing is consignment logs, which key on a consignmentid the
|
||||
/// delivery flow does not always hold.
|
||||
static String get createDeliveryLog => url('/miler/consignments/logs');
|
||||
|
||||
@@ -1,260 +1,46 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Riderlog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE DELIVERY LINE'S RIDER LOG IS THE PARCEL LINE'S RIDER LOG
|
||||
///
|
||||
/// **Empty since the delivery line moved to Doormile.** The one entry here
|
||||
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
|
||||
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
|
||||
/// host any more — every delivery-line write goes to `api.doormile.com` via
|
||||
/// [DeliveryApi] — so the pin was routing advice about a server this app no
|
||||
/// longer talks to.
|
||||
/// ── What this file used to be ──
|
||||
///
|
||||
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
|
||||
/// the old backend, and pinning it for `api.doormile.com` would send every
|
||||
/// request to the wrong server with the right SNI. If Doormile ever needs the
|
||||
/// same workaround, add it here with its own verified address.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
/// A second, complete implementation of duty, breaks and the location
|
||||
/// heartbeat, posting to the two retired pre-v1 hosts over raw `http`. It survived the migration only because
|
||||
/// nothing in `lib/` imported it: the delivery shell reaches it through
|
||||
/// [riderShell], which is production for any tenant on the delivery line.
|
||||
///
|
||||
/// It carried two things that had already been deleted from the parcel line:
|
||||
///
|
||||
/// • **`_buildSslBypassClient()`** — an `IOClient` whose
|
||||
/// `badCertificateCallback` returned `true` for *every* certificate, with a
|
||||
/// hand-rolled `SecureSocket` upgrade and a hardcoded IP pin. Every request
|
||||
/// it carried — a rider's GPS track, his duty state, his break times — could
|
||||
/// be read and rewritten by anything on the path. The parcel line removed
|
||||
/// this; the delivery line kept it.
|
||||
/// • **The legacy hosts themselves**, which the v1 migration left behind.
|
||||
///
|
||||
/// ── Why this is an export and not a rewrite ──
|
||||
///
|
||||
/// `lib/providers/Riderlog/riderlog_provider.dart` already does exactly this
|
||||
/// job against `api.doormile.com`: same four class names, same legacy
|
||||
/// `{status, details}` envelopes, mapped onto `POST /miler/duty/start`,
|
||||
/// `PUT /miler/location`, `POST /miler/logs` and the break endpoints. The
|
||||
/// delivery line's controller calls the same four classes by the same names.
|
||||
///
|
||||
/// So there is nothing to port. Re-exporting means one implementation, one duty
|
||||
/// gate, one telemetry payload and one place where a contract change lands —
|
||||
/// which is the whole point of having migrated the parcel line at all. The two
|
||||
/// lines cannot drift, because there is only one of them.
|
||||
///
|
||||
/// The `String urldata` first parameter is gone from every method; callers that
|
||||
/// were building a legacy URL to pass in have had that construction deleted
|
||||
/// rather than made to compile.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
library;
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
// Use known-good IP if available, else resolve normally (prefer IPv4)
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host, // SNI = original hostname for Nginx routing
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
const maxAttempts = 3;
|
||||
try {
|
||||
debugPrint('createRiderLog payload ${json.encode(data)}');
|
||||
} catch (_) {}
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createRiderLog url $urldata (attempt $attempt)');
|
||||
debugPrint('createRiderLog status ${response.statusCode}');
|
||||
debugPrint('createRiderLog response ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint(
|
||||
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
|
||||
);
|
||||
// On 404/5xx, wait and retry to potentially hit a different CDN node
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('createRiderLog exception (attempt $attempt): $e');
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('createRiderLog failed after $maxAttempts attempts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> updateRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('updateRiderLog url: $urldata');
|
||||
debugPrint('updateRiderLog response: ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
} else {
|
||||
debugPrint(
|
||||
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Exception in updateRiderLog: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
|
||||
Map<String, dynamic>? getRiderLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderLog response ${response.body}');
|
||||
debugPrint('getRiderLog url ${urldata.toString()}');
|
||||
getRiderLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
|
||||
Map<String, dynamic>? getRiderCountResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('getRiderCount response ${response.body}');
|
||||
debugPrint('getRiderCount url ${urldata.toString()}');
|
||||
getRiderCountResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderCountResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createBreakRiderLog url $urldata');
|
||||
debugPrint('createBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('updateBreakRiderLog url $urldata');
|
||||
debugPrint('updateBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
}
|
||||
export 'package:miler/providers/Riderlog/riderlog_provider.dart'
|
||||
show
|
||||
BreakRiderLogProvider,
|
||||
CreateRiderLogProvider,
|
||||
GetRiderLogProvider,
|
||||
UpdateRiderLogProvider,
|
||||
unawaited;
|
||||
|
||||
@@ -1,67 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Deliverylog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken CDN nodes.
|
||||
/// Confirmed: 66.116.225.226 = 200 OK, 125.21.240.67 = 404/405.
|
||||
/// Empty since the delivery line moved to Doormile — see the note on the
|
||||
/// matching map in `providers/Riderlog/riderlog_provider.dart`. The old entry
|
||||
/// pinned `queue.workolik.com`, a host nothing calls now, and must not be
|
||||
/// re-pointed at `api.doormile.com`.
|
||||
const _knownGoodIPs = <String, String>{};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs to avoid broken CDN nodes from carrier DNS
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
|
||||
true;
|
||||
|
||||
httpClient
|
||||
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then(
|
||||
(plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host,
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
),
|
||||
)
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
/// ── The SSL bypass is gone ──
|
||||
///
|
||||
/// This file built its own `IOClient` with `onBadCertificate: (_) => true` and
|
||||
/// a hand-rolled `SecureSocket` upgrade, so **every certificate was accepted**
|
||||
/// — a delivery rider's position, his order ids and his session were readable
|
||||
/// and rewritable by anything on the path. The parcel line deleted the same
|
||||
/// code (see `providers/Riderlog/riderlog_provider.dart`); the delivery line
|
||||
/// kept it because nothing in `lib/` imported this file directly.
|
||||
///
|
||||
/// It also sent **no `Authorization` header**. The URLs it posts to are v1
|
||||
/// routes on `api.doormile.com` — resolved through the [DeliveryApi] facade —
|
||||
/// so every one of these calls could only ever have been answered with a 401.
|
||||
/// A plain client plus [ApiConfig.authHeaders] fixes both at once.
|
||||
Future<Map<String, String>> _headers() => ApiConfig.authHeaders();
|
||||
|
||||
class CreateDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> createDeliveryLog(
|
||||
@@ -70,19 +28,12 @@ class CreateDeliveryLogProvider {
|
||||
bool wrapInArray = true,
|
||||
}) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final body = json.encode(wrapInArray ? [data] : data);
|
||||
final response = await client
|
||||
.post(
|
||||
url,
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.post(url, body: body, headers: await _headers())
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('createDeliveryLog url $urldata');
|
||||
debugPrint(body);
|
||||
@@ -110,18 +61,11 @@ class UpdateDeliveryProvider {
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.put(url, body: json.encode(data), headers: await _headers())
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateDelivery url $urldata');
|
||||
debugPrint('updateDelivery status ${response.statusCode}');
|
||||
@@ -148,18 +92,11 @@ class UpdateDeliveryProvider {
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.put(url, body: json.encode(data), headers: await _headers())
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateArrived url $urldata');
|
||||
debugPrint('updateArrived status ${response.statusCode}');
|
||||
@@ -185,18 +122,11 @@ class UpdateDeliveryProvider {
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.put(url, body: json.encode(data), headers: await _headers())
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updatePicked url $urldata');
|
||||
debugPrint('updatePicked status ${response.statusCode}');
|
||||
@@ -222,18 +152,11 @@ class UpdateDeliveryProvider {
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client
|
||||
.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
.put(url, body: json.encode(data), headers: await _headers())
|
||||
.timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateActive url $urldata');
|
||||
debugPrint('updateActive status ${response.statusCode}');
|
||||
@@ -258,7 +181,7 @@ class UpdateDeliveryProvider {
|
||||
class GetDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> getDeliveryLog(String urldata) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.get(
|
||||
|
||||
@@ -34,6 +34,7 @@ import 'package:miler/xpress/views/Dashboard/deliveries/deliveries.dart'
|
||||
as deliveries;
|
||||
import 'package:miler/xpress/background/live_tracking_service.dart';
|
||||
import 'package:miler/xpress/data/delivery_api.dart';
|
||||
import 'package:miler/data/heartbeat.dart';
|
||||
|
||||
/// Helper function to launch phone dialer - works in both debug and release builds
|
||||
/// In release builds, canLaunchUrl may fail due to R8/ProGuard, so we always try to launch
|
||||
@@ -320,11 +321,13 @@ class _HomepageState extends State<Homepage>
|
||||
Future<Duration> _getLogInterval() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final secs = prefs.getInt('logseconds');
|
||||
final int interval = (secs != null && secs > 0) ? secs : 30;
|
||||
// Already correct — routed through the shared resolver so the 30 lives in
|
||||
// one place. See [kDefaultLogSeconds] for what a zero here used to cost
|
||||
// the rider-log loop, which read the same pref and did not guard it.
|
||||
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
|
||||
return Duration(seconds: interval);
|
||||
} catch (_) {
|
||||
return const Duration(seconds: 30);
|
||||
return const Duration(seconds: kDefaultLogSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user