diff --git a/lib/providers/app_state_provider.dart b/lib/providers/app_state_provider.dart index 059dda5..70ba024 100644 --- a/lib/providers/app_state_provider.dart +++ b/lib/providers/app_state_provider.dart @@ -5100,6 +5100,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver { // Extract session_id into local variable — never stored in shared state final offlineSessionId = effectiveAuth!['session_id'] as String?; + final uploadPublicKey = publicKey!; if (offlineSessionId == null) { debugError('[OFFLINE] Auth succeeded but no session_id in response'); return OfflineUploadResult.authFailed; @@ -5179,6 +5180,12 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver { if (result == UploadResult.success) { uploadedCount += batch.length; debugLog('[OFFLINE] Uploaded batch $batchNum: ${batch.length} pings'); + _forwardOfflineBatchToCustomApi( + batch, + batchNum: batchNum, + publicKey: uploadPublicKey, + auth: effectiveAuth, + ); continue; } @@ -5234,6 +5241,36 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver { } } + /// Forward a successfully uploaded offline batch to the Custom API endpoint. + /// + /// The whole batch is stamped with the upload-location zone (the fresh + /// /auth at the current GPS position), not where the pings were recorded — + /// the client cannot compute per-ping zones for sessions that span regions. + void _forwardOfflineBatchToCustomApi( + List> batch, { + required int batchNum, + required String publicKey, + required Map auth, + }) { + final contact = publicKey.length >= 8 + ? publicKey.substring(0, 8).toUpperCase() + : null; + var iata = zoneCode ?? _preferences.iataCode; + final zone = auth['zone']; + if (zone is Map && zone['code'] != null) { + iata = zone['code'].toString(); + } + debugLog( + '[OFFLINE] Custom API forward for batch $batchNum ' + '(${batch.length} pings, contact=$contact, iata=$iata)'); + _customApiService.forwardPings( + batch, + contactOverride: contact, + iataOverride: iata, + source: 'offline', + ); + } + /// Delete an offline session without uploading Future deleteOfflineSession(String filename) async { await _offlineSessionService.deleteSession(filename); diff --git a/lib/services/custom_api_service.dart b/lib/services/custom_api_service.dart index f5ba0c7..a657c86 100644 --- a/lib/services/custom_api_service.dart +++ b/lib/services/custom_api_service.dart @@ -41,17 +41,44 @@ class CustomApiService { _client = client ?? http.Client(); /// Fire-and-forget: forward pings to the custom endpoint. - /// Called after successful MeshMapper upload. Never throws. - void forwardPings(List> pings) { + /// Called after successful MeshMapper upload (online queue or offline session). + /// Never throws. + void forwardPings( + List> pings, { + String? contactOverride, + String? iataOverride, + String source = 'online', + }) { + if (pings.isEmpty) return; + final prefs = _prefsGetter(); if (!prefs.customApiEnabled) return; - if (prefs.customApiUrl == null || prefs.customApiUrl!.isEmpty) return; - if (prefs.customApiKey == null || prefs.customApiKey!.isEmpty) return; + + debugLog( + '[CUSTOM API] Forward requested ($source): ${pings.length} item(s)'); + + if (prefs.customApiUrl == null || prefs.customApiUrl!.isEmpty) { + debugLog('[CUSTOM API] Forward skipped ($source): URL not configured'); + return; + } + if (prefs.customApiKey == null || prefs.customApiKey!.isEmpty) { + debugLog('[CUSTOM API] Forward skipped ($source): API key not configured'); + return; + } // Enrich with contact and iata (custom API only — never sent to MeshMapper) - final contact = - prefs.customApiIncludeContact ? contactGetter?.call() : null; - final iata = iataGetter?.call(); + final contact = prefs.customApiIncludeContact + ? (contactOverride ?? contactGetter?.call()) + : null; + final iata = iataOverride ?? iataGetter?.call(); + + // Log the endpoint host only — the full URL may embed auth tokens, and + // debug log files are uploaded verbatim with bug reports. + var endpointHost = Uri.tryParse(prefs.customApiUrl!)?.host ?? ''; + if (endpointHost.isEmpty) endpointHost = 'custom endpoint'; + + debugLog('[CUSTOM API] Forwarding ($source) → $endpointHost ' + '(contact=${contact ?? "off"}, iata=${iata ?? "none"})'); final enriched = pings.map((ping) { final enrichedPing = Map.from(ping);