diff --git a/lib/services/agent_health_service.dart b/lib/services/agent_health_service.dart index 52d8eab..be90ac3 100644 --- a/lib/services/agent_health_service.dart +++ b/lib/services/agent_health_service.dart @@ -225,8 +225,9 @@ class AgentHealthService { // Primary: server relay (Twilio / Edge) — no Android SEND_SMS required. final relayUrl = dotenv.env['SMS_DISPATCH_URL']?.trim() ?? ''; final relayKey = dotenv.env['SMS_DISPATCH_ANON_KEY']?.trim() ?? ''; - if (relayUrl.isNotEmpty && relayKey.isNotEmpty) + if (relayUrl.isNotEmpty && relayKey.isNotEmpty) { return AgentReadiness.ready; + } // Fallback: open SMS app intent (no permission). If relay isn't configured, // we mark this as degraded (still usable, but requires user interaction). diff --git a/lib/services/family_tracking_service.dart b/lib/services/family_tracking_service.dart index 7f42975..4f55cb2 100644 --- a/lib/services/family_tracking_service.dart +++ b/lib/services/family_tracking_service.dart @@ -30,10 +30,12 @@ class FamilyTrackingService { final digits = raw.replaceAll(RegExp(r'\D'), ''); if (digits.length < 10) return null; if (digits.length == 10) return digits; - if (digits.length == 11 && digits.startsWith('0')) + if (digits.length == 11 && digits.startsWith('0')) { return digits.substring(1); - if (digits.length >= 12 && digits.startsWith('91')) + } + if (digits.length >= 12 && digits.startsWith('91')) { return digits.substring(digits.length - 10); + } return digits.length > 10 ? digits.substring(digits.length - 10) : digits; } diff --git a/lib/services/gemma_model_manager.dart b/lib/services/gemma_model_manager.dart index 500d00c..0fde426 100644 --- a/lib/services/gemma_model_manager.dart +++ b/lib/services/gemma_model_manager.dart @@ -56,8 +56,7 @@ class GemmaModelManager { static const String hfTokenUrl = 'https://huggingface.co/settings/tokens'; /// SharedPreferences keys. - static const String prefAutoDownloadOptOut = - 'gemma_auto_download_opt_out_v1'; + static const String prefAutoDownloadOptOut = 'gemma_auto_download_opt_out_v1'; static const String prefAutoDownloadInFlight = 'gemma_auto_download_in_flight_v1'; @@ -147,7 +146,9 @@ class GemmaModelManager { e.statusCode != 429) { rethrow; } - appLog.w('[GemmaModel] $url failed (${e.statusCode}); trying next mirror'); + appLog.w( + '[GemmaModel] $url failed (${e.statusCode}); trying next mirror', + ); } } throw lastErr ?? @@ -233,7 +234,9 @@ class GemmaModelManager { try { await for (final chunk in response.stream) { if (cancelToken?.isCancelled ?? false) { - appLog.i('[GemmaModel] Download cancelled — partial file kept for resume'); + appLog.i( + '[GemmaModel] Download cancelled — partial file kept for resume', + ); await sink.flush(); await sink.close(); return; @@ -260,7 +263,9 @@ class GemmaModelManager { // Atomic rename: .download → final path. await tmpFile.rename(path); - appLog.i('[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path'); + appLog.i( + '[GemmaModel] ✓ Download complete — ${(finalSize / 1e6).round()} MB at $path', + ); } finally { client.close(); } diff --git a/lib/services/government_facility_seed_service.dart b/lib/services/government_facility_seed_service.dart index 3ef6e93..0a8437e 100644 --- a/lib/services/government_facility_seed_service.dart +++ b/lib/services/government_facility_seed_service.dart @@ -45,14 +45,11 @@ class GovernmentFacilitySeedService { } if (batchParameters.isNotEmpty) { - await db.executeBatch( - ''' + await db.executeBatch(''' INSERT OR REPLACE INTO emergency_facilities (id, name, type, latitude, longitude, contact_number, capabilities, data_source, state_code, district) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', - batchParameters, - ); + ''', batchParameters); } await prefs.setInt(_prefsKeyImportedVersion, v); diff --git a/lib/services/roadsos_assistant_service.dart b/lib/services/roadsos_assistant_service.dart index 28fb277..c083aba 100644 --- a/lib/services/roadsos_assistant_service.dart +++ b/lib/services/roadsos_assistant_service.dart @@ -195,21 +195,25 @@ class RoadSosAssistantService extends StateNotifier { /// into the generic five-question script when offline. String _detectSceneContextLocal(String input) { final lower = input.toLowerCase(); - if (lower.contains('pedestrian') || lower.contains('पैदल')) + if (lower.contains('pedestrian') || lower.contains('पैदल')) { return 'pedestrian_hit'; + } if (lower.contains('rollover') || lower.contains('पलटा') || - lower.contains('overturned')) + lower.contains('overturned')) { return 'rollover'; + } if (lower.contains('fire') || lower.contains('आग') || lower.contains('smoke') || - lower.contains('धुआँ')) + lower.contains('धुआँ')) { return 'fire_hazard'; + } if (lower.contains('collision') || lower.contains('टक्कर') || - lower.contains('crash')) + lower.contains('crash')) { return 'vehicle_collision'; + } return 'unknown'; } diff --git a/lib/services/tier2_local_triage_model.dart b/lib/services/tier2_local_triage_model.dart index c121bd7..ba8d425 100644 --- a/lib/services/tier2_local_triage_model.dart +++ b/lib/services/tier2_local_triage_model.dart @@ -20,12 +20,15 @@ class Tier2LocalTriageModel { var score = 0; // Life-threatening indicators. - if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest'])) + if (_hasAny(t, const ['not breathing', 'no pulse', 'cardiac arrest'])) { score += 8; - if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive'])) + } + if (_hasAny(t, const ['unconscious', 'passed out', 'unresponsive'])) { score += 6; - if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting'])) + } + if (_hasAny(t, const ['bleeding heavily', 'severe bleeding', 'spurting'])) { score += 6; + } if (_hasAny(t, const ['trapped', 'pinned', 'stuck in vehicle'])) score += 6; // Major trauma / dangerous context. @@ -34,8 +37,9 @@ class Tier2LocalTriageModel { if (_hasAny(t, const ['fire', 'smoke', 'burning', 'explosion'])) score += 5; // Moderate indicators. - if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident'])) + if (_hasAny(t, const ['pain', 'hurt', 'bleeding', 'crash', 'accident'])) { score += 2; + } // Hint from crash detection (1-5): amplify but do not let it dominate fully. score += (h - 1) * 2; @@ -93,17 +97,21 @@ class Tier2LocalTriageModel { } String _firstAidQueryFromText(String t) { - if (t.contains('bleed')) + if (t.contains('bleed')) { return 'severe bleeding wound management tourniquet'; - if (_hasAny(t, const ['burn', 'smoke'])) + } + if (_hasAny(t, const ['burn', 'smoke'])) { return 'burn wound first aid cool water'; + } if (_hasAny(t, const ['not breathing', 'cpr', 'choking'])) { return 'CPR rescue breathing Heimlich'; } - if (_hasAny(t, const ['fracture', 'broken'])) + if (_hasAny(t, const ['fracture', 'broken'])) { return 'fracture immobilization splint'; - if (_hasAny(t, const ['head injury', 'concussion'])) + } + if (_hasAny(t, const ['head injury', 'concussion'])) { return 'head injury concussion protocol'; + } return 'general road accident first aid emergency response'; } } diff --git a/lib/services/vital_signs_service.dart b/lib/services/vital_signs_service.dart index e4afe26..5220a49 100644 --- a/lib/services/vital_signs_service.dart +++ b/lib/services/vital_signs_service.dart @@ -89,8 +89,9 @@ class VitalSignsLogger extends StateNotifier { if (respiratoryRate >= 24) flags.add('tachypnoea (rapid breathing)'); if (respiratoryRate <= 8) flags.add('bradypnoea (slow breathing)'); if (bloodOxygen < 90) flags.add('hypoxia (SpO2 <90% — CRITICAL)'); - if (bloodOxygen >= 90 && bloodOxygen < 94) + if (bloodOxygen >= 90 && bloodOxygen < 94) { flags.add('borderline oxygen (SpO2 90-93%)'); + } if (flags.isEmpty) { return 'No immediately critical vital signs from bystander observation.'; diff --git a/lib/ui/gemma_model_download_screen.dart b/lib/ui/gemma_model_download_screen.dart index 2eaf45a..8e3f141 100644 --- a/lib/ui/gemma_model_download_screen.dart +++ b/lib/ui/gemma_model_download_screen.dart @@ -119,8 +119,9 @@ class _GemmaModelDownloadScreenState extends State { Future _openUrl(String url) async { final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) + if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); + } } // ── Build ───────────────────────────────────────────────────────────────── diff --git a/lib/ui/map_widget.dart b/lib/ui/map_widget.dart index 9bf073c..5dddabe 100644 --- a/lib/ui/map_widget.dart +++ b/lib/ui/map_widget.dart @@ -46,8 +46,9 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { // - If custom provider fails (keys/restrictions), fall back to Carto. // - If Carto fails (blocked network / DNS / captive portal), fall back to // OSM tile CDN for demo resilience (do not ship high-volume traffic there). - if (!t.contains('basemaps.cartocdn.com')) + if (!t.contains('basemaps.cartocdn.com')) { return MapTileConfig.cartoDarkMatter; + } return MapTileConfig.tileOpenstreetmapOrgViolatesPolicyAtScale; } @@ -303,8 +304,9 @@ class _RoadSosMapState extends State with TickerProviderStateMixin { } List _buildIncidentMarkers() { - if (widget.state.incidentId == null || widget.state.location == null) + if (widget.state.incidentId == null || widget.state.location == null) { return []; + } if (widget.state.location!.source == 'unknown') return []; return [ diff --git a/test/dispatch_status_panel_test.dart b/test/dispatch_status_panel_test.dart index 43b4fd2..bda3e96 100644 --- a/test/dispatch_status_panel_test.dart +++ b/test/dispatch_status_panel_test.dart @@ -4,7 +4,9 @@ import 'package:roadsos/models/dispatch_channel_status.dart'; import 'package:roadsos/ui/dispatch_status_panel.dart'; void main() { - testWidgets('DispatchStatusPanel shows channel titles and lifecycle detail', (tester) async { + testWidgets('DispatchStatusPanel shows channel titles and lifecycle detail', ( + tester, + ) async { const channels = [ DispatchChannelRow( id: 'sms', @@ -22,10 +24,11 @@ void main() { await tester.pumpWidget( MaterialApp( - theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)), - home: const Scaffold( - body: DispatchStatusPanel(channels: channels), + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), ), + home: const Scaffold(body: DispatchStatusPanel(channels: channels)), ), ); @@ -36,12 +39,12 @@ void main() { expect(find.text('No peers in range.'), findsOneWidget); }); - testWidgets('DispatchStatusPanel renders nothing for empty channels', (tester) async { + testWidgets('DispatchStatusPanel renders nothing for empty channels', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( - home: Scaffold( - body: DispatchStatusPanel(channels: const []), - ), + home: Scaffold(body: DispatchStatusPanel(channels: const [])), ), ); diff --git a/test/india_emergency_routing_test.dart b/test/india_emergency_routing_test.dart index ece3e54..035a346 100644 --- a/test/india_emergency_routing_test.dart +++ b/test/india_emergency_routing_test.dart @@ -22,32 +22,38 @@ void main() { expect(r?.stateCode, 'IN-KA'); }); - test('Tamil Nadu box bounds (8.0–13.6N, 76.0–80.6E) excluding KA overlap', () { - // TN SW corner - var r = resolveIndiaEmergencyRoute(8.0, 76.0); - expect(r?.stateCode, 'IN-TN'); - - // TN NW corner (Note: 13.6, 76.0 overlaps with KA box. The code checks KA first, - // so it will resolve to IN-KA. We test a point just east of KA box for TN NW corner.) - r = resolveIndiaEmergencyRoute(13.6, 79.0); - expect(r?.stateCode, 'IN-TN'); - - // TN NE corner - r = resolveIndiaEmergencyRoute(13.6, 80.6); - expect(r?.stateCode, 'IN-TN'); - - // TN SE corner - r = resolveIndiaEmergencyRoute(8.0, 80.6); - expect(r?.stateCode, 'IN-TN'); - }); - - test('Tamil Nadu bounds that overlap with Karnataka resolve to Karnataka', () { - // The point (12.0, 77.0) is in both TN box (8.0-13.6, 76.0-80.6) - // and KA box (11.5-18.6, 74.0-78.9). - // Since KA is checked first in _resolveByCoarseBoxes, it must resolve to KA. - final r = resolveIndiaEmergencyRoute(12.0, 77.0); - expect(r?.stateCode, 'IN-KA'); - }); + test( + 'Tamil Nadu box bounds (8.0–13.6N, 76.0–80.6E) excluding KA overlap', + () { + // TN SW corner + var r = resolveIndiaEmergencyRoute(8.0, 76.0); + expect(r?.stateCode, 'IN-TN'); + + // TN NW corner (Note: 13.6, 76.0 overlaps with KA box. The code checks KA first, + // so it will resolve to IN-KA. We test a point just east of KA box for TN NW corner.) + r = resolveIndiaEmergencyRoute(13.6, 79.0); + expect(r?.stateCode, 'IN-TN'); + + // TN NE corner + r = resolveIndiaEmergencyRoute(13.6, 80.6); + expect(r?.stateCode, 'IN-TN'); + + // TN SE corner + r = resolveIndiaEmergencyRoute(8.0, 80.6); + expect(r?.stateCode, 'IN-TN'); + }, + ); + + test( + 'Tamil Nadu bounds that overlap with Karnataka resolve to Karnataka', + () { + // The point (12.0, 77.0) is in both TN box (8.0-13.6, 76.0-80.6) + // and KA box (11.5-18.6, 74.0-78.9). + // Since KA is checked first in _resolveByCoarseBoxes, it must resolve to KA. + final r = resolveIndiaEmergencyRoute(12.0, 77.0); + expect(r?.stateCode, 'IN-KA'); + }, + ); }); group('just outside coarse boxes', () { diff --git a/test/services/crash_confidence_engine_test.dart b/test/services/crash_confidence_engine_test.dart index 0c60130..4921404 100644 --- a/test/services/crash_confidence_engine_test.dart +++ b/test/services/crash_confidence_engine_test.dart @@ -57,7 +57,10 @@ void main() { final result = CrashConfidenceEngine.score(signals); expect(result.tier, equals(CrashConfidenceTier.high)); - expect(result.incidentLabel, equals('Detected incident — possible emergency')); + expect( + result.incidentLabel, + equals('Detected incident — possible emergency'), + ); expect(result.score, equals(1.0)); }); diff --git a/test/structured_sms_service_test.dart b/test/structured_sms_service_test.dart index 092c139..a490a02 100644 --- a/test/structured_sms_service_test.dart +++ b/test/structured_sms_service_test.dart @@ -62,39 +62,41 @@ void main() { expect(body, contains('inc:0123456')); }); - test('Chennai medium severity routes to Tamil Nadu with "?" profile', - () async { - final body = await svc.buildStructured112Sms( - incidentId: 'abcdefab-0000-0000-0000-000000000000', - location: LocationFix( - latitude: 13.0827, - longitude: 80.2707, - accuracy: 12, - source: 'gps', - timestamp: DateTime.now(), - ), - triage: TriageResult( - functionCall: 'dispatch_emergency', - location: '13.0827,80.2707', - severityLevel: 3, - requiredServices: const ['ambulance'], - firstAidQuery: 'general', - compressedPayload: 'x', - thinkingTrace: null, - isDegradedMode: false, - source: TriageSource.localTier2, - visionUsed: false, - ), - profileOverride: UserProfile(), - useGemma: false, - ); + test( + 'Chennai medium severity routes to Tamil Nadu with "?" profile', + () async { + final body = await svc.buildStructured112Sms( + incidentId: 'abcdefab-0000-0000-0000-000000000000', + location: LocationFix( + latitude: 13.0827, + longitude: 80.2707, + accuracy: 12, + source: 'gps', + timestamp: DateTime.now(), + ), + triage: TriageResult( + functionCall: 'dispatch_emergency', + location: '13.0827,80.2707', + severityLevel: 3, + requiredServices: const ['ambulance'], + firstAidQuery: 'general', + compressedPayload: 'x', + thinkingTrace: null, + isDegradedMode: false, + source: TriageSource.localTier2, + visionUsed: false, + ), + profileOverride: UserProfile(), + useGemma: false, + ); - expect(body, startsWith('RSOS|IN-TN|')); - expect(body, contains('|S3|')); - expect(body, contains('|amb|')); - expect(body, contains('|?|')); - expect(body.length, lessThanOrEqualTo(kMaxStructured112SmsLength)); - }); + expect(body, startsWith('RSOS|IN-TN|')); + expect(body, contains('|S3|')); + expect(body, contains('|amb|')); + expect(body, contains('|?|')); + expect(body.length, lessThanOrEqualTo(kMaxStructured112SmsLength)); + }, + ); test('Off-India coordinates fall back to "IN" state token', () async { final body = await svc.buildStructured112Sms( diff --git a/test/widget_test.dart b/test/widget_test.dart index ae0ef1b..8e67ec6 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -7,7 +7,8 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('dotenv loads strings used by RuntimeConfig / bootstrap', () { dotenv.loadFromString( - envString: 'SUPABASE_URL=https://example.supabase.co\nSUPABASE_ANON_KEY=test_anon', + envString: + 'SUPABASE_URL=https://example.supabase.co\nSUPABASE_ANON_KEY=test_anon', ); expect(dotenv.env['SUPABASE_URL'], contains('supabase')); expect(dotenv.env['SUPABASE_ANON_KEY'], 'test_anon');