@@ -6,6 +6,9 @@ import 'package:http/http.dart' as http;
66import '../models/repeater.dart' ;
77import '../utils/debug_logger_io.dart' ;
88
9+ /// Result of a batch upload attempt
10+ enum UploadResult { success, retryable, nonRetryable }
11+
912/// MeshMapper API service
1013/// Handles communication with the MeshMapper backend
1114///
@@ -473,7 +476,7 @@ class ApiService {
473476 const criticalErrors = {
474477 'session_expired' , 'session_invalid' , 'session_revoked' , 'bad_session' ,
475478 'invalid_key' , 'unauthorized' , 'bad_key' ,
476- 'outside_zone' , 'zone_full' ,
479+ 'outside_zone' , 'zone_full' , 'zone_disabled' ,
477480 };
478481 if (criticalErrors.contains (reason)) {
479482 _clearSession ();
@@ -544,9 +547,28 @@ class ApiService {
544547 if (result? ['success' ] == true ) {
545548 debugLog ('[HEARTBEAT] Heartbeat successful' );
546549 // Next heartbeat will be scheduled when we get new expires_at
547- } else {
548- debugWarn ('[HEARTBEAT] Heartbeat failed: ${result ?['message' ]}' );
550+ } else if (result == null ) {
551+ // Network error — transient, trigger session expiring
552+ debugWarn ('[HEARTBEAT] Heartbeat failed: network error' );
549553 _onSessionExpiring? .call ();
554+ } else {
555+ // Server returned an error — check if critical
556+ final reason = result['reason' ] as String ? ;
557+ final message = result['message' ] as String ? ;
558+ debugWarn ('[HEARTBEAT] Heartbeat failed: $reason - $message ' );
559+
560+ const criticalErrors = {
561+ 'session_expired' , 'session_invalid' , 'session_revoked' , 'bad_session' ,
562+ 'invalid_key' , 'unauthorized' , 'bad_key' ,
563+ 'outside_zone' , 'zone_full' , 'zone_disabled' ,
564+ };
565+
566+ if (criticalErrors.contains (reason)) {
567+ _clearSession ();
568+ onSessionError? .call (reason, message);
569+ } else {
570+ _onSessionExpiring? .call ();
571+ }
550572 }
551573 }
552574
@@ -572,28 +594,28 @@ class ApiService {
572594 /// Callback for maintenance mode detection (while connected)
573595 void Function (String message, String ? url)? onMaintenanceMode;
574596
575- /// Legacy: Upload batch (wrapper for submitWardriveData)
576- /// Returns true on success, false on failure
597+ /// Upload batch of wardrive data
598+ /// Returns UploadResult indicating success, retryable failure, or non-retryable failure
577599 /// Triggers onSessionError callback for session-related errors
578- Future <bool > uploadBatch (List <Map <String , dynamic >> pings) async {
579- if (pings.isEmpty) return true ;
600+ Future <UploadResult > uploadBatch (List <Map <String , dynamic >> pings) async {
601+ if (pings.isEmpty) return UploadResult .success ;
580602
581603 try {
582604 final result = await submitWardriveData (pings);
583605
584606 if (result == null ) {
585607 debugError ('[API] Upload batch failed: no response' );
586- return false ;
608+ return UploadResult .retryable ;
587609 }
588610
589611 // Check for maintenance mode first
590612 if (_checkMaintenanceMode (result)) {
591- return false ;
613+ return UploadResult .retryable ;
592614 }
593615
594616 if (result['success' ] == true ) {
595617 debugLog ('[API] Upload batch SUCCESS: ${pings .length } items' );
596- return true ;
618+ return UploadResult .success ;
597619 }
598620
599621 // Check for session errors that require disconnect
@@ -606,7 +628,7 @@ class ApiService {
606628 // Auth errors
607629 'invalid_key' , 'unauthorized' , 'bad_key' ,
608630 // Zone errors
609- 'outside_zone' , 'zone_full' ,
631+ 'outside_zone' , 'zone_full' , 'zone_disabled' ,
610632 };
611633
612634 if (criticalErrors.contains (reason)) {
@@ -616,12 +638,22 @@ class ApiService {
616638 _clearSession ();
617639 // Notify listener for auto-disconnect
618640 onSessionError? .call (reason, message);
641+ return UploadResult .nonRetryable;
642+ }
643+
644+ // Errors where the batch data itself is invalid — retrying won't help
645+ const nonRetryableErrors = {
646+ 'gps_inaccurate' , 'gps_stale' , 'invalid_request' , 'zone_disabled' , 'outofdate' ,
647+ };
648+ if (nonRetryableErrors.contains (reason)) {
649+ debugWarn ('[API] Upload batch non-retryable error: $reason - discarding batch' );
650+ return UploadResult .nonRetryable;
619651 }
620652
621- return false ;
653+ return UploadResult .retryable ;
622654 } catch (e) {
623655 debugError ('[API] Upload batch exception: $e ' );
624- return false ;
656+ return UploadResult .retryable ;
625657 }
626658 }
627659
0 commit comments