diff --git a/assets/speed_test_indicator_img/coverage.png b/assets/speed_test_indicator_img/coverage.png new file mode 100644 index 0000000..81bb82a Binary files /dev/null and b/assets/speed_test_indicator_img/coverage.png differ diff --git a/assets/speed_test_indicator_img/validation_ap.png b/assets/speed_test_indicator_img/validation_ap.png new file mode 100644 index 0000000..25113e6 Binary files /dev/null and b/assets/speed_test_indicator_img/validation_ap.png differ diff --git a/assets/speed_test_indicator_img/validation_ont.png b/assets/speed_test_indicator_img/validation_ont.png new file mode 100644 index 0000000..4d05821 Binary files /dev/null and b/assets/speed_test_indicator_img/validation_ont.png differ diff --git a/docs/IPERF3_IMPLEMENTATION.md b/docs/IPERF3_IMPLEMENTATION.md new file mode 100644 index 0000000..2cb1f45 --- /dev/null +++ b/docs/IPERF3_IMPLEMENTATION.md @@ -0,0 +1,1527 @@ +# iPerf3 Speed Test Implementation + +## Overview + +This document describes the iPerf3 speed test implementation in the FDK (Field Development Kit) application. The system provides network performance measurement using native iPerf3 binaries on iOS and Android platforms. + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Flutter (Dart) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ SpeedTestService │────▶│ Iperf3Service │ │ +│ │ (Orchestrator) │ │ (Native Bridge) │ │ +│ │ │ │ │ │ +│ │ • Test lifecycle │ │ • MethodChannel │ │ +│ │ • Server fallback │ │ • EventChannel │ │ +│ │ • Progress streams │ │ • JSON parsing │ │ +│ └─────────────────────┘ └──────────┬──────────┘ │ +│ │ │ +│ ┌─────────────────────┐ │ │ +│ │ NetworkGatewayService│ │ │ +│ │ │ │ │ +│ │ • Gateway detection│ │ │ +│ │ • WiFi info │ │ │ +│ └─────────────────────┘ │ │ +│ │ │ +└─────────────────────────────────────────┼───────────────────────────────────┘ + │ + MethodChannel: com.rgnets.fdk/iperf3 + EventChannel: com.rgnets.fdk/iperf3_progress + │ +┌─────────────────────────────────────────┼───────────────────────────────────┐ +│ Native Platform │ +├─────────────────────────────────────────┼───────────────────────────────────┤ +│ │ │ +│ ┌──────────────────────────────────────┴──────────────────────────────┐ │ +│ │ Platform Channel Handler │ │ +│ │ │ │ +│ │ iOS: Iperf3Plugin.swift │ │ +│ │ Android: Iperf3Plugin.kt │ │ +│ └──────────────────────────────────────┬──────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────┴──────────────────────────────┐ │ +│ │ iPerf3 Native Binary │ │ +│ │ │ │ +│ │ iOS: libiperf.a (static library) │ │ +│ │ Android: libiperf3.so (shared library per ABI) │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## File Structure + +``` +lib/features/speed_test/ +├── data/ +│ ├── datasources/ +│ │ ├── speed_test_data_source.dart # Abstract interface +│ │ └── speed_test_websocket_data_source.dart # WebSocket implementation +│ ├── repositories/ +│ │ └── speed_test_repository_impl.dart # Repository implementation +│ └── services/ +│ ├── iperf3_service.dart # Native bridge service +│ ├── speed_test_service.dart # Main orchestrator +│ └── network_gateway_service.dart # Network utilities +├── domain/ +│ ├── entities/ +│ │ ├── speed_test_config.dart # Test configuration (Freezed) +│ │ ├── speed_test_result.dart # Test result (Freezed) +│ │ ├── speed_test_with_results.dart # Joined entity +│ │ └── speed_test_status.dart # Status enum +│ └── repositories/ +│ └── speed_test_repository.dart # Repository interface +└── presentation/ + ├── providers/ + │ └── speed_test_providers.dart # Riverpod providers + └── widgets/ + ├── speed_test_card.dart # UI card widget + └── speed_test_popup.dart # Test popup dialog +``` + +--- + +## Core Components + +### 1. Iperf3Service (Native Bridge) + +**File:** `lib/features/speed_test/data/services/iperf3_service.dart` + +This service acts as a bridge between Flutter and the native iPerf3 implementation. + +#### Platform Channels + +```dart +static const MethodChannel _channel = MethodChannel('com.rgnets.fdk/iperf3'); +static const EventChannel _progressChannel = EventChannel('com.rgnets.fdk/iperf3_progress'); +``` + +#### Available Methods + +| Method | Description | Parameters | +|--------|-------------|------------| +| `runClient()` | Execute speed test | host, port, duration, streams, reverse, useUdp, bandwidth | +| `startServer()` | Start iPerf3 server mode | port, useUdp | +| `stopServer()` | Stop running server | - | +| `cancelClient()` | Cancel running test | - | +| `getVersion()` | Get iPerf3 version | - | +| `getDefaultGateway()` | Get device gateway IP | - | +| `getGatewayForDestination()` | Get gateway for hostname | hostname | + +#### runClient() Parameters + +```dart +Future> runClient({ + required String serverHost, // Target server IP/hostname + int port = 5201, // iPerf3 port (default 5201) + int durationSeconds = 10, // Test duration per phase + int parallelStreams = 1, // Concurrent streams + bool reverse = false, // true=download, false=upload + bool useUdp = true, // UDP or TCP protocol + int? bandwidthMbps, // Bandwidth limit (UDP only) +}) +``` + +#### Return Value Structure + +```dart +{ + 'success': bool, // Test completed successfully + 'error': String?, // Error message if failed + + // Speed measurements + 'sendMbps': double, // Upload speed in Mbps + 'receiveMbps': double, // Download speed in Mbps + 'sentBytes': int, // Total bytes sent + 'receivedBytes': int, // Total bytes received + + // Latency (protocol-dependent) + 'rtt': double, // TCP: Round-trip time (ms) + 'jitter': double, // UDP: Jitter (ms) + + // UDP-specific + 'lostPackets': int, // Packets lost + 'totalPackets': int, // Total packets + 'lostPercent': double, // Packet loss percentage + + 'jsonOutput': String, // Raw iPerf3 JSON output +} +``` + +--- + +### 2. SpeedTestService (Orchestrator) - In Depth + +**File:** `lib/features/speed_test/data/services/speed_test_service.dart` + +The SpeedTestService is the main orchestrator that manages the complete speed test lifecycle. It coordinates between the native iPerf3 bridge, network gateway detection, and provides reactive streams for UI updates. + +--- + +#### Singleton Pattern + +The service uses a singleton pattern to ensure only one instance exists throughout the app lifecycle: + +```dart +class SpeedTestService { + // Private singleton instance + static final SpeedTestService _instance = SpeedTestService._internal(); + + // Factory constructor returns the singleton + factory SpeedTestService() => _instance; + + // Private internal constructor + SpeedTestService._internal(); + + // Dependencies + final Iperf3Service _iperf3Service = Iperf3Service(); + final NetworkGatewayService _gatewayService = NetworkGatewayService(); +} +``` + +**Why Singleton?** +- Ensures consistent state across the app +- Prevents multiple simultaneous tests +- Maintains single connection to native layer +- Preserves configuration and last result + +--- + +#### Internal State Management + +The service maintains several pieces of internal state: + +```dart +// ═══════════════════════════════════════════════════════════════════ +// CONFIGURATION STATE (persisted to SharedPreferences) +// ═══════════════════════════════════════════════════════════════════ +String _serverHost = ''; // Current/last tested server +String _serverLabel = ''; // Human-readable server name +int _serverPort = 5201; // iPerf3 port +int _testDuration = 10; // Seconds per phase +bool _useUdp = true; // Protocol selection +int _bandwidthMbps = 81; // UDP bandwidth limit +int _parallelStreams = 16; // Concurrent streams + +// ═══════════════════════════════════════════════════════════════════ +// RUNTIME STATE (not persisted) +// ═══════════════════════════════════════════════════════════════════ +SpeedTestStatus _status = SpeedTestStatus.idle; // Current status +SpeedTestResult? _lastResult; // Most recent result +double _progress = 0.0; // 0-100% + +// Phase tracking for live updates +bool _isDownloadPhase = true; // Which phase active +bool _isRetryingFallback = false; // Suppress errors during retry + +// Speed preservation across phases +double _completedDownloadSpeed = 0.0; // After download completes +double _completedUploadSpeed = 0.0; // After upload completes +``` + +--- + +#### Default Configuration + +| Parameter | Default | Description | Why This Value | +|-----------|---------|-------------|----------------| +| `serverPort` | 5201 | Standard iPerf3 port | Industry standard | +| `testDuration` | 10 sec | Duration per test phase | Balance of accuracy vs time | +| `useUdp` | true | UDP protocol | More accurate for WiFi | +| `bandwidthMbps` | 81 | Target bandwidth | Prevents network saturation | +| `parallelStreams` | 16 | Concurrent streams | Maximizes throughput measurement | + +--- + +#### Reactive Streams Architecture + +The service exposes four broadcast streams for UI reactivity: + +```dart +// ═══════════════════════════════════════════════════════════════════ +// STREAM CONTROLLERS (broadcast = multiple listeners allowed) +// ═══════════════════════════════════════════════════════════════════ + +final StreamController _statusController = + StreamController.broadcast(); + +final StreamController _resultController = + StreamController.broadcast(); + +final StreamController _progressController = + StreamController.broadcast(); + +final StreamController _statusMessageController = + StreamController.broadcast(); + +// ═══════════════════════════════════════════════════════════════════ +// PUBLIC STREAM GETTERS +// ═══════════════════════════════════════════════════════════════════ + +// Status: idle → running → completed/error +Stream get statusStream => _statusController.stream; + +// Results: emits live updates DURING test + final result +Stream get resultStream => _resultController.stream; + +// Progress: 0.0 to 100.0 percentage +Stream get progressStream => _progressController.stream; + +// Messages: "Testing download speed...", "Connected to gateway", etc. +Stream get statusMessageStream => _statusMessageController.stream; +``` + +**Stream Data Flow:** + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Native iPerf3 Progress │ +│ │ +│ EventChannel: com.rgnets.fdk/iperf3_progress │ +│ Emits: { status, interval, mbps, details } │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ _progressSubscription.listen() │ +│ │ +│ Receives native events and routes to appropriate handler │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌───────────────────────────┐ ┌───────────────────────────┐ +│ _handleStatusUpdate() │ │ _handleProgressUpdate() │ +│ │ │ │ +│ • Updates _status │ │ • Calculates progress % │ +│ • Emits status stream │ │ • Emits progress stream │ +│ • Emits message stream │ │ • Creates live result │ +│ • Handles errors │ │ • Emits result stream │ +└───────────────────────────┘ └───────────────────────────┘ + │ │ + ▼ ▼ + ┌───────────┐ ┌───────────┐ + │ UI │ │ UI │ + │ Widgets │ │ Widgets │ + └───────────┘ └───────────┘ +``` + +--- + +#### Initialization Process + +```dart +Future initialize() async { + // 1. Get SharedPreferences instance + _prefs = await SharedPreferences.getInstance(); + + // 2. Load saved configuration + await _loadConfiguration(); + + // 3. Override with optimal defaults (UDP, 16 streams, 81 Mbps) + _useUdp = true; + _parallelStreams = 16; + _bandwidthMbps = 81; + + // 4. Save the configuration + await _saveConfiguration(); + + // 5. Load last result (for UI display on app start) + await _loadLastResult(); + + // 6. Subscribe to native progress stream + _progressSubscription = _iperf3Service.getProgressStream().listen((progress) { + final status = progress['status']; + if (status != null && status is String) { + _handleStatusUpdate(status, progress['details']); + } else { + _handleProgressUpdate(progress); + } + }); +} +``` + +--- + +#### Test Execution Flow (Detailed) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ runSpeedTestWithFallback() │ +│ │ +│ Entry point for running a speed test with automatic retry │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 1: Guard Against Concurrent Tests │ +│ │ +│ if (_status == SpeedTestStatus.running) { │ +│ LoggerService.warning('Speed test already running'); │ +│ return; // Don't start another test │ +│ } │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 2: Initialize Test State │ +│ │ +│ _updateStatus(SpeedTestStatus.running); // Notify UI │ +│ _progress = 0.0; │ +│ _progressController.add(_progress); │ +│ _isRetryingFallback = true; // Suppress errors │ +│ _completedDownloadSpeed = 0.0; // Reset from last test │ +│ _completedUploadSpeed = 0.0; │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 3: Get Local IP Address │ +│ │ +│ final localIp = await _getLocalIpAddress(); │ +�� │ +│ // Iterates through network interfaces │ +│ // Returns first non-loopback IPv4 address │ +│ // Used to identify device in results │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 4: Build Fallback Server List │ +│ │ +│ final fallbackServers = await _buildFallbackList(configTarget); │ +│ │ +│ Priority Order: │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 1. Default Gateway (e.g., 192.168.1.1) │ │ +│ │ - Detected via NetworkGatewayService │ │ +│ │ - Fastest, tests local network │ │ +│ │ - Requires iPerf3 server running on gateway │ │ +│ ├─────────────────────────────────────────────────────────────┤ │ +│ │ 2. Config Target (from SpeedTestConfig.target) │ │ +│ │ - Only added if different from gateway │ │ +│ │ - May be external server hostname/IP │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 5: Server Iteration Loop │ +│ │ +│ for (int i = 0; i < fallbackServers.length; i++) { │ +│ final serverHost = fallbackServers[i]['host']; │ +│ final serverLabel = fallbackServers[i]['label']; │ +│ │ +│ // Show progress to user │ +│ _statusMessageController.add( │ +│ 'Attempt ${i+1}/${fallbackServers.length}: $serverLabel' │ +│ ); │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ STEP 6: Run Test With Server (_runTestWithServer) │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ PHASE 1: DOWNLOAD TEST │ │ +│ │ │ │ +│ │ _isDownloadPhase = true; │ │ +│ │ │ │ +│ │ downloadResult = await _iperf3Service.runClient( │ │ +│ │ serverHost: serverHost, │ │ +│ │ port: 5201, │ │ +│ │ durationSeconds: 10, │ │ +│ │ parallelStreams: 16, │ │ +│ │ reverse: true, // Server → Client = DOWNLOAD │ │ +│ │ useUdp: true, │ │ +│ │ bandwidthMbps: 81, │ │ +│ │ ); │ │ +│ │ │ │ +│ │ if (!success) return null; // Try next server │ │ +│ │ │ │ +│ │ _completedDownloadSpeed = downloadResult['receiveMbps']; │ │ +│ │ latency = downloadResult['jitter']; // or 'rtt' for TCP │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ PHASE 2: UPLOAD TEST │ │ +│ │ │ │ +│ │ _isDownloadPhase = false; │ │ +│ │ │ │ +│ │ uploadResult = await _iperf3Service.runClient( │ │ +│ │ serverHost: serverHost, │ │ +│ │ port: 5201, │ │ +│ │ durationSeconds: 10, │ │ +│ │ parallelStreams: 16, │ │ +│ │ reverse: false, // Client → Server = UPLOAD │ │ +│ │ useUdp: true, │ │ +│ │ bandwidthMbps: 81, │ │ +│ │ ); │ │ +│ │ │ │ +│ │ if (!success) return null; // Try next server │ │ +│ │ │ │ +│ │ uploadSpeed = uploadResult['sendMbps']; │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ CREATE RESULT │ │ +│ │ │ │ +│ │ return SpeedTestResult( │ │ +│ │ downloadMbps: downloadSpeed, │ │ +│ │ uploadMbps: uploadSpeed, │ │ +│ │ rtt: latency, │ │ +│ │ completedAt: DateTime.now(), │ │ +│ │ localIpAddress: localIp, │ │ +│ │ serverHost: serverHost, │ │ +│ │ ); │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + ▼ ▼ + ┌───────────┐ ┌───────────────┐ + │ Success │ │ Failed │ + │ │ │ │ + │ result │ │ result == │ + │ != null │ │ null │ + └─────┬─────┘ └───────┬───────┘ + │ │ + ▼ ▼ +┌─────────────────────────────┐ ┌─────────────────────────────┐ +│ STEP 7A: Success Path │ │ STEP 7B: Failure Path │ +│ │ │ │ +│ _isRetryingFallback=false; │ │ if (more servers left) { │ +│ _lastResult = result; │ │ // 1 second pause │ +│ _resultController.add(); │ │ await Future.delayed(); │ +│ _updateStatus(completed); │ │ continue; // Try next │ +│ _saveLastResult(result); │ │ } else { │ +│ return; // Exit loop │ │ _setErrorResult( │ +│ │ │ 'Unable to connect' │ +│ │ │ ); │ +│ │ │ } │ +└─────────────────────────────┘ └─────────────────────────────┘ +``` + +--- + +#### Live Progress Updates + +During test execution, the native layer sends progress events that are processed to provide real-time UI updates: + +```dart +void _handleProgressUpdate(Map progress) { + final interval = progress['interval'] as int?; // Current second + final speedMbps = progress['mbps'] as double?; // Current speed + + if (interval != null && _testDuration > 0) { + // Calculate percentage (0-100) + _progress = (interval / _testDuration * 100).clamp(0.0, 100.0); + _progressController.add(_progress); + + // Emit live result for UI updates + if (speedMbps != null && speedMbps > 0) { + final liveResult = SpeedTestResult( + // During download: show live download, no upload yet + // During upload: preserve completed download, show live upload + downloadMbps: _isDownloadPhase ? speedMbps : _completedDownloadSpeed, + uploadMbps: !_isDownloadPhase ? speedMbps : _completedUploadSpeed, + rtt: 0.0, + completedAt: DateTime.now(), + ); + + _resultController.add(liveResult); + } + } +} +``` + +**Visual Timeline:** + +``` +Download Phase (10 seconds) Upload Phase (10 seconds) +├──────────────────────────────────────────┼──────────────────────────────────────────┤ +│ sec 1: emit(down: 45.2, up: 0) │ sec 1: emit(down: 95.5, up: 12.3) │ +│ sec 2: emit(down: 67.8, up: 0) │ sec 2: emit(down: 95.5, up: 28.7) │ +│ sec 3: emit(down: 82.1, up: 0) │ sec 3: emit(down: 95.5, up: 35.4) │ +│ ... │ ... │ +│ sec 10: _completedDownloadSpeed = 95.5 │ sec 10: FINAL RESULT │ +│ switch to upload phase │ down: 95.5, up: 42.3 │ +└──────────────────────────────────────────┴──────────────────────────────────────────┘ +``` + +--- + +#### Status Message Generation + +Human-readable messages are generated based on the current state: + +```dart +void _handleStatusUpdate(String status, dynamic details) { + String getMessage() { + final serverInfo = _serverHost.isNotEmpty ? ' to $_serverHost' : ''; + + switch (status) { + case 'starting': + return 'Starting speed test...'; + + case 'running': + if (_isDownloadPhase) { + return 'Testing download speed$serverInfo...'; + } else { + return 'Testing upload speed$serverInfo...'; + } + + case 'completed': + return 'Test completed!'; + + case 'cancelled': + return 'Test cancelled'; + + case 'error': + final message = (details is Map && details['message'] != null) + ? details['message'].toString() + : 'Speed test failed'; + return 'Error: $message'; + + case 'idle': + return 'Ready'; + + default: + return 'Performing speed test$serverInfo...'; + } + } + + _statusMessageController.add(getMessage()); +} +``` + +--- + +#### Persistence (SharedPreferences) + +Configuration and last result are persisted for app restarts: + +```dart +// ═══════════════════════════════════════════════════════════════════ +// KEYS USED IN SHARED PREFERENCES +// ═══════════════════════════════════════════════════════════════════ +// 'speed_test_server_host' → String +// 'speed_test_server_port' → int +// 'speed_test_duration' → int +// 'speed_test_use_udp' → bool +// 'speed_test_bandwidth_mbps' → int +// 'speed_test_parallel_streams' → int +// 'speed_test_last_result' → String (JSON) + +Future _saveLastResult(SpeedTestResult result) async { + // Use compute() for JSON encoding on isolate (prevents UI jank) + final json = await compute(_encodeJson, result.toJson()); + await _prefs?.setString('speed_test_last_result', json); +} + +Future _loadLastResult() async { + final resultJson = _prefs?.getString('speed_test_last_result'); + if (resultJson != null) { + // Parse on isolate + final map = Map.from( + await compute(_parseJson, resultJson), + ); + _lastResult = SpeedTestResult.fromJson(map); + } +} +``` + +--- + +#### Error Handling Strategy + +```dart +// During fallback retry, errors are suppressed to avoid confusing the user +if (!_isRetryingFallback) { + _updateStatus(SpeedTestStatus.error); + _statusMessageController.add('Error: $message'); + _setErrorResult(message); +} + +// Error result factory +void _setErrorResult(String message) { + final result = SpeedTestResult.error(message); // hasError=true, passed=false + _lastResult = result; + _resultController.add(result); + _updateStatus(SpeedTestStatus.error); +} +``` + +**User Experience:** +- During fallback: User sees "Trying gateway...", "Trying test configuration..." +- Only after ALL servers fail: User sees "Unable to connect to server" +- No confusing intermediate error messages + +--- + +#### Cleanup + +```dart +void dispose() { + _progressSubscription?.cancel(); // Stop listening to native events + _statusController.close(); // Close all stream controllers + _resultController.close(); + _progressController.close(); + _statusMessageController.close(); +} +``` + +--- + +## Submitting Results & Fetching Configurations + +This section explains how speed test results are submitted to the server and how configurations are retrieved. + +### Complete Data Flow + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 1. FETCH CONFIGURATIONS │ +│ │ +│ UI Widget │ +│ │ │ +│ │ ref.watch(speedTestConfigsNotifierProvider) │ +│ ▼ │ +│ SpeedTestConfigsNotifier │ +│ │ │ +│ │ repository.getSpeedTestConfigs() │ +│ ▼ │ +│ SpeedTestRepositoryImpl │ +│ │ │ +│ │ dataSource.getSpeedTestConfigs() │ +│ ▼ │ +│ SpeedTestWebSocketDataSource │ +│ │ │ +│ │ webSocketService.requestActionCable( │ +│ │ action: 'index', │ +│ │ resourceType: 'speed_tests' │ +│ │ ) │ +│ ▼ │ +│ Rails Server │ +│ │ │ +│ │ Returns: { data: [ {id: 1, name: "Office", target: "192.168.1.1", │ +│ │ min_download_mbps: 50, ...}, ... ] } │ +│ ▼ │ +│ List │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 2. RUN SPEED TEST │ +│ │ +│ User taps "Run Test" button │ +│ │ │ +│ │ speedTestService.runSpeedTestWithFallback( │ +│ │ configTarget: config.target // e.g., "192.168.1.1" │ +│ │ ) │ +│ ▼ │ +│ SpeedTestService runs iPerf3 test │ +│ │ │ +│ │ Download test (reverse=true) → Upload test (reverse=false) │ +│ ▼ │ +│ SpeedTestResult created locally │ +│ (downloadMbps: 95.5, uploadMbps: 42.3, rtt: 12.5, ...) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 3. SUBMIT RESULT TO SERVER │ +│ │ +│ UI receives result from speedTestService.resultStream │ +│ │ │ +│ │ // Add the speed_test_id to link result to config │ +│ │ final resultToSave = result.copyWith(speedTestId: config.id); │ +│ │ │ +│ │ ref.read(speedTestResultsNotifierProvider.notifier) │ +│ │ .createResult(resultToSave); │ +│ ▼ │ +│ SpeedTestResultsNotifier │ +│ │ │ +│ │ repository.createSpeedTestResult(result) │ +│ ▼ │ +│ SpeedTestRepositoryImpl │ +│ │ │ +│ │ dataSource.createSpeedTestResult(result) │ +│ ▼ │ +│ SpeedTestWebSocketDataSource │ +│ │ │ +│ │ webSocketService.requestActionCable( │ +│ │ action: 'create', │ +│ │ resourceType: 'speed_test_results', │ +│ │ additionalData: result.toJson() │ +│ │ ) │ +│ ▼ │ +│ Rails Server │ +│ │ │ +│ │ Creates record in speed_test_results table │ +│ │ Returns: { data: { id: 456, speed_test_id: 123, ... } } │ +│ ▼ │ +│ SpeedTestResult (with server-assigned id) │ +└─────────���───────────────────────────────────────────────────────────────────┘ +``` + +--- + +### Fetching Speed Test Configurations + +Configurations define HOW a speed test should be run and what thresholds determine pass/fail. + +#### Provider Usage + +```dart +// In your widget +class SpeedTestScreen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + // Watch all configurations + final configsAsync = ref.watch(speedTestConfigsNotifierProvider); + + return configsAsync.when( + loading: () => CircularProgressIndicator(), + error: (e, _) => Text('Error: $e'), + data: (configs) { + return ListView.builder( + itemCount: configs.length, + itemBuilder: (context, index) { + final config = configs[index]; + return ListTile( + title: Text(config.name ?? 'Unnamed Test'), + subtitle: Text('Target: ${config.target}'), + trailing: Text(config.passing ? '✓ Pass' : '✗ Fail'), + onTap: () => _runTest(ref, config), + ); + }, + ); + }, + ); + } +} +``` + +#### Data Source Implementation + +```dart +// In SpeedTestWebSocketDataSource + +@override +Future> getSpeedTestConfigs() async { + if (!_webSocketService.isConnected) { + return []; + } + + final response = await _webSocketService.requestActionCable( + action: 'index', + resourceType: 'speed_tests', // Maps to SpeedTest model in Rails + ); + + final data = response.payload['data']; + if (data is List) { + return data + .map((json) => SpeedTestConfig.fromJson( + Map.from(json as Map), + )) + .toList(); + } + + return []; +} +``` + +#### SpeedTestConfig Entity + +```dart +@freezed +class SpeedTestConfig with _$SpeedTestConfig { + const factory SpeedTestConfig({ + int? id, + String? name, // "Office WiFi Test" + @JsonKey(name: 'test_type') String? testType, // "iperf3" + String? target, // "192.168.1.1" - server to test against + int? port, // 5201 + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, // "udp" or "tcp" + @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, // 50.0 + @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, // 25.0 + int? period, // 60 (run every 60...) + @JsonKey(name: 'period_unit') String? periodUnit, // "minutes" + @JsonKey(name: 'starts_at') DateTime? startsAt, + @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, + @Default(false) bool passing, // Current pass/fail status + // ... more fields + }) = _SpeedTestConfig; +} +``` + +--- + +### Submitting Speed Test Results + +After running a test, the result must be submitted to the server with the correct `speed_test_id`. + +#### Step-by-Step Process + +```dart +// 1. User selects a config and runs test +Future _runTest(WidgetRef ref, SpeedTestConfig config) async { + final speedTestService = SpeedTestService(); + + // 2. Run the test with the config's target server + await speedTestService.runSpeedTestWithFallback( + configTarget: config.target, // Use config's target as fallback server + ); +} + +// 3. Listen to results and submit +void _setupResultListener(WidgetRef ref, SpeedTestConfig config) { + final speedTestService = SpeedTestService(); + + speedTestService.resultStream.listen((result) { + // Only submit final results (not live updates) + if (result.hasError) { + // Handle error - don't submit + return; + } + + // 4. Add the speed_test_id to link this result to the config + final resultToSubmit = SpeedTestResult( + speedTestId: config.id, // CRITICAL: Links result to config + downloadMbps: result.downloadMbps, + uploadMbps: result.uploadMbps, + rtt: result.rtt, + jitter: result.jitter, + passed: _checkIfPassed(result, config), // Determine pass/fail + completedAt: DateTime.now(), + localIpAddress: result.localIpAddress, + serverHost: result.serverHost, + ); + + // 5. Submit to server via provider + ref.read(speedTestResultsNotifierProvider.notifier) + .createResult(resultToSubmit); + }); +} + +// Helper to determine pass/fail based on config thresholds +bool _checkIfPassed(SpeedTestResult result, SpeedTestConfig config) { + final downloadOk = config.minDownloadMbps == null || + (result.downloadMbps ?? 0) >= config.minDownloadMbps!; + + final uploadOk = config.minUploadMbps == null || + (result.uploadMbps ?? 0) >= config.minUploadMbps!; + + return downloadOk && uploadOk; +} +``` + +#### Data Source Create Implementation + +```dart +// In SpeedTestWebSocketDataSource + +@override +Future createSpeedTestResult(SpeedTestResult result) async { + if (!_webSocketService.isConnected) { + throw StateError('WebSocket not connected'); + } + + final response = await _webSocketService.requestActionCable( + action: 'create', + resourceType: 'speed_test_results', + additionalData: result.toJson(), // Includes speed_test_id + ); + + final data = response.payload['data']; + if (data != null) { + // Use validation to fix any swapped speeds in response + return SpeedTestResult.fromJsonWithValidation( + Map.from(data as Map), + ); + } + + throw Exception( + response.payload['error']?.toString() ?? 'Failed to create result', + ); +} +``` + +#### JSON Payload Sent to Server + +```json +{ + "command": "message", + "identifier": "{\"channel\":\"ResourceChannel\"}", + "data": "{ + \"action\": \"create\", + \"resource_type\": \"speed_test_results\", + \"speed_test_id\": 123, + \"download_mbps\": 95.5, + \"upload_mbps\": 42.3, + \"rtt\": 12.5, + \"jitter\": 2.1, + \"passed\": true, + \"completed_at\": \"2024-01-15T10:30:00.000Z\", + \"local_ip_address\": \"192.168.1.100\", + \"server_host\": \"192.168.1.1\" + }" +} +``` + +--- + +### Getting Configs with Their Results (Joined) + +To display a config along with its test history, use the joined entity: + +```dart +// Get a single config with all its results +final configWithResults = ref.watch( + speedTestWithResultsNotifierProvider(configId), +); + +configWithResults.when( + data: (joined) { + print('Config: ${joined.config.name}'); + print('Total Results: ${joined.resultCount}'); + print('Pass Rate: ${joined.passRate}%'); + print('Latest Speed: ${joined.latestResult?.downloadMbps} Mbps'); + print('Currently Passing: ${joined.isCurrentlyPassing}'); + print('Meets Download Req: ${joined.meetsDownloadRequirement}'); + }, + loading: () => ..., + error: (e, _) => ..., +); + +// Or get ALL configs with their results +final allTests = ref.watch(allSpeedTestsWithResultsNotifierProvider); + +allTests.when( + data: (tests) { + for (final test in tests) { + print('${test.config.name}: ${test.passRate}% pass rate'); + } + }, + // ... +); +``` + +#### Repository Implementation + +```dart +// In SpeedTestRepositoryImpl + +@override +Future> getSpeedTestWithResults( + int configId, +) async { + try { + // 1. Fetch the config + final config = await _dataSource.getSpeedTestConfig(configId); + + // 2. Fetch results WHERE speed_test_id = configId + final results = await _dataSource.getSpeedTestResults( + speedTestId: configId, + ); + + // 3. Join them into a single entity + return Right(SpeedTestWithResults( + config: config, + results: results, + )); + } catch (e) { + return Left(ServerFailure('Failed to load speed test: $e')); + } +} + +@override +Future>> getAllSpeedTestsWithResults() async { + try { + // 1. Fetch all configs + final configs = await _dataSource.getSpeedTestConfigs(); + + // 2. Fetch ALL results + final allResults = await _dataSource.getSpeedTestResults(); + + // 3. Group results by speed_test_id + final resultsByConfigId = >{}; + for (final result in allResults) { + if (result.speedTestId != null) { + resultsByConfigId + .putIfAbsent(result.speedTestId!, () => []) + .add(result); + } + } + + // 4. Join each config with its results + final joined = configs.map((config) { + return SpeedTestWithResults( + config: config, + results: resultsByConfigId[config.id] ?? [], + ); + }).toList(); + + return Right(joined); + } catch (e) { + return Left(ServerFailure('Failed to load speed tests: $e')); + } +} +``` + +--- + +### Complete Usage Example + +```dart +class SpeedTestWidget extends ConsumerStatefulWidget { + final SpeedTestConfig config; + + @override + ConsumerState createState() => _SpeedTestWidgetState(); +} + +class _SpeedTestWidgetState extends ConsumerState { + final _speedTestService = SpeedTestService(); + StreamSubscription? _resultSubscription; + + @override + void initState() { + super.initState(); + _setupResultListener(); + } + + void _setupResultListener() { + _resultSubscription = _speedTestService.resultStream.listen((result) { + // Check if this is a final result (not live update) + if (!result.hasError && _speedTestService.status == SpeedTestStatus.completed) { + _submitResult(result); + } + }); + } + + Future _submitResult(SpeedTestResult result) async { + // Create result with speed_test_id linking to config + final resultToSubmit = result.copyWith( + speedTestId: widget.config.id, + passed: _checkPassed(result), + ); + + // Submit via Riverpod + final saved = await ref + .read(speedTestResultsNotifierProvider.notifier) + .createResult(resultToSubmit); + + if (saved != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Result saved! ID: ${saved.id}')), + ); + + // Refresh the joined data + ref.invalidate(speedTestWithResultsNotifierProvider(widget.config.id!)); + } + } + + bool _checkPassed(SpeedTestResult result) { + final minDown = widget.config.minDownloadMbps ?? 0; + final minUp = widget.config.minUploadMbps ?? 0; + + return (result.downloadMbps ?? 0) >= minDown && + (result.uploadMbps ?? 0) >= minUp; + } + + Future _runTest() async { + await _speedTestService.runSpeedTestWithFallback( + configTarget: widget.config.target, + ); + } + + @override + void dispose() { + _resultSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _speedTestService.statusStream, + builder: (context, snapshot) { + final status = snapshot.data ?? SpeedTestStatus.idle; + + return Column( + children: [ + Text('Config: ${widget.config.name}'), + Text('Min Download: ${widget.config.minDownloadMbps} Mbps'), + Text('Min Upload: ${widget.config.minUploadMbps} Mbps'), + + ElevatedButton( + onPressed: status == SpeedTestStatus.running ? null : _runTest, + child: Text(status == SpeedTestStatus.running + ? 'Testing...' + : 'Run Test'), + ), + + // Show live results + StreamBuilder( + stream: _speedTestService.resultStream, + builder: (context, resultSnapshot) { + final result = resultSnapshot.data; + if (result == null) return SizedBox.shrink(); + + return Column( + children: [ + Text('Download: ${result.formattedDownloadSpeed}'), + Text('Upload: ${result.formattedUploadSpeed}'), + Text('Latency: ${result.formattedRtt}'), + ], + ); + }, + ), + ], + ); + }, + ); + } +} +``` + +--- + +### 3. NetworkGatewayService + +**File:** `lib/features/speed_test/data/services/network_gateway_service.dart` + +Handles network detection and gateway resolution. + +#### Gateway Detection + +**iOS:** +```dart +// Uses native getDefaultGateway() - reads system routing table +final gateway = await _iperf3Service.getDefaultGateway(); +``` + +**Android:** +```dart +// Calculates from WiFi IP and subnet mask +final wifiIP = await _networkInfo.getWifiIP(); // e.g., 192.168.1.100 +final subnetMask = await _networkInfo.getWifiSubmask(); // e.g., 255.255.255.0 + +// Network = IP & Mask = 192.168.1.0 +// Gateway = Network + 1 = 192.168.1.1 +``` + +--- + +## Protocol Comparison + +### TCP vs UDP + +| Feature | TCP | UDP | +|---------|-----|-----| +| **Latency Metric** | RTT (Round Trip Time) | Jitter | +| **Bandwidth Limit** | Not used | Required (81 Mbps default) | +| **Packet Loss** | Retransmitted | Measured | +| **Accuracy** | Higher for wired | Better for WiFi | +| **Default** | No | Yes | + +### Why UDP is Default + +1. **WiFi Performance**: TCP retransmissions can mask real WiFi issues +2. **Accurate Throughput**: Measures actual channel capacity +3. **Latency Metrics**: Jitter is more relevant for WiFi quality +4. **Bandwidth Control**: Prevents network saturation + +--- + +## iPerf3 JSON Output Parsing + +### TCP Response Structure + +```json +{ + "end": { + "sum_sent": { + "bits_per_second": 94500000, + "bytes": 118125000 + }, + "sum_received": { + "bits_per_second": 94200000, + "bytes": 117750000 + }, + "streams": [{ + "sender": { + "mean_rtt": 12500 // microseconds + } + }] + } +} +``` + +### UDP Response Structure + +```json +{ + "end": { + "sum": { + "jitter_ms": 2.5, + "lost_packets": 3, + "packets": 10000, + "lost_percent": 0.03 + }, + "sum_received": { + "bits_per_second": 81000000, + "bytes": 101250000 + } + } +} +``` + +### Reverse Mode Logic + +```dart +// Download Test (reverse=true): Server → Client +// sum_received = what client received FROM server = DOWNLOAD speed + +// Upload Test (reverse=false): Client → Server +// sum_received = what server received FROM client = UPLOAD speed +``` + +--- + +## Data Persistence + +### SpeedTestResult Entity + +```dart +@freezed +class SpeedTestResult with _$SpeedTestResult { + const factory SpeedTestResult({ + int? id, + @JsonKey(name: 'speed_test_id') int? speedTestId, // FK to config + @JsonKey(name: 'download_mbps') double? downloadMbps, + @JsonKey(name: 'upload_mbps') double? uploadMbps, + double? rtt, + double? jitter, + @JsonKey(name: 'packet_loss') double? packetLoss, + @Default(false) bool passed, + @JsonKey(name: 'completed_at') DateTime? completedAt, + // ... additional fields + }) = _SpeedTestResult; +} +``` + +### Speed Swap Validation + +The system automatically detects and corrects swapped download/upload values: + +```dart +static SpeedTestResult fromJsonWithValidation(Map json) { + final processedJson = _preprocessJson(json); + return _$SpeedTestResultFromJson(processedJson); +} + +// Heuristics: +// 1. Download < 5 Mbps AND Upload > 50 Mbps → Swap +// 2. Upload > Download × 10 → Swap +``` + +--- + +## Server Fallback Strategy + +### Priority Order + +1. **Default Gateway** (e.g., 192.168.1.1) + - Fastest response time + - Tests local network performance + - Requires iPerf3 server on gateway + +2. **Test Configuration Target** + - From `SpeedTestConfig.target` + - Configured per deployment + - May be external server + +### Fallback Behavior + +``` +Attempt 1: Default Gateway (192.168.1.1) + │ + ├── Success → Return Result + │ + └── Fail → "Trying test configuration..." + │ + ▼ +Attempt 2: Config Target (speedtest.example.com) + │ + ├── Success → Return Result + │ + └── Fail → "Unable to connect to server" +``` + +--- + +## Usage Examples + +### Basic Speed Test + +```dart +final speedTestService = SpeedTestService(); +await speedTestService.initialize(); + +// Listen to results +speedTestService.resultStream.listen((result) { + print('Download: ${result.downloadMbps} Mbps'); + print('Upload: ${result.uploadMbps} Mbps'); + print('Latency: ${result.rtt} ms'); +}); + +// Run test with automatic fallback +await speedTestService.runSpeedTestWithFallback(); +``` + +### With Riverpod Providers + +```dart +// Watch all configs with results +final speedTests = ref.watch(allSpeedTestsWithResultsNotifierProvider); + +speedTests.when( + data: (tests) { + for (final test in tests) { + print('Config: ${test.config.name}'); + print('Latest: ${test.latestResult?.downloadMbps} Mbps'); + print('Pass Rate: ${test.passRate}%'); + } + }, + loading: () => CircularProgressIndicator(), + error: (e, _) => Text('Error: $e'), +); +``` + +### Save Result to Server + +```dart +final result = SpeedTestResult( + speedTestId: config.id, + downloadMbps: 95.5, + uploadMbps: 42.3, + rtt: 12.5, + passed: true, + completedAt: DateTime.now(), +); + +await ref.read(speedTestResultsNotifierProvider.notifier).createResult(result); +``` + +--- + +## Error Handling + +### Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Connection refused" | No iPerf3 server | Check server is running | +| "Network unreachable" | No network | Check WiFi/cellular | +| "Timeout" | Server not responding | Try different server | +| "Busy" | Server in use | Wait and retry | + +### Error Result Factory + +```dart +factory SpeedTestResult.error(String message) { + return SpeedTestResult( + hasError: true, + errorMessage: message, + passed: false, + ); +} +``` + +--- + +## Native Implementation Notes + +### iOS + +- **Library**: `libiperf.a` (static library) +- **Integration**: Linked via Xcode project +- **Gateway Detection**: Uses system routing table (`SCNetworkReachability`) +- **Permissions**: None required for speed test + +### Android + +- **Library**: `libiperf3.so` (shared library) +- **ABIs**: arm64-v8a, armeabi-v7a, x86_64 +- **Gateway Detection**: Calculated from `WifiManager` +- **Permissions**: `ACCESS_WIFI_STATE`, `ACCESS_NETWORK_STATE` + +--- + +## Performance Considerations + +### Bandwidth Limiting + +UDP tests use 81 Mbps bandwidth limit to: +- Prevent network saturation +- Allow accurate measurements +- Avoid overwhelming routers + +### Parallel Streams + +16 parallel streams provide: +- Better throughput measurement +- Reduced impact of individual packet delays +- More accurate WiFi performance data + +### Test Duration + +10 seconds per phase (download/upload): +- Long enough for stable measurements +- Short enough for good UX +- Allows TCP slow-start to complete + +--- + +## Troubleshooting + +### Test Always Fails + +1. Check iPerf3 server is running: `iperf3 -s` +2. Verify port 5201 is open +3. Check firewall rules +4. Try TCP instead of UDP + +### Inconsistent Results + +1. Ensure stable WiFi connection +2. Check for network congestion +3. Try different bandwidth limit +4. Use fewer parallel streams + +### Gateway Detection Fails + +**iOS**: Should work automatically +**Android**: Check WiFi permissions are granted + +--- + +## Related Documentation + +- [iPerf3 Official Documentation](https://iperf.fr/iperf-doc.php) +- [Flutter Platform Channels](https://docs.flutter.dev/development/platform-integration/platform-channels) +- [Freezed Package](https://pub.dev/packages/freezed) +- [Riverpod Package](https://riverpod.dev/) diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 0000000..e4687c9 Binary files /dev/null and b/flutter_01.png differ diff --git a/lib/core/config/environment.dart b/lib/core/config/environment.dart index 6dd0dc0..c0062e7 100644 --- a/lib/core/config/environment.dart +++ b/lib/core/config/environment.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - /// Environment configuration for different build flavors enum Environment { development, staging, production } @@ -11,15 +9,6 @@ class EnvironmentConfig { static void setEnvironment(Environment env) { _environment = env; - // Only log in debug mode to avoid memory issues - if (kDebugMode) { - debugPrint( - 'EnvironmentConfig: Environment set, isDevelopment=$isDevelopment, ' - 'isStaging=$isStaging, isProduction=$isProduction', - ); - debugPrint('EnvironmentConfig: WebSocket URL will be: $websocketBaseUrl'); - debugPrint('EnvironmentConfig: useSyntheticData=$useSyntheticData'); - } } static Environment get environment => _environment; diff --git a/lib/core/providers/websocket_providers.dart b/lib/core/providers/websocket_providers.dart index b3b39dc..8790888 100644 --- a/lib/core/providers/websocket_providers.dart +++ b/lib/core/providers/websocket_providers.dart @@ -5,6 +5,7 @@ import 'package:rgnets_fdk/core/config/environment.dart'; import 'package:rgnets_fdk/core/config/logger_config.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; +import 'package:rgnets_fdk/core/services/ap_uplink_service.dart'; import 'package:rgnets_fdk/core/services/cache_manager.dart'; import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; import 'package:rgnets_fdk/core/services/websocket_data_sync_service.dart'; @@ -162,6 +163,15 @@ final webSocketCacheIntegrationProvider = Provider(( return integration; }); +/// Provides AP uplink info via cached lookups (fetching as needed). +final apUplinkInfoProvider = FutureProvider.family(( + ref, + apId, +) { + final cacheIntegration = ref.watch(webSocketCacheIntegrationProvider); + return cacheIntegration.getAPUplinkInfo(apId); +}); + /// Emits the last device-cache update time for WebSocket snapshots/updates. final webSocketDeviceLastUpdateProvider = StreamProvider((ref) { final integration = ref.watch(webSocketCacheIntegrationProvider); diff --git a/lib/core/services/ap_uplink_service.dart b/lib/core/services/ap_uplink_service.dart new file mode 100644 index 0000000..4f61d29 --- /dev/null +++ b/lib/core/services/ap_uplink_service.dart @@ -0,0 +1,154 @@ +import 'package:logger/logger.dart'; +import 'package:rgnets_fdk/core/services/websocket_service.dart'; + +class APUplinkInfo { + const APUplinkInfo({ + required this.apId, + this.linkSpeed, + this.speedInBps, + this.portName, + this.portNumber, + this.rawPortData, + }); + + final int apId; + final int? linkSpeed; + final int? speedInBps; + final String? portName; + final int? portNumber; + final Map? rawPortData; +} + +class APUplinkService { + APUplinkService({ + required WebSocketService webSocketService, + Logger? logger, + Map? cache, + }) : _webSocketService = webSocketService, + _logger = logger ?? Logger(), + _cache = cache ?? {}; + + final WebSocketService _webSocketService; + final Logger _logger; + final Map _cache; + final Map> _inFlight = {}; + + Map get cache => _cache; + + APUplinkInfo? getCachedUplink(int apId) { + return _cache[apId]; + } + + Future getAPUplinkPortDetail(int apId) { + final cached = _cache[apId]; + if (cached != null) { + return Future.value(cached); + } + + final inflight = _inFlight[apId]; + if (inflight != null) { + return inflight; + } + + final request = fetchAPUplinkDetail(apId); + _inFlight[apId] = request; + return request.whenComplete(() => _inFlight.remove(apId)); + } + + Future fetchAPUplinkDetail(int apId) async { + if (!_webSocketService.isConnected) { + _logger.w( + 'APUplinkService: WebSocket disconnected, cannot fetch uplink for AP $apId', + ); + return null; + } + + try { + final apResponse = await _webSocketService.requestActionCable( + action: 'resource_action', + resourceType: 'access_points', + additionalData: {'crud_action': 'show', 'id': apId}, + timeout: const Duration(seconds: 15), + ); + + final infrastructureLinkId = _parseInt( + apResponse.payload['data']?['infrastructure_link_id'], + ); + if (infrastructureLinkId == null) { + _logger.i( + 'APUplinkService: No infrastructure_link_id found for AP $apId', + ); + return null; + } + + final linkResponse = await _webSocketService.requestActionCable( + action: 'resource_action', + resourceType: 'infrastructure_links', + additionalData: {'crud_action': 'show', 'id': infrastructureLinkId}, + timeout: const Duration(seconds: 15), + ); + + final switchPorts = linkResponse.payload['data']?['switch_ports']; + if (switchPorts is! List || switchPorts.isEmpty) { + _logger.i( + 'APUplinkService: No switch_ports found for infrastructure link $infrastructureLinkId', + ); + return null; + } + + final firstPort = switchPorts.first; + final portId = firstPort is Map + ? _parseInt(firstPort['id']) + : _parseInt(firstPort); + if (portId == null) { + _logger.w( + 'APUplinkService: Could not determine switch port id for AP $apId', + ); + return null; + } + + final portResponse = await _webSocketService.requestActionCable( + action: 'resource_action', + resourceType: 'switch_ports', + additionalData: {'crud_action': 'show', 'id': portId}, + timeout: const Duration(seconds: 15), + ); + + final portData = portResponse.payload['data']; + if (portData is! Map) { + _logger.w( + 'APUplinkService: Invalid switch_port payload for port $portId', + ); + return null; + } + + final info = APUplinkInfo( + apId: apId, + linkSpeed: _parseInt(portData['link_speed']), + speedInBps: _parseInt(portData['speed_in_bps']), + portName: portData['name']?.toString(), + portNumber: _parseInt(portData['port']), + rawPortData: portData, + ); + + _cache[apId] = info; + return info; + } catch (e) { + _logger.e('APUplinkService: Failed to fetch uplink for AP $apId: $e'); + return null; + } + } + + void clearCache() { + _cache.clear(); + _inFlight.clear(); + } + + int? _parseInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is String) return int.tryParse(value); + return int.tryParse(value.toString()); + } +} diff --git a/lib/core/services/device_cache_migration_service.dart b/lib/core/services/device_cache_migration_service.dart new file mode 100644 index 0000000..86d1e6f --- /dev/null +++ b/lib/core/services/device_cache_migration_service.dart @@ -0,0 +1,254 @@ +import 'dart:convert'; + +import 'package:rgnets_fdk/core/config/logger_config.dart'; +import 'package:rgnets_fdk/core/services/storage_service.dart'; +import 'package:rgnets_fdk/features/devices/data/datasources/typed_device_local_data_source.dart'; +import 'package:rgnets_fdk/features/devices/data/models/device_model_sealed.dart'; + +/// One-time migration service to convert old unified device cache +/// to new type-specific caches. +/// +/// Old format: +/// - `device_index` → List of device IDs +/// - `cached_device_{id}` → Individual device JSON +/// +/// New format: +/// - `cached_ap_devices` → JSON array of all APs +/// - `cached_ont_devices` → JSON array of all ONTs +/// - `cached_switch_devices` → JSON array of all Switches +/// - `cached_wlan_devices` → JSON array of all WLANs +class DeviceCacheMigrationService { + DeviceCacheMigrationService({ + required this.storageService, + required this.apDataSource, + required this.ontDataSource, + required this.switchDataSource, + required this.wlanDataSource, + }); + + final StorageService storageService; + final APLocalDataSource apDataSource; + final ONTLocalDataSource ontDataSource; + final SwitchLocalDataSource switchDataSource; + final WLANLocalDataSource wlanDataSource; + final _logger = LoggerConfig.getLogger(); + + // Old cache keys + static const String _oldIndexKey = 'device_index'; + static const String _oldDeviceKeyPrefix = 'cached_device_'; + static const String _oldTimestampKey = 'devices_cache_timestamp'; + static const String _migrationCompleteKey = 'device_cache_migration_v2_complete'; + + /// Check if migration is needed + Future needsMigration() async { + // Already migrated + final migrated = storageService.getString(_migrationCompleteKey); + if (migrated == 'true') { + return false; + } + + // Check if old cache exists + final oldIndex = storageService.getString(_oldIndexKey); + return oldIndex != null && oldIndex.isNotEmpty; + } + + /// Run the migration + Future migrate() async { + if (!await needsMigration()) { + return const MigrationResult( + success: true, + migrated: false, + message: 'Migration not needed', + ); + } + + _logger.i('Starting device cache migration...'); + + try { + // Load old device index + final indexJson = storageService.getString(_oldIndexKey); + if (indexJson == null) { + return const MigrationResult( + success: true, + migrated: false, + message: 'No old cache found', + ); + } + + final index = (json.decode(indexJson) as List).cast(); + _logger.d('Found ${index.length} devices in old cache'); + + // Collect devices by type + final apDevices = []; + final ontDevices = []; + final switchDevices = []; + final wlanDevices = []; + final failedIds = []; + + for (final id in index) { + final deviceJson = storageService.getString('$_oldDeviceKeyPrefix$id'); + if (deviceJson == null) { + failedIds.add(id); + continue; + } + + try { + final data = json.decode(deviceJson) as Map; + final deviceType = _determineDeviceType(data); + + switch (deviceType) { + case DeviceModelSealed.typeAccessPoint: + apDevices.add(_parseAsAP(data)); + case DeviceModelSealed.typeONT: + ontDevices.add(_parseAsONT(data)); + case DeviceModelSealed.typeSwitch: + switchDevices.add(_parseAsSwitch(data)); + case DeviceModelSealed.typeWLAN: + wlanDevices.add(_parseAsWLAN(data)); + default: + _logger.w('Unknown device type for id $id: $deviceType'); + failedIds.add(id); + } + } on Exception catch (e) { + _logger.w('Failed to parse device $id: $e'); + failedIds.add(id); + } + } + + // Cache to new typed data sources + if (apDevices.isNotEmpty) { + await apDataSource.cacheDevices(apDevices); + await apDataSource.flushNow(); + } + if (ontDevices.isNotEmpty) { + await ontDataSource.cacheDevices(ontDevices); + await ontDataSource.flushNow(); + } + if (switchDevices.isNotEmpty) { + await switchDataSource.cacheDevices(switchDevices); + await switchDataSource.flushNow(); + } + if (wlanDevices.isNotEmpty) { + await wlanDataSource.cacheDevices(wlanDevices); + await wlanDataSource.flushNow(); + } + + // Clean up old cache + await _cleanupOldCache(index); + + // Mark migration complete + await storageService.setString(_migrationCompleteKey, 'true'); + + final total = apDevices.length + ontDevices.length + + switchDevices.length + wlanDevices.length; + + _logger.i('Migration complete: $total devices migrated ' + '(AP: ${apDevices.length}, ONT: ${ontDevices.length}, ' + 'Switch: ${switchDevices.length}, WLAN: ${wlanDevices.length})'); + + return MigrationResult( + success: true, + migrated: true, + message: 'Migrated $total devices', + apCount: apDevices.length, + ontCount: ontDevices.length, + switchCount: switchDevices.length, + wlanCount: wlanDevices.length, + failedCount: failedIds.length, + ); + } on Exception catch (e, stack) { + _logger.e('Migration failed: $e', error: e, stackTrace: stack); + return MigrationResult( + success: false, + migrated: false, + message: 'Migration failed: $e', + ); + } + } + + /// Determine device type from old cache data + String? _determineDeviceType(Map data) { + // Check explicit type field + final type = data['type']?.toString(); + if (type != null && DeviceModelSealed.allTypes.contains(type)) { + return type; + } + + // Check device_type field (used by Freezed) + final deviceType = data['device_type']?.toString(); + if (deviceType != null && DeviceModelSealed.allTypes.contains(deviceType)) { + return deviceType; + } + + return null; + } + + APModel _parseAsAP(Map data) { + // Ensure device_type is set for Freezed + final normalized = Map.from(data); + normalized['device_type'] = DeviceModelSealed.typeAccessPoint; + return APModel.fromJson(normalized); + } + + ONTModel _parseAsONT(Map data) { + final normalized = Map.from(data); + normalized['device_type'] = DeviceModelSealed.typeONT; + return ONTModel.fromJson(normalized); + } + + SwitchModel _parseAsSwitch(Map data) { + final normalized = Map.from(data); + normalized['device_type'] = DeviceModelSealed.typeSwitch; + return SwitchModel.fromJson(normalized); + } + + WLANModel _parseAsWLAN(Map data) { + final normalized = Map.from(data); + normalized['device_type'] = DeviceModelSealed.typeWLAN; + return WLANModel.fromJson(normalized); + } + + /// Clean up old cache keys + Future _cleanupOldCache(List deviceIds) async { + // Remove old index + await storageService.remove(_oldIndexKey); + await storageService.remove(_oldTimestampKey); + + // Remove individual device entries + for (final id in deviceIds) { + await storageService.remove('$_oldDeviceKeyPrefix$id'); + } + + _logger.d('Cleaned up ${deviceIds.length + 2} old cache keys'); + } + + /// Reset migration state (for testing) + Future resetMigration() async { + await storageService.remove(_migrationCompleteKey); + } +} + +/// Result of a migration attempt +class MigrationResult { + const MigrationResult({ + required this.success, + required this.migrated, + required this.message, + this.apCount = 0, + this.ontCount = 0, + this.switchCount = 0, + this.wlanCount = 0, + this.failedCount = 0, + }); + + final bool success; + final bool migrated; + final String message; + final int apCount; + final int ontCount; + final int switchCount; + final int wlanCount; + final int failedCount; + + int get totalMigrated => apCount + ontCount + switchCount + wlanCount; +} diff --git a/lib/core/services/websocket_cache_integration.dart b/lib/core/services/websocket_cache_integration.dart index a127dec..0578216 100644 --- a/lib/core/services/websocket_cache_integration.dart +++ b/lib/core/services/websocket_cache_integration.dart @@ -4,8 +4,12 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; +<<<<<<< HEAD import 'package:rgnets_fdk/core/constants/device_field_sets.dart'; import 'package:rgnets_fdk/core/services/device_update_event_bus.dart'; +======= +import 'package:rgnets_fdk/core/services/ap_uplink_service.dart'; +>>>>>>> 3bdf0aa (Uplink added) import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; import 'package:rgnets_fdk/core/utils/image_url_normalizer.dart'; @@ -30,13 +34,29 @@ class WebSocketCacheIntegration { DeviceUpdateEventBus? deviceUpdateEventBus, }) : _webSocketService = webSocketService, _imageBaseUrl = imageBaseUrl, +<<<<<<< HEAD _logger = logger ?? Logger(), _deviceUpdateEventBus = deviceUpdateEventBus; +======= + _logger = logger ?? Logger() { + _apUplinkCache = {}; + _apUplinkService = APUplinkService( + webSocketService: _webSocketService, + logger: _logger, + cache: _apUplinkCache, + ); + } +>>>>>>> 3bdf0aa (Uplink added) final WebSocketService _webSocketService; final String? _imageBaseUrl; final Logger _logger; +<<<<<<< HEAD final DeviceUpdateEventBus? _deviceUpdateEventBus; +======= + late final Map _apUplinkCache; + late final APUplinkService _apUplinkService; +>>>>>>> 3bdf0aa (Uplink added) /// Device resource types to subscribe to. static const List _deviceResourceTypes = [ @@ -109,6 +129,470 @@ class WebSocketCacheIntegration { /// Check if we have cached device data. bool get hasDeviceCache => _deviceCache.values.any((list) => list.isNotEmpty); +<<<<<<< HEAD +======= + /// Check if we have cached speed test config data. + bool get hasSpeedTestConfigCache => _speedTestConfigCache.isNotEmpty; + + /// Check if we have cached speed test result data. + bool get hasSpeedTestResultCache => _speedTestResultCache.isNotEmpty; + + /// Get cached AP uplink info for a specific access point. + APUplinkInfo? getCachedAPUplink(int apId) { + return _apUplinkCache[apId]; + } + + /// Get AP uplink info, fetching and caching if needed. + Future getAPUplinkInfo(int apId) { + return _apUplinkService.getAPUplinkPortDetail(apId); + } + + /// Fetch AP uplink detail (3-step lookup) and update cache. + Future fetchAPUplinkDetail(int apId) { + return _apUplinkService.fetchAPUplinkDetail(apId); + } + + /// Register a callback for speed test config data updates. + void onSpeedTestConfigData(void Function(List) callback) { + _speedTestConfigCallbacks.add(callback); + } + + /// Register a callback for speed test result data updates. + void onSpeedTestResultData(void Function(List) callback) { + _speedTestResultCallbacks.add(callback); + } + + /// Get cached speed test configs as domain entities. + List getCachedSpeedTestConfigs() { + return _speedTestConfigCache.map((json) { + try { + return SpeedTestConfig.fromJson(json); + } catch (e) { + _logger.w('Failed to parse speed test config: $e'); + return null; + } + }).whereType().toList(); + } + + /// Get cached speed test results as domain entities. + List getCachedSpeedTestResults() { + var parseFailures = 0; + final results = _speedTestResultCache.map((json) { + try { + return SpeedTestResult.fromJsonWithValidation(json); + } catch (e) { + parseFailures++; + LoggerService.warning( + 'Failed to parse speed test result id=${json['id']}: $e', + tag: 'SpeedTestCache', + ); + return null; + } + }).whereType().toList(); + + if (parseFailures > 0) { + LoggerService.warning( + 'Parse failures: $parseFailures out of ${_speedTestResultCache.length} raw results', + tag: 'SpeedTestCache', + ); + } + + return results; + } + + /// Get cached speed test results for a specific access point. + List getSpeedTestResultsForAccessPointId(int accessPointId) { + // Debug: Log raw cache size + LoggerService.info( + 'Raw cache has ${_speedTestResultCache.length} items, looking for accessPointId=$accessPointId', + tag: 'SpeedTestCache', + ); + + // Log first few raw items to see tested_via_access_point_id values + for (var i = 0; i < _speedTestResultCache.length && i < 5; i++) { + final raw = _speedTestResultCache[i]; + // Check both the direct ID field and the nested association object + final directId = raw['tested_via_access_point_id']; + final nestedObj = raw['tested_via_access_point']; + final nestedId = nestedObj is Map ? nestedObj['id'] : null; + LoggerService.info( + 'RawCache[$i]: id=${raw['id']}, direct_id=$directId, nested_id=$nestedId', + tag: 'SpeedTestCache', + ); + } + + final results = getCachedSpeedTestResults(); + if (results.isEmpty) { + LoggerService.info( + 'Parsed results is empty', + tag: 'SpeedTestCache', + ); + return []; + } + + LoggerService.info( + 'Parsed ${results.length} results from cache', + tag: 'SpeedTestCache', + ); + + // Log parsed results with their testedViaAccessPointId values + final apIdSet = results + .map((r) => r.testedViaAccessPointId) + .where((id) => id != null) + .toSet(); + LoggerService.info( + 'Unique testedViaAccessPointId values after parsing: $apIdSet', + tag: 'SpeedTestCache', + ); + + final filtered = results + .where((result) => result.testedViaAccessPointId == accessPointId) + .toList() + ..sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + LoggerService.info( + 'Found ${filtered.length} results for accessPointId=$accessPointId', + tag: 'SpeedTestCache', + ); + + return filtered; + } + + /// Get the adhoc speed test config (first config with "adhoc" in name, or first config if none match). + SpeedTestConfig? getAdhocSpeedTestConfig() { + final configs = getCachedSpeedTestConfigs(); + if (configs.isEmpty) return null; + + // Try to find a config with "adhoc" in the name + final adhocConfig = configs.where( + (c) => c.name?.toLowerCase().contains('adhoc') ?? false, + ).firstOrNull; + + // Return adhoc config if found, otherwise return first config + return adhocConfig ?? configs.first; + } + + /// Get a speed test config by its ID. + SpeedTestConfig? getSpeedTestConfigById(int? configId) { + if (configId == null) { + return null; + } + + final configs = getCachedSpeedTestConfigs(); + return configs.where((c) => c.id == configId).firstOrNull; + } + + /// Get cached speed test results for a specific device. + /// Filters results by tested_via_access_point_id (AP) or + /// tested_via_media_converter_id (ONT). + /// + /// [deviceId] can be prefixed ("ap_123") or unprefixed ("123"). + /// [deviceType] optional - if provided, used to determine which field to match. + /// Should be "access_point" or "ont" (matches DeviceTypes constants). + List getSpeedTestResultsForDevice( + String deviceId, { + String? deviceType, + }) { + final results = getCachedSpeedTestResults(); + if (results.isEmpty) return []; + + // Try to extract device type and numeric ID from prefixed deviceId (e.g., "ap_123") + String? extractedType; + int? numericId; + + final parts = deviceId.split('_'); + if (parts.length >= 2) { + // Prefixed format: "ap_123" or "ont_456" + extractedType = parts[0].toLowerCase(); + numericId = int.tryParse(parts.sublist(1).join('_')); + } else { + // Unprefixed format: just "123" + numericId = int.tryParse(deviceId); + } + + if (numericId == null) return []; + + // Determine the effective device type + // Priority: extracted from ID > passed deviceType parameter + String? effectiveType = extractedType; + if (effectiveType == null && deviceType != null) { + // Map DeviceTypes constants to our internal types + if (deviceType == 'access_point') { + effectiveType = 'ap'; + } else if (deviceType == 'ont') { + effectiveType = 'ont'; + } + } + + if (effectiveType == null) { + _logger.w( + 'getSpeedTestResultsForDevice: Cannot determine device type for $deviceId', + ); + return []; + } + + _logger.i( + 'getSpeedTestResultsForDevice: Searching for $effectiveType device with numericId=$numericId ' + 'in ${results.length} cached results (raw cache: ${_speedTestResultCache.length})', + ); + + // Log first few raw results to see what's actually in the cache + for (var i = 0; i < _speedTestResultCache.length && i < 3; i++) { + final raw = _speedTestResultCache[i]; + _logger.i( + 'RawResult[$i]: id=${raw['id']}, access_point_id=${raw['access_point_id']}, ' + 'tested_via_access_point_id=${raw['tested_via_access_point_id']}, ' + 'tested_via_media_converter_id=${raw['tested_via_media_converter_id']}', + ); + } + + // Log first few parsed results for debugging + for (var i = 0; i < results.length && i < 3; i++) { + final r = results[i]; + _logger.i( + 'ParsedResult[$i]: id=${r.id}, accessPointId=${r.accessPointId}, ' + 'testedViaAccessPointId=${r.testedViaAccessPointId}, ' + 'testedViaMediaConverterId=${r.testedViaMediaConverterId}', + ); + } + + // Filter results based on device type + return results.where((result) { + if (effectiveType == 'ap') { + // For access points, check tested_via_access_point_id only + return result.testedViaAccessPointId == numericId; + } else if (effectiveType == 'ont') { + // For ONTs (media converters), check tested_via_media_converter_id + return result.testedViaMediaConverterId == numericId; + } + return false; + }).toList() + // Sort by timestamp descending (most recent first) + ..sort((a, b) => b.timestamp.compareTo(a.timestamp)); + } + + /// Get the most recent speed test result for a specific device. + SpeedTestResult? getLatestSpeedTestResultForDevice( + String deviceId, { + String? deviceType, + }) { + final results = getSpeedTestResultsForDevice(deviceId, deviceType: deviceType); + return results.isNotEmpty ? results.first : null; + } + + /// Create an adhoc speed test result and send via WebSocket. + /// Returns true if successful, false otherwise. + /// + /// If [deviceId] is provided (format: "ap_123" or "ont_456"), the appropriate + /// device field (access_point_id or tested_via_media_converter_id) will be set. + Future createAdhocSpeedTestResult({ + required double downloadSpeed, + required double uploadSpeed, + required double latency, + String? source, + String? destination, + int? port, + String? protocol, + bool? passed, + String? deviceId, + }) async { + // Find the adhoc config to get its ID + final adhocConfig = getAdhocSpeedTestConfig(); + if (adhocConfig?.id == null) { + _logger.w('WebSocketCacheIntegration: No adhoc config found for result submission'); + return false; + } + + if (!_webSocketService.isConnected) { + _logger.w('WebSocketCacheIntegration: Cannot submit result - WebSocket not connected'); + return false; + } + + try { + _logger.i( + 'WebSocketCacheIntegration: Submitting adhoc speed test result - ' + 'configId=${adhocConfig!.id}, download=$downloadSpeed, upload=$uploadSpeed, deviceId=$deviceId', + ); + + // Parse deviceId to extract type and numeric ID for device association + int? accessPointId; + int? testedViaMediaConverterId; + if (deviceId != null) { + final parts = deviceId.split('_'); + if (parts.length >= 2) { + final deviceType = parts[0].toLowerCase(); + final numericId = int.tryParse(parts.sublist(1).join('_')); + if (numericId != null) { + if (deviceType == 'ap') { + accessPointId = numericId; + } else if (deviceType == 'ont') { + testedViaMediaConverterId = numericId; + } + } + } + } + + // Send CREATE request via ActionCable WebSocket + final response = await _webSocketService.requestActionCable( + action: 'create_resource', + resourceType: _speedTestResultResourceType, + additionalData: { + 'params': { + 'speed_test_id': adhocConfig.id, + 'download_mbps': downloadSpeed, + 'upload_mbps': uploadSpeed, + 'rtt': latency, + 'completed_at': DateTime.now().toIso8601String(), + 'test_type': 'iperf3', + if (source != null) 'source': source, + if (destination != null) 'destination': destination, + if (port != null) 'port': port, + if (protocol != null) 'iperf_protocol': protocol, + if (passed != null) 'passed': passed, + if (accessPointId != null) 'access_point_id': accessPointId, + if (testedViaMediaConverterId != null) 'tested_via_media_converter_id': testedViaMediaConverterId, + }, + }, + timeout: const Duration(seconds: 15), + ); + + final hasError = response.payload['error'] != null; + if (hasError) { + _logger.e( + 'WebSocketCacheIntegration: Failed to submit result - ${response.payload['error']}', + ); + return false; + } + + _logger.i('WebSocketCacheIntegration: Adhoc speed test result submitted successfully'); + return true; + } catch (e) { + _logger.e('WebSocketCacheIntegration: Error submitting adhoc result: $e'); + return false; + } + } + + /// Update an existing speed test result for a specific device. + /// Finds the existing result by device ID (AP or ONT) and updates it. + /// Returns true if successful, false otherwise. + /// + /// [deviceId] format: "ap_123" or "ont_456" + Future updateDeviceSpeedTestResult({ + required String deviceId, + required double downloadSpeed, + required double uploadSpeed, + required double latency, + String? source, + String? destination, + int? port, + String? protocol, + bool? passed, + }) async { + if (!_webSocketService.isConnected) { + _logger.w('WebSocketCacheIntegration: Cannot update result - WebSocket not connected'); + return false; + } + + // Find existing result for this device + final existingResult = getLatestSpeedTestResultForDevice(deviceId); + if (existingResult == null || existingResult.id == null) { + _logger.w( + 'WebSocketCacheIntegration: No existing speed test result found for device $deviceId', + ); + return false; + } + + try { + _logger.i( + 'WebSocketCacheIntegration: Updating speed test result ${existingResult.id} for device $deviceId - ' + 'download=$downloadSpeed, upload=$uploadSpeed', + ); + + // Send UPDATE request via ActionCable WebSocket + final response = await _webSocketService.requestActionCable( + action: 'update_resource', + resourceType: _speedTestResultResourceType, + additionalData: { + 'id': existingResult.id, + 'params': { + 'download_mbps': downloadSpeed, + 'upload_mbps': uploadSpeed, + 'rtt': latency, + 'completed_at': DateTime.now().toIso8601String(), + if (source != null) 'source': source, + if (destination != null) 'destination': destination, + if (port != null) 'port': port, + if (protocol != null) 'iperf_protocol': protocol, + if (passed != null) 'passed': passed, + }, + }, + timeout: const Duration(seconds: 15), + ); + + final hasError = response.payload['error'] != null; + if (hasError) { + _logger.e( + 'WebSocketCacheIntegration: Failed to update result - ${response.payload['error']}', + ); + return false; + } + + _logger.i( + 'WebSocketCacheIntegration: Speed test result ${existingResult.id} updated successfully', + ); + return true; + } catch (e) { + _logger.e('WebSocketCacheIntegration: Error updating device result: $e'); + return false; + } + } + + /// Get cached speed test configs as raw JSON maps. + List> getCachedSpeedTestConfigsRaw() { + return List.unmodifiable(_speedTestConfigCache); + } + + /// Get cached speed test results as raw JSON maps. + List> getCachedSpeedTestResultsRaw() { + return List.unmodifiable(_speedTestResultCache); + } + + /// Update a single speed test result in the cache. + /// This is useful after updating a result via the API to keep the cache in sync. + /// Merges new data with existing cache entry to preserve fields the server may not return. + void updateSpeedTestResultInCache(SpeedTestResult result) { + if (result.id == null) { + LoggerService.warning( + 'Cannot update speed test result in cache without id', + tag: 'SpeedTestCache', + ); + return; + } + + final newJson = result.toJson(); + final index = _speedTestResultCache.indexWhere((item) => item['id'] == result.id); + + if (index >= 0) { + // Merge new data with existing cache entry to preserve fields like pms_room_id + // that the server may not return in the update response + final existingJson = Map.from(_speedTestResultCache[index]) + ..addAll(newJson); + _speedTestResultCache[index] = existingJson; + LoggerService.info( + 'Updated speed test result ${result.id} in cache (merged with existing)', + tag: 'SpeedTestCache', + ); + } else { + _speedTestResultCache.add(newJson); + LoggerService.info( + 'Added speed test result ${result.id} to cache', + tag: 'SpeedTestCache', + ); + } + _bumpLastUpdate(); + } + +>>>>>>> 3bdf0aa (Uplink added) /// Get cached devices by resource type. List>? getCachedDevices(String resourceType) { return _deviceCache[resourceType]; @@ -193,12 +677,22 @@ class WebSocketCacheIntegration { imageSignedIds: apImageData?.signedIds, hnCounts: hnCounts, healthNotices: healthNotices, +<<<<<<< HEAD metadata: deviceMap, onboardingStatus: deviceMap['ap_onboarding_status'] != null ? OnboardingStatusPayload.fromJson( deviceMap['ap_onboarding_status'] as Map, ) : null, +======= + infrastructureLinkId: _parseOptionalInt( + deviceMap['infrastructure_link_id'], + ), +<<<<<<< HEAD +>>>>>>> 3bdf0aa (Uplink added) +======= + metadata: deviceMap, +>>>>>>> 81c4e9a (Add deployment phase filtering (#13)) ); case 'media_converters': @@ -218,11 +712,14 @@ class WebSocketCacheIntegration { hnCounts: hnCounts, healthNotices: healthNotices, metadata: deviceMap, +<<<<<<< HEAD onboardingStatus: deviceMap['ont_onboarding_status'] != null ? OnboardingStatusPayload.fromJson( deviceMap['ont_onboarding_status'] as Map, ) : null, +======= +>>>>>>> 81c4e9a (Add deployment phase filtering (#13)) ); case 'switch_devices': @@ -305,8 +802,25 @@ class WebSocketCacheIntegration { return null; } +<<<<<<< HEAD /// Extract images with both URLs and signed IDs. ImageExtraction? _extractImagesData(Map deviceMap) { +======= + int? _parseOptionalInt(dynamic value) { + if (value == null) { + return null; + } + if (value is int) { + return value; + } + if (value is double) { + return value.toInt(); + } + return int.tryParse(value.toString()); + } + + List? _extractImages(Map deviceMap) { +>>>>>>> 3bdf0aa (Uplink added) final imagesValue = deviceMap['images'] ?? deviceMap['pictures']; return extractImagesWithSignedIds(imagesValue, baseUrl: _imageBaseUrl); } @@ -1082,6 +1596,14 @@ class WebSocketCacheIntegration { _deviceCache.clear(); _roomCache.clear(); +<<<<<<< HEAD +======= + // Clear speed test caches + _speedTestConfigCache.clear(); + _speedTestResultCache.clear(); + _apUplinkService.clearCache(); + +>>>>>>> 3bdf0aa (Uplink added) // Clear snapshot state for (final timer in _snapshotFlushTimers.values) { timer.cancel(); @@ -1118,6 +1640,12 @@ class WebSocketCacheIntegration { _roomDataCallbacks.clear(); _deviceCache.clear(); _roomCache.clear(); +<<<<<<< HEAD +======= + _speedTestConfigCache.clear(); + _speedTestResultCache.clear(); + _apUplinkService.clearCache(); +>>>>>>> 3bdf0aa (Uplink added) } /// Request a specific resource type snapshot manually. diff --git a/lib/core/services/websocket_data_sync_service.dart b/lib/core/services/websocket_data_sync_service.dart index 4b45572..5f41647 100644 --- a/lib/core/services/websocket_data_sync_service.dart +++ b/lib/core/services/websocket_data_sync_service.dart @@ -505,6 +505,9 @@ class WebSocketDataSyncService { note: data['note']?.toString(), images: _extractImages(data), metadata: data, + infrastructureLinkId: _parseOptionalInt( + data['infrastructure_link_id'], + ), connectionState: data['connection_state']?.toString(), signalStrength: data['signal_strength'] as int?, connectedClients: data['connected_clients'] as int?, @@ -680,6 +683,19 @@ class WebSocketDataSyncService { return null; } + int? _parseOptionalInt(dynamic value) { + if (value == null) { + return null; + } + if (value is int) { + return value; + } + if (value is double) { + return value.toInt(); + } + return int.tryParse(value.toString()); + } + List? _extractImages(Map deviceMap) { final imageKeys = [ 'images', @@ -770,6 +786,40 @@ class WebSocketDataSyncService { final id = entry['id']; if (id != null) { deviceIds.add('$prefix$id'); +<<<<<<< HEAD +======= + } + + final nested = entry['devices']; + if (nested is List) { + for (final device in nested) { + if (device is Map) { + final nestedId = device['id']; + if (nestedId != null) { + deviceIds.add('$prefix$nestedId'); + } + } + } + } + } + } + + void addSwitchPortDevices(List? list) { + if (list == null) { + return; + } + for (final entry in list) { + if (entry is! Map) { + continue; + } + final switchDevice = entry['switch_device']; + final switchDeviceId = switchDevice is Map + ? switchDevice['id'] + : entry['switch_device_id']; + final id = switchDeviceId ?? entry['id']; + if (id != null) { + deviceIds.add('sw_$id'); +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) } final nested = entry['devices']; @@ -786,6 +836,7 @@ class WebSocketDataSyncService { } } +<<<<<<< HEAD void addSwitchPortDevices(List? list) { if (list == null) { return; @@ -817,6 +868,8 @@ class WebSocketDataSyncService { } } +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) addDevices(roomData['access_points'] as List?, prefix: 'ap_'); addDevices(roomData['media_converters'] as List?, prefix: 'ont_'); final switchPorts = roomData['switch_ports']; diff --git a/lib/core/widgets/unified_list/unified_list_item.dart b/lib/core/widgets/unified_list/unified_list_item.dart index 5a54f67..a37bd25 100644 --- a/lib/core/widgets/unified_list/unified_list_item.dart +++ b/lib/core/widgets/unified_list/unified_list_item.dart @@ -11,6 +11,7 @@ class UnifiedListItem extends StatelessWidget { super.key, this.subtitleLines = const [], this.iconColorOverride, + this.titleColor, this.statusBadge, this.onTap, this.showChevron = false, @@ -23,6 +24,7 @@ class UnifiedListItem extends StatelessWidget { final UnifiedItemStatus status; final List subtitleLines; final Color? iconColorOverride; + final Color? titleColor; final UnifiedStatusBadge? statusBadge; final VoidCallback? onTap; final bool showChevron; @@ -66,7 +68,7 @@ class UnifiedListItem extends StatelessWidget { title, style: TextStyle( fontWeight: isUnread ? FontWeight.bold : FontWeight.w600, - color: Colors.white, + color: titleColor ?? AppColors.textPrimary, ), ), ), @@ -244,4 +246,4 @@ class UnifiedInfoLine { final IconData? icon; final Color? color; final int maxLines; -} \ No newline at end of file +} diff --git a/lib/features/devices/data/models/device_model_sealed.dart b/lib/features/devices/data/models/device_model_sealed.dart index 5a02c11..ef0aa45 100644 --- a/lib/features/devices/data/models/device_model_sealed.dart +++ b/lib/features/devices/data/models/device_model_sealed.dart @@ -1,7 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:rgnets_fdk/features/devices/data/models/room_model.dart'; import 'package:rgnets_fdk/features/devices/domain/entities/device.dart'; -import 'package:rgnets_fdk/features/onboarding/data/models/onboarding_status_payload.dart'; // Assuming this path +import 'package:rgnets_fdk/features/onboarding/data/models/onboarding_status_payload.dart'; import 'package:rgnets_fdk/features/issues/data/models/health_counts_model.dart'; import 'package:rgnets_fdk/features/issues/data/models/health_notice_model.dart'; @@ -17,10 +17,6 @@ part 'device_model_sealed.g.dart'; sealed class DeviceModelSealed with _$DeviceModelSealed { const DeviceModelSealed._(); - // ============================================================================ - // Device Type Constants - // ============================================================================ - /// Device type identifier for Access Points static const String typeAccessPoint = 'access_point'; @@ -70,13 +66,8 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { return deviceTypeToResourceType[deviceType]; } - // ============================================================================ - // Access Point Model - // ============================================================================ - @FreezedUnionValue('access_point') const factory DeviceModelSealed.ap({ - // Common fields required String id, required String name, required String status, @@ -95,8 +86,7 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, - - // AP-specific fields + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -108,13 +98,8 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { @JsonKey(name: 'ap_onboarding_status') OnboardingStatusPayload? onboardingStatus, }) = APModel; - // ============================================================================ - // ONT (Optical Network Terminal) Model - // ============================================================================ - @FreezedUnionValue('ont') const factory DeviceModelSealed.ont({ - // Common fields required String id, required String name, required String status, @@ -133,8 +118,6 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, - - // ONT-specific fields @JsonKey(name: 'is_registered') bool? isRegistered, @JsonKey(name: 'switch_port') Map? switchPort, @JsonKey(name: 'ont_onboarding_status') OnboardingStatusPayload? onboardingStatus, @@ -143,13 +126,8 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { String? phase, }) = ONTModel; - // ============================================================================ - // Switch Model - // ============================================================================ - @FreezedUnionValue('switch') const factory DeviceModelSealed.switchDevice({ - // Common fields required String id, required String name, required String status, @@ -168,8 +146,6 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, - - // Switch-specific fields String? host, @JsonKey(name: 'switch_ports') List>? ports, @JsonKey(name: 'last_config_sync_at') DateTime? lastConfigSync, @@ -179,13 +155,8 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { int? temperature, }) = SwitchModel; - // ============================================================================ - // WLAN Controller Model - // ============================================================================ - @FreezedUnionValue('wlan_controller') const factory DeviceModelSealed.wlan({ - // Common fields required String id, required String name, required String status, @@ -204,8 +175,6 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, - - // WLAN-specific fields @JsonKey(name: 'controller_type') String? controllerType, @JsonKey(name: 'managed_aps') int? managedAPs, int? vlan, @@ -220,10 +189,6 @@ sealed class DeviceModelSealed with _$DeviceModelSealed { _$DeviceModelSealedFromJson(json); } -// ============================================================================ -// Extension for converting to Domain Entity -// ============================================================================ - extension DeviceModelSealedX on DeviceModelSealed { /// Converts this model to the unified [Device] domain entity Device toEntity() { diff --git a/lib/features/devices/data/models/device_model_sealed.freezed.dart b/lib/features/devices/data/models/device_model_sealed.freezed.dart index aa1e983..5f63b4d 100644 --- a/lib/features/devices/data/models/device_model_sealed.freezed.dart +++ b/lib/features/devices/data/models/device_model_sealed.freezed.dart @@ -33,7 +33,6 @@ DeviceModelSealed _$DeviceModelSealedFromJson(Map json) { /// @nodoc mixin _$DeviceModelSealed { -// Common fields String get id => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get status => throw _privateConstructorUsedError; @@ -55,8 +54,6 @@ mixin _$DeviceModelSealed { String? get firmware => throw _privateConstructorUsedError; String? get note => throw _privateConstructorUsedError; List? get images => throw _privateConstructorUsedError; - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds => throw _privateConstructorUsedError; @JsonKey(name: 'health_notices') List? get healthNotices => throw _privateConstructorUsedError; @@ -80,10 +77,10 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -111,7 +108,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -139,7 +135,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -168,7 +163,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -201,10 +195,10 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -232,7 +226,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -260,7 +253,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -289,7 +281,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -322,10 +313,10 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -353,7 +344,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -381,7 +371,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -410,7 +399,6 @@ mixin _$DeviceModelSealed { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -479,7 +467,6 @@ abstract class $DeviceModelSealedCopyWith<$Res> { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts}); @@ -515,7 +502,6 @@ class _$DeviceModelSealedCopyWithImpl<$Res, $Val extends DeviceModelSealed> Object? firmware = freezed, Object? note = freezed, Object? images = freezed, - Object? imageSignedIds = freezed, Object? healthNotices = freezed, Object? hnCounts = freezed, }) { @@ -580,10 +566,6 @@ class _$DeviceModelSealedCopyWithImpl<$Res, $Val extends DeviceModelSealed> ? _value.images : images // ignore: cast_nullable_to_non_nullable as List?, - imageSignedIds: freezed == imageSignedIds - ? _value.imageSignedIds - : imageSignedIds // ignore: cast_nullable_to_non_nullable - as List?, healthNotices: freezed == healthNotices ? _value.healthNotices : healthNotices // ignore: cast_nullable_to_non_nullable @@ -644,9 +626,9 @@ abstract class _$$APModelImplCopyWith<$Res> String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -691,9 +673,9 @@ class __$$APModelImplCopyWithImpl<$Res> Object? firmware = freezed, Object? note = freezed, Object? images = freezed, - Object? imageSignedIds = freezed, Object? healthNotices = freezed, Object? hnCounts = freezed, + Object? infrastructureLinkId = freezed, Object? connectionState = freezed, Object? signalStrength = freezed, Object? connectedClients = freezed, @@ -765,10 +747,6 @@ class __$$APModelImplCopyWithImpl<$Res> ? _value._images : images // ignore: cast_nullable_to_non_nullable as List?, - imageSignedIds: freezed == imageSignedIds - ? _value._imageSignedIds - : imageSignedIds // ignore: cast_nullable_to_non_nullable - as List?, healthNotices: freezed == healthNotices ? _value._healthNotices : healthNotices // ignore: cast_nullable_to_non_nullable @@ -777,6 +755,10 @@ class __$$APModelImplCopyWithImpl<$Res> ? _value.hnCounts : hnCounts // ignore: cast_nullable_to_non_nullable as HealthCountsModel?, + infrastructureLinkId: freezed == infrastructureLinkId + ? _value.infrastructureLinkId + : infrastructureLinkId // ignore: cast_nullable_to_non_nullable + as int?, connectionState: freezed == connectionState ? _value.connectionState : connectionState // ignore: cast_nullable_to_non_nullable @@ -849,10 +831,10 @@ class _$APModelImpl extends APModel { this.firmware, this.note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') this.hnCounts, + @JsonKey(name: 'infrastructure_link_id') this.infrastructureLinkId, @JsonKey(name: 'connection_state') this.connectionState, @JsonKey(name: 'signal_strength') this.signalStrength, @JsonKey(name: 'connected_clients') this.connectedClients, @@ -865,7 +847,6 @@ class _$APModelImpl extends APModel { final String? $type}) : _metadata = metadata, _images = images, - _imageSignedIds = imageSignedIds, _healthNotices = healthNotices, $type = $type ?? 'access_point', super._(); @@ -873,7 +854,6 @@ class _$APModelImpl extends APModel { factory _$APModelImpl.fromJson(Map json) => _$$APModelImplFromJson(json); -// Common fields @override final String id; @override @@ -926,17 +906,6 @@ class _$APModelImpl extends APModel { return EqualUnmodifiableListView(value); } - final List? _imageSignedIds; - @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds { - final value = _imageSignedIds; - if (value == null) return null; - if (_imageSignedIds is EqualUnmodifiableListView) return _imageSignedIds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - final List? _healthNotices; @override @JsonKey(name: 'health_notices') @@ -951,7 +920,9 @@ class _$APModelImpl extends APModel { @override @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts; -// AP-specific fields + @override + @JsonKey(name: 'infrastructure_link_id') + final int? infrastructureLinkId; @override @JsonKey(name: 'connection_state') final String? connectionState; @@ -983,7 +954,7 @@ class _$APModelImpl extends APModel { @override String toString() { - return 'DeviceModelSealed.ap(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, imageSignedIds: $imageSignedIds, healthNotices: $healthNotices, hnCounts: $hnCounts, connectionState: $connectionState, signalStrength: $signalStrength, connectedClients: $connectedClients, ssid: $ssid, channel: $channel, maxClients: $maxClients, currentUpload: $currentUpload, currentDownload: $currentDownload, onboardingStatus: $onboardingStatus)'; + return 'DeviceModelSealed.ap(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, healthNotices: $healthNotices, hnCounts: $hnCounts, infrastructureLinkId: $infrastructureLinkId, connectionState: $connectionState, signalStrength: $signalStrength, connectedClients: $connectedClients, ssid: $ssid, channel: $channel, maxClients: $maxClients, currentUpload: $currentUpload, currentDownload: $currentDownload, onboardingStatus: $onboardingStatus)'; } @override @@ -1013,12 +984,12 @@ class _$APModelImpl extends APModel { other.firmware == firmware) && (identical(other.note, note) || other.note == note) && const DeepCollectionEquality().equals(other._images, _images) && - const DeepCollectionEquality() - .equals(other._imageSignedIds, _imageSignedIds) && const DeepCollectionEquality() .equals(other._healthNotices, _healthNotices) && (identical(other.hnCounts, hnCounts) || other.hnCounts == hnCounts) && + (identical(other.infrastructureLinkId, infrastructureLinkId) || + other.infrastructureLinkId == infrastructureLinkId) && (identical(other.connectionState, connectionState) || other.connectionState == connectionState) && (identical(other.signalStrength, signalStrength) || @@ -1056,9 +1027,9 @@ class _$APModelImpl extends APModel { firmware, note, const DeepCollectionEquality().hash(_images), - const DeepCollectionEquality().hash(_imageSignedIds), const DeepCollectionEquality().hash(_healthNotices), hnCounts, + infrastructureLinkId, connectionState, signalStrength, connectedClients, @@ -1095,10 +1066,10 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -1126,7 +1097,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1154,7 +1124,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1183,7 +1152,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1213,9 +1181,9 @@ class _$APModelImpl extends APModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, + infrastructureLinkId, connectionState, signalStrength, connectedClients, @@ -1246,10 +1214,10 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -1277,7 +1245,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1305,7 +1272,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1334,7 +1300,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1364,9 +1329,9 @@ class _$APModelImpl extends APModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, + infrastructureLinkId, connectionState, signalStrength, connectedClients, @@ -1397,10 +1362,10 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -1428,7 +1393,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1456,7 +1420,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1485,7 +1448,6 @@ class _$APModelImpl extends APModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -1517,9 +1479,9 @@ class _$APModelImpl extends APModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, + infrastructureLinkId, connectionState, signalStrength, connectedClients, @@ -1595,10 +1557,10 @@ abstract class APModel extends DeviceModelSealed { final String? firmware, final String? note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') final int? infrastructureLinkId, @JsonKey(name: 'connection_state') final String? connectionState, @JsonKey(name: 'signal_strength') final int? signalStrength, @JsonKey(name: 'connected_clients') final int? connectedClients, @@ -1613,7 +1575,7 @@ abstract class APModel extends DeviceModelSealed { factory APModel.fromJson(Map json) = _$APModelImpl.fromJson; - @override // Common fields + @override String get id; @override String get name; @@ -1650,14 +1612,13 @@ abstract class APModel extends DeviceModelSealed { @override List? get images; @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds; - @override @JsonKey(name: 'health_notices') List? get healthNotices; @override @JsonKey(name: 'hn_counts') - HealthCountsModel? get hnCounts; // AP-specific fields + HealthCountsModel? get hnCounts; + @JsonKey(name: 'infrastructure_link_id') + int? get infrastructureLinkId; @JsonKey(name: 'connection_state') String? get connectionState; @JsonKey(name: 'signal_strength') @@ -1704,7 +1665,6 @@ abstract class _$$ONTModelImplCopyWith<$Res> String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @JsonKey(name: 'is_registered') bool? isRegistered, @@ -1748,7 +1708,6 @@ class __$$ONTModelImplCopyWithImpl<$Res> Object? firmware = freezed, Object? note = freezed, Object? images = freezed, - Object? imageSignedIds = freezed, Object? healthNotices = freezed, Object? hnCounts = freezed, Object? isRegistered = freezed, @@ -1819,10 +1778,6 @@ class __$$ONTModelImplCopyWithImpl<$Res> ? _value._images : images // ignore: cast_nullable_to_non_nullable as List?, - imageSignedIds: freezed == imageSignedIds - ? _value._imageSignedIds - : imageSignedIds // ignore: cast_nullable_to_non_nullable - as List?, healthNotices: freezed == healthNotices ? _value._healthNotices : healthNotices // ignore: cast_nullable_to_non_nullable @@ -1891,7 +1846,6 @@ class _$ONTModelImpl extends ONTModel { this.firmware, this.note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') this.hnCounts, @@ -1904,7 +1858,6 @@ class _$ONTModelImpl extends ONTModel { final String? $type}) : _metadata = metadata, _images = images, - _imageSignedIds = imageSignedIds, _healthNotices = healthNotices, _switchPort = switchPort, _ports = ports, @@ -1914,7 +1867,6 @@ class _$ONTModelImpl extends ONTModel { factory _$ONTModelImpl.fromJson(Map json) => _$$ONTModelImplFromJson(json); -// Common fields @override final String id; @override @@ -1967,17 +1919,6 @@ class _$ONTModelImpl extends ONTModel { return EqualUnmodifiableListView(value); } - final List? _imageSignedIds; - @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds { - final value = _imageSignedIds; - if (value == null) return null; - if (_imageSignedIds is EqualUnmodifiableListView) return _imageSignedIds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - final List? _healthNotices; @override @JsonKey(name: 'health_notices') @@ -1992,7 +1933,6 @@ class _$ONTModelImpl extends ONTModel { @override @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts; -// ONT-specific fields @override @JsonKey(name: 'is_registered') final bool? isRegistered; @@ -2031,7 +1971,7 @@ class _$ONTModelImpl extends ONTModel { @override String toString() { - return 'DeviceModelSealed.ont(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, imageSignedIds: $imageSignedIds, healthNotices: $healthNotices, hnCounts: $hnCounts, isRegistered: $isRegistered, switchPort: $switchPort, onboardingStatus: $onboardingStatus, ports: $ports, uptime: $uptime, phase: $phase)'; + return 'DeviceModelSealed.ont(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, healthNotices: $healthNotices, hnCounts: $hnCounts, isRegistered: $isRegistered, switchPort: $switchPort, onboardingStatus: $onboardingStatus, ports: $ports, uptime: $uptime, phase: $phase)'; } @override @@ -2061,8 +2001,6 @@ class _$ONTModelImpl extends ONTModel { other.firmware == firmware) && (identical(other.note, note) || other.note == note) && const DeepCollectionEquality().equals(other._images, _images) && - const DeepCollectionEquality() - .equals(other._imageSignedIds, _imageSignedIds) && const DeepCollectionEquality() .equals(other._healthNotices, _healthNotices) && (identical(other.hnCounts, hnCounts) || @@ -2097,7 +2035,6 @@ class _$ONTModelImpl extends ONTModel { firmware, note, const DeepCollectionEquality().hash(_images), - const DeepCollectionEquality().hash(_imageSignedIds), const DeepCollectionEquality().hash(_healthNotices), hnCounts, isRegistered, @@ -2133,10 +2070,10 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -2164,7 +2101,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2192,7 +2128,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2221,7 +2156,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2251,7 +2185,6 @@ class _$ONTModelImpl extends ONTModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, isRegistered, @@ -2281,10 +2214,10 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -2312,7 +2245,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2340,7 +2272,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2369,7 +2300,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2399,7 +2329,6 @@ class _$ONTModelImpl extends ONTModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, isRegistered, @@ -2429,10 +2358,10 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -2460,7 +2389,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2488,7 +2416,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2517,7 +2444,6 @@ class _$ONTModelImpl extends ONTModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -2549,7 +2475,6 @@ class _$ONTModelImpl extends ONTModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, isRegistered, @@ -2624,7 +2549,6 @@ abstract class ONTModel extends DeviceModelSealed { final String? firmware, final String? note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts, @@ -2640,7 +2564,7 @@ abstract class ONTModel extends DeviceModelSealed { factory ONTModel.fromJson(Map json) = _$ONTModelImpl.fromJson; - @override // Common fields + @override String get id; @override String get name; @@ -2677,14 +2601,11 @@ abstract class ONTModel extends DeviceModelSealed { @override List? get images; @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds; - @override @JsonKey(name: 'health_notices') List? get healthNotices; @override @JsonKey(name: 'hn_counts') - HealthCountsModel? get hnCounts; // ONT-specific fields + HealthCountsModel? get hnCounts; @JsonKey(name: 'is_registered') bool? get isRegistered; @JsonKey(name: 'switch_port') @@ -2725,7 +2646,6 @@ abstract class _$$SwitchModelImplCopyWith<$Res> String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, String? host, @@ -2769,7 +2689,6 @@ class __$$SwitchModelImplCopyWithImpl<$Res> Object? firmware = freezed, Object? note = freezed, Object? images = freezed, - Object? imageSignedIds = freezed, Object? healthNotices = freezed, Object? hnCounts = freezed, Object? host = freezed, @@ -2841,10 +2760,6 @@ class __$$SwitchModelImplCopyWithImpl<$Res> ? _value._images : images // ignore: cast_nullable_to_non_nullable as List?, - imageSignedIds: freezed == imageSignedIds - ? _value._imageSignedIds - : imageSignedIds // ignore: cast_nullable_to_non_nullable - as List?, healthNotices: freezed == healthNotices ? _value._healthNotices : healthNotices // ignore: cast_nullable_to_non_nullable @@ -2904,7 +2819,6 @@ class _$SwitchModelImpl extends SwitchModel { this.firmware, this.note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') this.hnCounts, @@ -2918,7 +2832,6 @@ class _$SwitchModelImpl extends SwitchModel { final String? $type}) : _metadata = metadata, _images = images, - _imageSignedIds = imageSignedIds, _healthNotices = healthNotices, _ports = ports, $type = $type ?? 'switch', @@ -2927,7 +2840,6 @@ class _$SwitchModelImpl extends SwitchModel { factory _$SwitchModelImpl.fromJson(Map json) => _$$SwitchModelImplFromJson(json); -// Common fields @override final String id; @override @@ -2980,17 +2892,6 @@ class _$SwitchModelImpl extends SwitchModel { return EqualUnmodifiableListView(value); } - final List? _imageSignedIds; - @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds { - final value = _imageSignedIds; - if (value == null) return null; - if (_imageSignedIds is EqualUnmodifiableListView) return _imageSignedIds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - final List? _healthNotices; @override @JsonKey(name: 'health_notices') @@ -3005,7 +2906,6 @@ class _$SwitchModelImpl extends SwitchModel { @override @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts; -// Switch-specific fields @override final String? host; final List>? _ports; @@ -3039,7 +2939,7 @@ class _$SwitchModelImpl extends SwitchModel { @override String toString() { - return 'DeviceModelSealed.switchDevice(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, imageSignedIds: $imageSignedIds, healthNotices: $healthNotices, hnCounts: $hnCounts, host: $host, ports: $ports, lastConfigSync: $lastConfigSync, lastConfigSyncAttempt: $lastConfigSyncAttempt, cpuUsage: $cpuUsage, memoryUsage: $memoryUsage, temperature: $temperature)'; + return 'DeviceModelSealed.switchDevice(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, healthNotices: $healthNotices, hnCounts: $hnCounts, host: $host, ports: $ports, lastConfigSync: $lastConfigSync, lastConfigSyncAttempt: $lastConfigSyncAttempt, cpuUsage: $cpuUsage, memoryUsage: $memoryUsage, temperature: $temperature)'; } @override @@ -3069,8 +2969,6 @@ class _$SwitchModelImpl extends SwitchModel { other.firmware == firmware) && (identical(other.note, note) || other.note == note) && const DeepCollectionEquality().equals(other._images, _images) && - const DeepCollectionEquality() - .equals(other._imageSignedIds, _imageSignedIds) && const DeepCollectionEquality() .equals(other._healthNotices, _healthNotices) && (identical(other.hnCounts, hnCounts) || @@ -3108,7 +3006,6 @@ class _$SwitchModelImpl extends SwitchModel { firmware, note, const DeepCollectionEquality().hash(_images), - const DeepCollectionEquality().hash(_imageSignedIds), const DeepCollectionEquality().hash(_healthNotices), hnCounts, host, @@ -3145,10 +3042,10 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -3176,7 +3073,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3204,7 +3100,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3233,7 +3128,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3263,7 +3157,6 @@ class _$SwitchModelImpl extends SwitchModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, host, @@ -3294,10 +3187,10 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -3325,7 +3218,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3353,7 +3245,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3382,7 +3273,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3412,7 +3302,6 @@ class _$SwitchModelImpl extends SwitchModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, host, @@ -3443,10 +3332,10 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -3474,7 +3363,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3502,7 +3390,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3531,7 +3418,6 @@ class _$SwitchModelImpl extends SwitchModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -3563,7 +3449,6 @@ class _$SwitchModelImpl extends SwitchModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, host, @@ -3639,7 +3524,6 @@ abstract class SwitchModel extends DeviceModelSealed { final String? firmware, final String? note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts, @@ -3656,7 +3540,7 @@ abstract class SwitchModel extends DeviceModelSealed { factory SwitchModel.fromJson(Map json) = _$SwitchModelImpl.fromJson; - @override // Common fields + @override String get id; @override String get name; @@ -3693,14 +3577,11 @@ abstract class SwitchModel extends DeviceModelSealed { @override List? get images; @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds; - @override @JsonKey(name: 'health_notices') List? get healthNotices; @override @JsonKey(name: 'hn_counts') - HealthCountsModel? get hnCounts; // Switch-specific fields + HealthCountsModel? get hnCounts; String? get host; @JsonKey(name: 'switch_ports') List>? get ports; @@ -3743,7 +3624,6 @@ abstract class _$$WLANModelImplCopyWith<$Res> String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @JsonKey(name: 'controller_type') String? controllerType, @@ -3787,7 +3667,6 @@ class __$$WLANModelImplCopyWithImpl<$Res> Object? firmware = freezed, Object? note = freezed, Object? images = freezed, - Object? imageSignedIds = freezed, Object? healthNotices = freezed, Object? hnCounts = freezed, Object? controllerType = freezed, @@ -3860,10 +3739,6 @@ class __$$WLANModelImplCopyWithImpl<$Res> ? _value._images : images // ignore: cast_nullable_to_non_nullable as List?, - imageSignedIds: freezed == imageSignedIds - ? _value._imageSignedIds - : imageSignedIds // ignore: cast_nullable_to_non_nullable - as List?, healthNotices: freezed == healthNotices ? _value._healthNotices : healthNotices // ignore: cast_nullable_to_non_nullable @@ -3927,7 +3802,6 @@ class _$WLANModelImpl extends WLANModel { this.firmware, this.note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') this.hnCounts, @@ -3942,7 +3816,6 @@ class _$WLANModelImpl extends WLANModel { final String? $type}) : _metadata = metadata, _images = images, - _imageSignedIds = imageSignedIds, _healthNotices = healthNotices, $type = $type ?? 'wlan_controller', super._(); @@ -3950,7 +3823,6 @@ class _$WLANModelImpl extends WLANModel { factory _$WLANModelImpl.fromJson(Map json) => _$$WLANModelImplFromJson(json); -// Common fields @override final String id; @override @@ -4003,17 +3875,6 @@ class _$WLANModelImpl extends WLANModel { return EqualUnmodifiableListView(value); } - final List? _imageSignedIds; - @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds { - final value = _imageSignedIds; - if (value == null) return null; - if (_imageSignedIds is EqualUnmodifiableListView) return _imageSignedIds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - final List? _healthNotices; @override @JsonKey(name: 'health_notices') @@ -4028,7 +3889,6 @@ class _$WLANModelImpl extends WLANModel { @override @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts; -// WLAN-specific fields @override @JsonKey(name: 'controller_type') final String? controllerType; @@ -4057,7 +3917,7 @@ class _$WLANModelImpl extends WLANModel { @override String toString() { - return 'DeviceModelSealed.wlan(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, imageSignedIds: $imageSignedIds, healthNotices: $healthNotices, hnCounts: $hnCounts, controllerType: $controllerType, managedAPs: $managedAPs, vlan: $vlan, totalUpload: $totalUpload, totalDownload: $totalDownload, packetLoss: $packetLoss, latency: $latency, restartCount: $restartCount)'; + return 'DeviceModelSealed.wlan(id: $id, name: $name, status: $status, pmsRoom: $pmsRoom, pmsRoomId: $pmsRoomId, ipAddress: $ipAddress, macAddress: $macAddress, location: $location, lastSeen: $lastSeen, metadata: $metadata, model: $model, serialNumber: $serialNumber, firmware: $firmware, note: $note, images: $images, healthNotices: $healthNotices, hnCounts: $hnCounts, controllerType: $controllerType, managedAPs: $managedAPs, vlan: $vlan, totalUpload: $totalUpload, totalDownload: $totalDownload, packetLoss: $packetLoss, latency: $latency, restartCount: $restartCount)'; } @override @@ -4087,8 +3947,6 @@ class _$WLANModelImpl extends WLANModel { other.firmware == firmware) && (identical(other.note, note) || other.note == note) && const DeepCollectionEquality().equals(other._images, _images) && - const DeepCollectionEquality() - .equals(other._imageSignedIds, _imageSignedIds) && const DeepCollectionEquality() .equals(other._healthNotices, _healthNotices) && (identical(other.hnCounts, hnCounts) || @@ -4128,7 +3986,6 @@ class _$WLANModelImpl extends WLANModel { firmware, note, const DeepCollectionEquality().hash(_images), - const DeepCollectionEquality().hash(_imageSignedIds), const DeepCollectionEquality().hash(_healthNotices), hnCounts, controllerType, @@ -4166,10 +4023,10 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -4197,7 +4054,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4225,7 +4081,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4254,7 +4109,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4284,7 +4138,6 @@ class _$WLANModelImpl extends WLANModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, controllerType, @@ -4316,10 +4169,10 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -4347,7 +4200,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4375,7 +4227,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4404,7 +4255,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4434,7 +4284,6 @@ class _$WLANModelImpl extends WLANModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, controllerType, @@ -4466,10 +4315,10 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, + @JsonKey(name: 'infrastructure_link_id') int? infrastructureLinkId, @JsonKey(name: 'connection_state') String? connectionState, @JsonKey(name: 'signal_strength') int? signalStrength, @JsonKey(name: 'connected_clients') int? connectedClients, @@ -4497,7 +4346,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4525,7 +4373,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4554,7 +4401,6 @@ class _$WLANModelImpl extends WLANModel { String? firmware, String? note, List? images, - @JsonKey(name: 'image_signed_ids') List? imageSignedIds, @JsonKey(name: 'health_notices') List? healthNotices, @JsonKey(name: 'hn_counts') HealthCountsModel? hnCounts, @@ -4586,7 +4432,6 @@ class _$WLANModelImpl extends WLANModel { firmware, note, images, - imageSignedIds, healthNotices, hnCounts, controllerType, @@ -4663,7 +4508,6 @@ abstract class WLANModel extends DeviceModelSealed { final String? firmware, final String? note, final List? images, - @JsonKey(name: 'image_signed_ids') final List? imageSignedIds, @JsonKey(name: 'health_notices') final List? healthNotices, @JsonKey(name: 'hn_counts') final HealthCountsModel? hnCounts, @@ -4681,7 +4525,7 @@ abstract class WLANModel extends DeviceModelSealed { factory WLANModel.fromJson(Map json) = _$WLANModelImpl.fromJson; - @override // Common fields + @override String get id; @override String get name; @@ -4718,14 +4562,11 @@ abstract class WLANModel extends DeviceModelSealed { @override List? get images; @override - @JsonKey(name: 'image_signed_ids') - List? get imageSignedIds; - @override @JsonKey(name: 'health_notices') List? get healthNotices; @override @JsonKey(name: 'hn_counts') - HealthCountsModel? get hnCounts; // WLAN-specific fields + HealthCountsModel? get hnCounts; @JsonKey(name: 'controller_type') String? get controllerType; @JsonKey(name: 'managed_aps') diff --git a/lib/features/devices/data/models/device_model_sealed.g.dart b/lib/features/devices/data/models/device_model_sealed.g.dart index 161985e..31ac58a 100644 --- a/lib/features/devices/data/models/device_model_sealed.g.dart +++ b/lib/features/devices/data/models/device_model_sealed.g.dart @@ -28,9 +28,6 @@ _$APModelImpl _$$APModelImplFromJson(Map json) => note: json['note'] as String?, images: (json['images'] as List?)?.map((e) => e as String).toList(), - imageSignedIds: (json['image_signed_ids'] as List?) - ?.map((e) => e as String) - .toList(), healthNotices: (json['health_notices'] as List?) ?.map((e) => HealthNoticeModel.fromJson(e as Map)) .toList(), @@ -38,6 +35,7 @@ _$APModelImpl _$$APModelImplFromJson(Map json) => ? null : HealthCountsModel.fromJson( json['hn_counts'] as Map), + infrastructureLinkId: (json['infrastructure_link_id'] as num?)?.toInt(), connectionState: json['connection_state'] as String?, signalStrength: (json['signal_strength'] as num?)?.toInt(), connectedClients: (json['connected_clients'] as num?)?.toInt(), @@ -78,10 +76,10 @@ Map _$$APModelImplToJson(_$APModelImpl instance) { writeNotNull('firmware', instance.firmware); writeNotNull('note', instance.note); writeNotNull('images', instance.images); - writeNotNull('image_signed_ids', instance.imageSignedIds); writeNotNull('health_notices', instance.healthNotices?.map((e) => e.toJson()).toList()); writeNotNull('hn_counts', instance.hnCounts?.toJson()); + writeNotNull('infrastructure_link_id', instance.infrastructureLinkId); writeNotNull('connection_state', instance.connectionState); writeNotNull('signal_strength', instance.signalStrength); writeNotNull('connected_clients', instance.connectedClients); @@ -117,9 +115,6 @@ _$ONTModelImpl _$$ONTModelImplFromJson(Map json) => note: json['note'] as String?, images: (json['images'] as List?)?.map((e) => e as String).toList(), - imageSignedIds: (json['image_signed_ids'] as List?) - ?.map((e) => e as String) - .toList(), healthNotices: (json['health_notices'] as List?) ?.map((e) => HealthNoticeModel.fromJson(e as Map)) .toList(), @@ -166,7 +161,6 @@ Map _$$ONTModelImplToJson(_$ONTModelImpl instance) { writeNotNull('firmware', instance.firmware); writeNotNull('note', instance.note); writeNotNull('images', instance.images); - writeNotNull('image_signed_ids', instance.imageSignedIds); writeNotNull('health_notices', instance.healthNotices?.map((e) => e.toJson()).toList()); writeNotNull('hn_counts', instance.hnCounts?.toJson()); @@ -202,9 +196,6 @@ _$SwitchModelImpl _$$SwitchModelImplFromJson(Map json) => note: json['note'] as String?, images: (json['images'] as List?)?.map((e) => e as String).toList(), - imageSignedIds: (json['image_signed_ids'] as List?) - ?.map((e) => e as String) - .toList(), healthNotices: (json['health_notices'] as List?) ?.map((e) => HealthNoticeModel.fromJson(e as Map)) .toList(), @@ -253,7 +244,6 @@ Map _$$SwitchModelImplToJson(_$SwitchModelImpl instance) { writeNotNull('firmware', instance.firmware); writeNotNull('note', instance.note); writeNotNull('images', instance.images); - writeNotNull('image_signed_ids', instance.imageSignedIds); writeNotNull('health_notices', instance.healthNotices?.map((e) => e.toJson()).toList()); writeNotNull('hn_counts', instance.hnCounts?.toJson()); @@ -292,9 +282,6 @@ _$WLANModelImpl _$$WLANModelImplFromJson(Map json) => note: json['note'] as String?, images: (json['images'] as List?)?.map((e) => e as String).toList(), - imageSignedIds: (json['image_signed_ids'] as List?) - ?.map((e) => e as String) - .toList(), healthNotices: (json['health_notices'] as List?) ?.map((e) => HealthNoticeModel.fromJson(e as Map)) .toList(), @@ -338,7 +325,6 @@ Map _$$WLANModelImplToJson(_$WLANModelImpl instance) { writeNotNull('firmware', instance.firmware); writeNotNull('note', instance.note); writeNotNull('images', instance.images); - writeNotNull('image_signed_ids', instance.imageSignedIds); writeNotNull('health_notices', instance.healthNotices?.map((e) => e.toJson()).toList()); writeNotNull('hn_counts', instance.hnCounts?.toJson()); diff --git a/lib/features/devices/presentation/providers/device_ui_state_provider.g.dart b/lib/features/devices/presentation/providers/device_ui_state_provider.g.dart index 562bb51..f9d99f9 100644 --- a/lib/features/devices/presentation/providers/device_ui_state_provider.g.dart +++ b/lib/features/devices/presentation/providers/device_ui_state_provider.g.dart @@ -7,7 +7,11 @@ part of 'device_ui_state_provider.dart'; // ************************************************************************** String _$filteredDevicesListHash() => +<<<<<<< HEAD r'dba3450757fbf31ea5312471972cef46c17bdb11'; +======= + r'eb08d75fe18e5b6dddaf44ecb541ccd0d035b2c6'; +>>>>>>> 81c4e9a (Add deployment phase filtering (#13)) /// Provider for filtered devices based on UI state /// diff --git a/lib/features/devices/presentation/providers/devices_provider.g.dart b/lib/features/devices/presentation/providers/devices_provider.g.dart index 7c2ac39..7d7610e 100644 --- a/lib/features/devices/presentation/providers/devices_provider.g.dart +++ b/lib/features/devices/presentation/providers/devices_provider.g.dart @@ -37,7 +37,11 @@ final devicesNotifierProvider = ); typedef _$DevicesNotifier = AsyncNotifier>; +<<<<<<< HEAD String _$deviceNotifierHash() => r'46818e2910dc06852fcac29e0ef4473d423c34f0'; +======= +String _$deviceNotifierHash() => r'6575f26908f09f5a5bae476dedfe95eac4e2567a'; +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/devices/presentation/screens/device_detail_screen.dart b/lib/features/devices/presentation/screens/device_detail_screen.dart index cb08e2d..d85e397 100644 --- a/lib/features/devices/presentation/screens/device_detail_screen.dart +++ b/lib/features/devices/presentation/screens/device_detail_screen.dart @@ -7,6 +7,7 @@ import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provi import 'package:rgnets_fdk/features/devices/presentation/screens/note_edit_screen.dart'; import 'package:rgnets_fdk/features/devices/presentation/widgets/advanced_info_section.dart'; import 'package:rgnets_fdk/features/devices/presentation/widgets/device_detail_sections.dart'; +import 'package:rgnets_fdk/features/devices/presentation/widgets/device_speed_test_section.dart'; import 'package:rgnets_fdk/features/devices/presentation/widgets/editable_note_section.dart'; import 'package:rgnets_fdk/features/devices/presentation/widgets/unified_summary_card.dart'; import 'package:rgnets_fdk/features/onboarding/presentation/widgets/onboarding_status_card.dart'; @@ -387,11 +388,14 @@ class _OverviewTabState extends ConsumerState<_OverviewTab> } } +<<<<<<< HEAD void _handleUploadComplete() { // Refresh device data to show newly uploaded images ref.read(deviceNotifierProvider(widget.device.id).notifier).refresh(); } +======= +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) Future _handleSaveNote(String note) async { final success = await ref .read(deviceNotifierProvider(widget.device.id).notifier) @@ -467,6 +471,16 @@ class _OverviewTabState extends ConsumerState<_OverviewTab> children: [ // Unified Summary section at the top UnifiedSummaryCardContent(device: widget.device), +<<<<<<< HEAD + + // Onboarding Status section (for AP/ONT devices) + OnboardingStatusCard(deviceId: widget.device.id), + ], + ), + ), + const SizedBox(height: 16), +======= +>>>>>>> 0a05c3e (Pass in onboarding state through websocket) // Onboarding Status section (for AP/ONT devices) OnboardingStatusCard(deviceId: widget.device.id), @@ -599,6 +613,13 @@ class _StatisticsTabState extends State<_StatisticsTab> child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Speed Test Section (for APs and ONTs) + if (device.type == DeviceTypes.accessPoint || + device.type == DeviceTypes.ont) ...[ + DeviceSpeedTestSection(device: device), + const SizedBox(height: 16), + ], + // Traffic Statistics SectionCard( title: 'Traffic Statistics', @@ -612,7 +633,7 @@ class _StatisticsTabState extends State<_StatisticsTab> ], ), const SizedBox(height: 16), - + // Performance Metrics SectionCard( title: 'Performance Metrics', @@ -625,7 +646,7 @@ class _StatisticsTabState extends State<_StatisticsTab> ], ), const SizedBox(height: 16), - + // Client Statistics (for Access Points) if (device.type == DeviceTypes.accessPoint) ...[ SectionCard( @@ -640,18 +661,18 @@ class _StatisticsTabState extends State<_StatisticsTab> ), const SizedBox(height: 16), ], - + const SizedBox(height: 80), // Space for bottom bar ], ), ); } - + String _formatUptime(int seconds) { final days = seconds ~/ 86400; final hours = (seconds % 86400) ~/ 3600; final minutes = (seconds % 3600) ~/ 60; - + if (days > 0) { return '${days}d ${hours}h ${minutes}m'; } else if (hours > 0) { diff --git a/lib/features/devices/presentation/screens/devices_screen.dart b/lib/features/devices/presentation/screens/devices_screen.dart index 3f8935b..4d802e8 100644 --- a/lib/features/devices/presentation/screens/devices_screen.dart +++ b/lib/features/devices/presentation/screens/devices_screen.dart @@ -2,11 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/core/utils/list_item_helpers.dart'; import 'package:rgnets_fdk/core/widgets/hud_tab_bar.dart'; import 'package:rgnets_fdk/core/widgets/unified_list/unified_list_item.dart'; import 'package:rgnets_fdk/core/widgets/widgets.dart'; +import 'package:rgnets_fdk/features/devices/domain/constants/device_types.dart'; import 'package:rgnets_fdk/features/devices/domain/entities/device.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/device_ui_state_provider.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; @@ -24,6 +28,23 @@ class _DevicesScreenState extends ConsumerState { final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); + int? _extractApId(String deviceId) { + final parts = deviceId.split('_'); + final rawId = parts.length >= 2 ? parts.sublist(1).join('_') : deviceId; + return int.tryParse(rawId); + } + + Color _getAPNameColor(int apId, WebSocketCacheIntegration cache) { + final uplink = cache.getCachedAPUplink(apId); + if (uplink == null) { + return AppColors.error; + } + if (uplink.speedInBps != null && uplink.speedInBps! < 2500000000) { + return AppColors.error; + } + return AppColors.textPrimary; + } + String _formatNetworkInfo(Device device) { // Safely handle null and empty values using null-aware operators final ip = (device.ipAddress?.trim().isEmpty ?? true) @@ -270,6 +291,10 @@ class _DevicesScreenState extends ConsumerState { ], ), ), +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) // Search bar SearchBarWidget( @@ -283,6 +308,12 @@ class _DevicesScreenState extends ConsumerState { }, ), +<<<<<<< HEAD +======= + +>>>>>>> 81c4e9a (Add deployment phase filtering (#13)) +======= +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) // Phase filter bar _buildPhaseFilterBar(ref, devices), @@ -340,9 +371,12 @@ class _DevicesScreenState extends ConsumerState { // Device list Expanded( - child: Consumer( + child: Consumer( builder: (context, ref, child) { final uiState = ref.watch(deviceUIStateNotifierProvider); + final cacheIntegration = ref.watch( + webSocketCacheIntegrationProvider, + ); return filteredDevices.isEmpty ? EmptyState( icon: Icons.devices_other, @@ -357,8 +391,20 @@ class _DevicesScreenState extends ConsumerState { itemCount: filteredDevices.length, itemBuilder: (context, index) { final device = filteredDevices[index]; + Color? titleColor; + if (device.type == DeviceTypes.accessPoint) { + final apId = _extractApId(device.id); + if (apId != null) { + ref.watch(apUplinkInfoProvider(apId)); + titleColor = _getAPNameColor( + apId, + cacheIntegration, + ); + } + } return UnifiedListItem( title: device.name, + titleColor: titleColor, icon: ListItemHelpers.getDeviceIcon(device.type), status: ListItemHelpers.mapDeviceStatus(device.status), subtitleLines: [ @@ -387,4 +433,4 @@ class _DevicesScreenState extends ConsumerState { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/devices/presentation/screens/note_edit_screen.dart b/lib/features/devices/presentation/screens/note_edit_screen.dart index 0165f30..34ce508 100644 --- a/lib/features/devices/presentation/screens/note_edit_screen.dart +++ b/lib/features/devices/presentation/screens/note_edit_screen.dart @@ -63,6 +63,30 @@ class _NoteEditScreenState extends State { icon: const Icon(Icons.close), onPressed: _cancel, ), +<<<<<<< HEAD +======= + actions: [ + TextButton( + onPressed: _isSaving ? null : _saveNote, + child: _isSaving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + 'Save', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) ), resizeToAvoidBottomInset: true, body: SafeArea( diff --git a/lib/features/devices/presentation/widgets/device_detail_sections.dart b/lib/features/devices/presentation/widgets/device_detail_sections.dart index 75b9b48..2b8bb48 100644 --- a/lib/features/devices/presentation/widgets/device_detail_sections.dart +++ b/lib/features/devices/presentation/widgets/device_detail_sections.dart @@ -39,9 +39,13 @@ class DeviceDetailSections extends ConsumerWidget { const SizedBox(height: 16), _buildTrafficSection(context), const SizedBox(height: 16), +<<<<<<< HEAD _buildSystemSection(context), const SizedBox(height: 16), _buildImagesSection(context, ref), +======= + _buildImagesSection(context), +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) ], ); } @@ -186,6 +190,7 @@ class DeviceDetailSections extends ConsumerWidget { ); } +<<<<<<< HEAD Widget _buildSystemSection(BuildContext context) { if (device.model == null && device.serialNumber == null && @@ -211,6 +216,9 @@ class DeviceDetailSections extends ConsumerWidget { } /// Filter to only valid HTTP/HTTPS image URLs (for display) +======= + /// Filter to only valid HTTP/HTTPS image URLs +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) List get _validImages { final images = device.images; if (images == null || images.isEmpty) { diff --git a/lib/features/devices/presentation/widgets/device_speed_test_section.dart b/lib/features/devices/presentation/widgets/device_speed_test_section.dart new file mode 100644 index 0000000..609d742 --- /dev/null +++ b/lib/features/devices/presentation/widgets/device_speed_test_section.dart @@ -0,0 +1,439 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; +import 'package:rgnets_fdk/core/widgets/widgets.dart'; +import 'package:rgnets_fdk/features/devices/domain/constants/device_types.dart'; +import 'package:rgnets_fdk/features/devices/domain/entities/device.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/presentation/widgets/speed_test_popup.dart'; + +/// A section that displays speed test results for a specific device +/// and allows running new speed tests. +class DeviceSpeedTestSection extends ConsumerStatefulWidget { + const DeviceSpeedTestSection({ + required this.device, + super.key, + }); + + final Device device; + + @override + ConsumerState createState() => + _DeviceSpeedTestSectionState(); +} + +class _DeviceSpeedTestSectionState + extends ConsumerState { + List _deviceResults = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadDeviceResults(); + } + + /// Get prefixed device ID for speed test lookups. + /// If already prefixed (e.g., "ap_1307"), return as-is. + /// If raw (e.g., "1307"), add prefix based on device type. + String _getPrefixedDeviceId() { + final id = widget.device.id; + // Check if already prefixed + if (id.startsWith('ap_') || id.startsWith('ont_')) { + return id; + } + // Add prefix based on device type + final prefix = widget.device.type == DeviceTypes.accessPoint ? 'ap' : 'ont'; + return '${prefix}_$id'; + } + + int? _getNumericDeviceId() { + final id = widget.device.id; + final parts = id.split('_'); + final rawId = parts.length >= 2 ? parts.sublist(1).join('_') : id; + return int.tryParse(rawId); + } + + void _loadDeviceResults() { + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + final List results; + if (widget.device.type == DeviceTypes.accessPoint) { + final apId = _getNumericDeviceId(); + results = apId == null + ? [] + : cacheIntegration.getSpeedTestResultsForAccessPointId(apId); + } else { + results = cacheIntegration.getSpeedTestResultsForDevice( + _getPrefixedDeviceId(), + deviceType: widget.device.type, + ); + } + + LoggerService.info( + 'Loaded ${results.length} speed test result(s) for device ${_getPrefixedDeviceId()}', + tag: 'DeviceSpeedTestSection', + ); + + if (mounted) { + setState(() { + _deviceResults = results; + _isLoading = false; + }); + } + } + + String _formatSpeed(double speed) { + if (speed < 1000.0) { + return '${speed.toStringAsFixed(2)} Mbps'; + } else { + return '${(speed / 1000).toStringAsFixed(2)} Gbps'; + } + } + + String _getTimeAgo(DateTime timestamp) { + final now = DateTime.now(); + final diff = now.difference(timestamp); + + if (diff.inMinutes < 1) { + return 'Just now'; + } else if (diff.inHours < 1) { + return '${diff.inMinutes}m ago'; + } else if (diff.inDays < 1) { + return '${diff.inHours}h ago'; + } else if (diff.inDays < 30) { + return '${diff.inDays}d ago'; + } else { + return '${(diff.inDays / 30).floor()}mo ago'; + } + } + + Future _runSpeedTest() async { + if (!mounted) return; + + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + + // Try to get config from the device's existing results (uses the same test config) + SpeedTestConfig? config; + if (_deviceResults.isNotEmpty) { + final speedTestId = _deviceResults.first.speedTestId; + config = cacheIntegration.getSpeedTestConfigById(speedTestId); + if (config != null) { + LoggerService.info( + 'Running speed test for device ${_getPrefixedDeviceId()} with config from result: ${config.name} (id: $speedTestId)', + tag: 'DeviceSpeedTestSection', + ); + } + } + + // Fall back to adhoc config if no matching config found + config ??= cacheIntegration.getAdhocSpeedTestConfig(); + + if (config != null) { + LoggerService.info( + 'Running speed test for device ${_getPrefixedDeviceId()} with config: ${config.name}', + tag: 'DeviceSpeedTestSection', + ); + } + + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return SpeedTestPopup( + cachedTest: config, + apId: widget.device.type == DeviceTypes.accessPoint + ? _getNumericDeviceId() + : null, + onCompleted: () { + if (mounted) { + // Reload results after test completion + _loadDeviceResults(); + } + }, + onResultSubmitted: (result) async { + if (!result.hasError) { + await _submitDeviceResult(result); + } + }, + ); + }, + ); + } + + Future _submitDeviceResult(SpeedTestResult result) async { + try { + final prefixedId = _getPrefixedDeviceId(); + LoggerService.info( + 'Updating speed test result for device $prefixedId: ' + 'download=${result.downloadMbps}, upload=${result.uploadMbps}', + tag: 'DeviceSpeedTestSection', + ); + + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + final success = await cacheIntegration.updateDeviceSpeedTestResult( + deviceId: prefixedId, + downloadSpeed: result.downloadMbps ?? 0, + uploadSpeed: result.uploadMbps ?? 0, + latency: result.rtt ?? 0, + source: result.source, + destination: result.destination, + port: result.port, + protocol: result.iperfProtocol, + passed: result.passed, + ); + + if (success) { + LoggerService.info( + 'Speed test result updated successfully for device $prefixedId', + tag: 'DeviceSpeedTestSection', + ); + if (mounted) { + // Refresh the displayed results + _loadDeviceResults(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Speed test result submitted successfully'), + backgroundColor: AppColors.success, + duration: const Duration(seconds: 3), + ), + ); + } + } else { + LoggerService.warning( + 'Speed test submission failed for device $prefixedId - no existing result found', + tag: 'DeviceSpeedTestSection', + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Speed test submission failed - no existing result found'), + backgroundColor: AppColors.error, + duration: const Duration(seconds: 4), + ), + ); + } + } + } catch (e) { + LoggerService.error( + 'Error updating speed test result for device ${_getPrefixedDeviceId()}', + error: e, + tag: 'DeviceSpeedTestSection', + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Error submitting speed test result'), + backgroundColor: AppColors.error, + duration: const Duration(seconds: 4), + ), + ); + } + } + } + + Widget _buildResultCard(SpeedTestResult result) { + final passedColor = result.passed ? AppColors.success : AppColors.warning; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: passedColor.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: passedColor.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row with timestamp and status + Row( + children: [ + Icon( + result.passed ? Icons.check_circle : Icons.warning_amber, + color: passedColor, + size: 16, + ), + const SizedBox(width: 6), + Text( + result.passed ? 'PASSED' : 'BELOW THRESHOLD', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: passedColor, + ), + ), + const Spacer(), + Text( + _getTimeAgo(result.timestamp), + style: TextStyle( + fontSize: 10, + color: AppColors.gray500, + ), + ), + ], + ), + const SizedBox(height: 10), + + // Speed metrics row + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildSpeedMetric( + 'Download', + result.downloadSpeed, + Icons.download, + AppColors.success, + ), + _buildSpeedMetric( + 'Upload', + result.uploadSpeed, + Icons.upload, + AppColors.info, + ), + _buildSpeedMetric( + 'Latency', + result.latency, + Icons.timer, + Colors.orange, + isLatency: true, + ), + ], + ), + + // Server info + if (result.destination != null || result.serverHost != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.dns, size: 12, color: AppColors.gray500), + const SizedBox(width: 4), + Text( + 'Server: ${result.destination ?? result.serverHost}', + style: TextStyle( + fontSize: 10, + color: AppColors.gray500, + fontFamily: 'monospace', + ), + ), + ], + ), + ], + ], + ), + ); + } + + Widget _buildSpeedMetric( + String label, + double value, + IconData icon, + Color color, { + bool isLatency = false, + }) { + return Column( + children: [ + Icon(icon, color: color, size: 18), + const SizedBox(height: 2), + Text( + isLatency ? '${value.toStringAsFixed(0)} ms' : _formatSpeed(value), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: color, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 9, + color: AppColors.gray500, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const SectionCard( + title: 'Speed Test', + children: [ + Center(child: LoadingIndicator()), + ], + ); + } + + final latestResult = + _deviceResults.isNotEmpty ? _deviceResults.first : null; + + return SectionCard( + title: 'Speed Test', + children: [ + // Show latest result if available + if (latestResult != null) ...[ + _buildResultCard(latestResult), + + // Show count of previous results + if (_deviceResults.length > 1) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + '${_deviceResults.length - 1} previous test(s) available', + style: TextStyle( + fontSize: 11, + color: AppColors.gray500, + fontStyle: FontStyle.italic, + ), + ), + ), + ] else ...[ + // No results placeholder + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Icon( + Icons.speed, + size: 40, + color: AppColors.gray500, + ), + const SizedBox(height: 8), + Text( + 'No speed tests run for this device', + style: TextStyle( + fontSize: 13, + color: AppColors.gray500, + ), + ), + ], + ), + ), + ], + + // Run test button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _runSpeedTest, + icon: Icon( + latestResult != null ? Icons.refresh : Icons.play_arrow, + size: 18, + ), + label: Text( + latestResult != null ? 'Run New Test' : 'Run Speed Test', + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/issues/data/datasources/health_notices_remote_data_source.dart b/lib/features/issues/data/datasources/health_notices_remote_data_source.dart index 66692ef..08dfe77 100644 --- a/lib/features/issues/data/datasources/health_notices_remote_data_source.dart +++ b/lib/features/issues/data/datasources/health_notices_remote_data_source.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; import 'package:rgnets_fdk/features/issues/data/models/health_notices_summary_model.dart'; @@ -14,6 +13,7 @@ class HealthNoticesRemoteDataSource { final WebSocketService _socketService; /// Fetches health notices summary (notices list + counts) from backend +<<<<<<< HEAD Future fetchSummary() async { if (kDebugMode) { print('HealthNoticesDataSource: fetchSummary called, isConnected=${_socketService.isConnected}'); @@ -24,14 +24,14 @@ class HealthNoticesRemoteDataSource { print('HealthNoticesDataSource: WebSocket not connected, returning empty'); } return const HealthNoticesSummaryModel(); +======= + Future fetchSummary() async { + if (!_socketService.isConnected) { + return HealthNoticesSummary.empty(); +>>>>>>> 3bdf0aa (Uplink added) } try { - if (kDebugMode) { - print('HealthNoticesDataSource: Sending request via requestActionCable...'); - } - - // Use the WebSocket service's built-in request/response correlation final response = await _socketService.requestActionCable( action: 'resource_action', resourceType: 'health_notices', @@ -39,26 +39,20 @@ class HealthNoticesRemoteDataSource { timeout: const Duration(seconds: 10), ); - if (kDebugMode) { - print('HealthNoticesDataSource: Got response type=${response.type}'); - } - - // Check for error response if (response.type == 'error') { +<<<<<<< HEAD if (kDebugMode) { print('HealthNoticesDataSource: Error response received'); } return const HealthNoticesSummaryModel(); +======= + return HealthNoticesSummary.empty(); +>>>>>>> 3bdf0aa (Uplink added) } - // For resource_response, the data is in payload['data'] - // which contains { notices: [...], counts: {...} } final responseData = response.payload['data']; - if (kDebugMode) { - print('HealthNoticesDataSource: responseData type=${responseData.runtimeType}'); - } - if (responseData is! Map) { +<<<<<<< HEAD if (kDebugMode) { print('HealthNoticesDataSource: responseData is not a Map, returning empty'); } @@ -80,6 +74,16 @@ class HealthNoticesRemoteDataSource { print('HealthNoticesDataSource: Request failed: $e'); } return const HealthNoticesSummaryModel(); +======= + return HealthNoticesSummary.empty(); + } + + return HealthNoticesSummary.fromJson(responseData); + } on TimeoutException { + return HealthNoticesSummary.empty(); + } on Exception { + return HealthNoticesSummary.empty(); +>>>>>>> 3bdf0aa (Uplink added) } } diff --git a/lib/features/issues/presentation/providers/health_notices_provider.dart b/lib/features/issues/presentation/providers/health_notices_provider.dart index e150b69..9e4719f 100644 --- a/lib/features/issues/presentation/providers/health_notices_provider.dart +++ b/lib/features/issues/presentation/providers/health_notices_provider.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; @@ -48,42 +47,24 @@ class AggregateHealthCountsNotifier extends _$AggregateHealthCountsNotifier { // Get cached devices with health notice data from in-memory WebSocket cache final devices = cacheIntegration.getAllCachedDeviceModels(); - if (kDebugMode) { - print('AggregateHealthCountsNotifier: Found ${devices.length} cached devices'); - } - // Aggregate health counts from all devices var totalFatal = 0; var totalCritical = 0; var totalWarning = 0; var totalNotice = 0; - var devicesWithHnCounts = 0; for (final device in devices) { final counts = device.hnCounts; if (counts != null) { - devicesWithHnCounts++; totalFatal += counts.fatal; totalCritical += counts.critical; totalWarning += counts.warning; totalNotice += counts.notice; - // Log first few devices with hn_counts to debug - if (kDebugMode && devicesWithHnCounts <= 5) { - print(' DEBUG Device ${device.deviceName} (${device.deviceId}): total=${counts.total}, fatal=${counts.fatal}, critical=${counts.critical}, warning=${counts.warning}, notice=${counts.notice}'); - } } } - if (kDebugMode) { - print('AggregateHealthCountsNotifier: $devicesWithHnCounts/${devices.length} devices have hn_counts'); - } - final total = totalFatal + totalCritical + totalWarning + totalNotice; - if (kDebugMode) { - print('AggregateHealthCountsNotifier: Aggregated counts - total=$total, fatal=$totalFatal, critical=$totalCritical, warning=$totalWarning, notice=$totalNotice'); - } - return HealthCounts( total: total, fatal: totalFatal, @@ -117,11 +98,7 @@ HealthCounts aggregateHealthCounts(AggregateHealthCountsRef ref) { @Riverpod(keepAlive: true) int criticalIssueCount(CriticalIssueCountRef ref) { final notices = ref.watch(healthNoticesListProvider); - final count = notices.criticalCount; - if (kDebugMode) { - print('criticalIssueCountProvider: criticalCount=$count from ${notices.length} total notices'); - } - return count; + return notices.criticalCount; } /// Provider that extracts health notices from cached device data @@ -162,6 +139,7 @@ class HealthNoticesNotifier extends _$HealthNoticesNotifier { } } +<<<<<<< HEAD LoggerService.debug( 'HEALTH: Extracted ${notices.length} total notices from $devicesWithNotices devices with notices', tag: 'HealthNotices', @@ -171,6 +149,8 @@ class HealthNoticesNotifier extends _$HealthNoticesNotifier { print('HealthNoticesNotifier: Found ${notices.length} total notices from ${devices.length} devices'); } +======= +>>>>>>> 3bdf0aa (Uplink added) // Sort by severity (highest first), then by creation time (newest first) notices.sort((a, b) { final severityCompare = b.severity.weight.compareTo(a.severity.weight); diff --git a/lib/features/issues/presentation/providers/health_notices_provider.g.dart b/lib/features/issues/presentation/providers/health_notices_provider.g.dart index 61d4092..e4aeff5 100644 --- a/lib/features/issues/presentation/providers/health_notices_provider.g.dart +++ b/lib/features/issues/presentation/providers/health_notices_provider.g.dart @@ -26,7 +26,7 @@ final aggregateHealthCountsProvider = Provider.internal( typedef AggregateHealthCountsRef = ProviderRef; String _$criticalIssueCountHash() => - r'6fe84d5eca813eee492c1c44bd6d43518311efc2'; + r'4932300bb1c0c4ebff301cd6f4dd3707bfb042cb'; /// Provider that returns the count of critical issues (fatal + critical) /// @@ -79,7 +79,7 @@ final filteredHealthNoticesProvider = Provider>.internal( typedef FilteredHealthNoticesRef = ProviderRef>; String _$aggregateHealthCountsNotifierHash() => - r'cbb9e11f850a5bbf4c3a520a4427550d1cf65b2b'; + r'846525b9a40c26861f9caa3e7f9d48e0a0f71f9c'; /// Provider that aggregates health counts from cached device data /// This uses device data that's already received via WebSocket @@ -99,7 +99,11 @@ final aggregateHealthCountsNotifierProvider = typedef _$AggregateHealthCountsNotifier = AsyncNotifier; String _$healthNoticesNotifierHash() => +<<<<<<< HEAD r'249e101081f539d2eceef8523c67a68998788c67'; +======= + r'ff0e1e399bb3a0c928b3639131f1b83dc6d53a5e'; +>>>>>>> 3bdf0aa (Uplink added) /// Provider that extracts health notices from cached device data /// diff --git a/lib/features/onboarding/presentation/providers/device_onboarding_provider.g.dart b/lib/features/onboarding/presentation/providers/device_onboarding_provider.g.dart index a061b53..0475b52 100644 --- a/lib/features/onboarding/presentation/providers/device_onboarding_provider.g.dart +++ b/lib/features/onboarding/presentation/providers/device_onboarding_provider.g.dart @@ -42,7 +42,11 @@ final messageResolverProvider = AutoDisposeProvider.internal( typedef MessageResolverRef = AutoDisposeProviderRef; String _$deviceOnboardingStateHash() => +<<<<<<< HEAD r'a6bb3f939d79c780098bca9aecae84a7b9d83e86'; +======= + r'78fa397037352d508c551bff635a9650bdc6ca6b'; +>>>>>>> 6a559fa (Draft for device onboarding) /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/onboarding/presentation/widgets/onboarding_status_card.dart b/lib/features/onboarding/presentation/widgets/onboarding_status_card.dart index 60d39d8..22ac188 100644 --- a/lib/features/onboarding/presentation/widgets/onboarding_status_card.dart +++ b/lib/features/onboarding/presentation/widgets/onboarding_status_card.dart @@ -46,7 +46,14 @@ class OnboardingStatusCardContent extends StatelessWidget { required this.title, required this.resolution, this.onTap, +<<<<<<< HEAD +<<<<<<< HEAD this.showDivider = true, +======= +>>>>>>> 6a559fa (Draft for device onboarding) +======= + this.showDivider = true, +>>>>>>> 0a05c3e (Pass in onboarding state through websocket) super.key, }); @@ -54,6 +61,8 @@ class OnboardingStatusCardContent extends StatelessWidget { final String title; final String resolution; final VoidCallback? onTap; +<<<<<<< HEAD +<<<<<<< HEAD final bool showDivider; @override @@ -157,6 +166,105 @@ class OnboardingStatusCardContent extends StatelessWidget { ), ), ], +======= +======= + final bool showDivider; +>>>>>>> 0a05c3e (Pass in onboarding state through websocket) + + @override + Widget build(BuildContext context) { + final isComplete = state.isComplete; + + // Use orange/warning styling when not complete + final titleColor = isComplete ? Colors.black87 : Colors.white; + final stageColor = isComplete ? Colors.green : Colors.orange; + final titleBgColor = isComplete ? null : Colors.orange; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Divider line at top (matches design) - only for complete state + if (showDivider && isComplete) + Divider( + color: Colors.grey[300], + thickness: 1, + height: 1, + ), + + // Title header with background when not complete + if (!isComplete) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + color: titleBgColor, + child: Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: titleColor, + ), + ), + ), + + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Error box (if error exists) - show at top + if (state.errorText != null && state.errorText!.isNotEmpty) ...[ + _buildErrorBox(context), + const SizedBox(height: 12), + ], + + // Title header (black text, bold) - only for complete state + if (isComplete) + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: titleColor, + ), + ), + + if (isComplete) const SizedBox(height: 4), + + // Stage indicator + Text( + 'Stage ${state.currentStage}/${state.maxStages}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: stageColor, + ), + ), + + const SizedBox(height: 16), + + // Stage progress circles + _buildStageCircles(context), + + const SizedBox(height: 16), + + // Resolution text + Text( + resolution, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + ), + ], + ), + ), +<<<<<<< HEAD + ), +>>>>>>> 6a559fa (Draft for device onboarding) +======= + ], +>>>>>>> 0a05c3e (Pass in onboarding state through websocket) ); } @@ -191,23 +299,82 @@ class OnboardingStatusCardContent extends StatelessWidget { ); } +<<<<<<< HEAD +<<<<<<< HEAD +======= + Widget _buildStageRow(BuildContext context) { + final elapsedText = state.elapsedTimeFormatted; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Stage indicator + Text( + 'Stage ${state.currentStage}/${state.maxStages}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.orange, + ), + ), + + // Elapsed time + if (elapsedText != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + elapsedText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.orange, + ), + ), + ), + ], + ); + } + +>>>>>>> 6a559fa (Draft for device onboarding) +======= +>>>>>>> 0a05c3e (Pass in onboarding state through websocket) Widget _buildStageCircles(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(state.maxStages, (index) { final stageNumber = index + 1; +<<<<<<< HEAD final isCompleted = stageNumber <= state.currentStage; final isCurrent = stageNumber == state.currentStage; final isComplete = state.isComplete; +======= + final isCompleted = stageNumber < state.currentStage; + final isCurrent = stageNumber == state.currentStage; + final isComplete = state.isComplete; + + // If onboarding is complete, all stages show as completed +>>>>>>> 6a559fa (Draft for device onboarding) if (isComplete) { return _buildCompletedCircle(); } +<<<<<<< HEAD +======= + // Current stage or completed stages +>>>>>>> 6a559fa (Draft for device onboarding) if (isCompleted || (isCurrent && state.isComplete)) { return _buildCompletedCircle(); } +<<<<<<< HEAD +======= + // Pending stages (including current if not complete) +>>>>>>> 6a559fa (Draft for device onboarding) return _buildPendingCircle(); }), ); diff --git a/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart b/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart index fa29fab..ce86869 100644 --- a/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart +++ b/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart @@ -181,6 +181,10 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { final roomId = _parseRoomId(roomData['id']); final roomName = _buildRoomName(roomData); +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) // DEBUG: Log room data structure _logger.i('DEBUG ROOM $roomId ($roomName): roomData keys = ${roomData.keys.toList()}'); _logger.i('DEBUG ROOM $roomId: access_points = ${roomData['access_points']}'); @@ -188,12 +192,25 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { _logger.i('DEBUG ROOM $roomId: switch_ports = ${roomData['switch_ports']}'); _logger.i('DEBUG ROOM $roomId: deviceModels count = ${deviceModels.length}'); +<<<<<<< HEAD +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) // Extract device references from room data final deviceRefs = _extractDeviceReferences(roomData); final totalDevices = deviceRefs.length; +<<<<<<< HEAD +<<<<<<< HEAD _logger.i('DEBUG ROOM $roomId: Extracted ${deviceRefs.length} device refs: $deviceRefs'); +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= + _logger.i('DEBUG ROOM $roomId: Extracted ${deviceRefs.length} device refs: $deviceRefs'); + +>>>>>>> 47e623e (Json credential and room readiness (#18)) if (totalDevices == 0) { return RoomReadinessMetrics( roomId: roomId, @@ -214,9 +231,19 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { for (final ref in deviceRefs) { final device = _findDevice(ref, deviceModels); +<<<<<<< HEAD +<<<<<<< HEAD _logger.i('DEBUG ROOM $roomId: Finding device ref=${ref['id']} type=${ref['type']} -> found=${device != null}'); if (device == null) { _logger.w('DEBUG ROOM $roomId: DEVICE NOT FOUND - ref=$ref'); +======= + if (device == null) { +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= + _logger.i('DEBUG ROOM $roomId: Finding device ref=${ref['id']} type=${ref['type']} -> found=${device != null}'); + if (device == null) { + _logger.w('DEBUG ROOM $roomId: DEVICE NOT FOUND - ref=$ref'); +>>>>>>> 47e623e (Json credential and room readiness (#18)) // Device reference exists but device not found - critical issue issues.add( Issue( @@ -366,10 +393,20 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { dynamic _findDevice(Map ref, List deviceModels) { final refId = ref['id']?.toString(); final refType = ref['type'] as String?; +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) if (refId == null) { _logger.w('DEBUG _findDevice: refId is null for ref=$ref'); return null; } +<<<<<<< HEAD +======= + if (refId == null) return null; +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) // Build expected device ID with prefix final prefix = switch (refType) { @@ -380,6 +417,10 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { }; final prefixedId = '$prefix$refId'; +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) _logger.i('DEBUG _findDevice: Looking for refId=$refId prefixedId=$prefixedId in ${deviceModels.length} devices'); // Log first few device IDs for comparison @@ -394,18 +435,37 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { _logger.i('DEBUG _findDevice: Sample device IDs in cache: $sampleIds'); } +<<<<<<< HEAD +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) for (final device in deviceModels) { try { final deviceId = device.id as String?; if (deviceId == prefixedId || deviceId == refId) { +<<<<<<< HEAD +<<<<<<< HEAD + _logger.i('DEBUG _findDevice: MATCH FOUND deviceId=$deviceId'); +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= _logger.i('DEBUG _findDevice: MATCH FOUND deviceId=$deviceId'); +>>>>>>> 47e623e (Json credential and room readiness (#18)) return device; } } catch (_) { // Handle case where device is a Map if (device is Map) { if (device['id']?.toString() == refId) { +<<<<<<< HEAD +<<<<<<< HEAD _logger.i('DEBUG _findDevice: MATCH FOUND (Map) id=${device['id']}'); +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= + _logger.i('DEBUG _findDevice: MATCH FOUND (Map) id=${device['id']}'); +>>>>>>> 47e623e (Json credential and room readiness (#18)) return device; } } @@ -415,11 +475,23 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { // Also check in the reference data itself (inline device data) final refData = ref['data']; if (refData is Map && refData.containsKey('online')) { +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) _logger.i('DEBUG _findDevice: Using inline refData with online=${refData['online']}'); return refData; } _logger.w('DEBUG _findDevice: NO MATCH for refId=$refId prefixedId=$prefixedId'); +<<<<<<< HEAD +======= + return refData; + } + +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +>>>>>>> 47e623e (Json credential and room readiness (#18)) return null; } @@ -507,6 +579,32 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { return RoomStatus.empty; } +<<<<<<< HEAD +<<<<<<< HEAD + // Check for device-missing issues (always DOWN - device expected but not found) + final hasMissingDevice = issues.any((i) => i.code == 'DEVICE_MISSING'); + if (hasMissingDevice) { + return RoomStatus.down; + } + + // DOWN only if ALL devices are offline + if (onlineDevices == 0) { + return RoomStatus.down; + } + + // PARTIAL if some devices offline OR any issues exist + if (onlineDevices < totalDevices || issues.isNotEmpty) { +======= + // Check for critical issues + final hasCritical = issues.any((i) => i.severity == IssueSeverity.critical); + if (hasCritical) { + return RoomStatus.down; + } + + // Check for any non-critical issues + if (issues.isNotEmpty) { +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= // Check for device-missing issues (always DOWN - device expected but not found) final hasMissingDevice = issues.any((i) => i.code == 'DEVICE_MISSING'); if (hasMissingDevice) { @@ -520,6 +618,7 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { // PARTIAL if some devices offline OR any issues exist if (onlineDevices < totalDevices || issues.isNotEmpty) { +>>>>>>> 47e623e (Json credential and room readiness (#18)) return RoomStatus.partial; } diff --git a/lib/features/rooms/presentation/providers/room_view_models.dart b/lib/features/rooms/presentation/providers/room_view_models.dart index eb07f87..e016e00 100644 --- a/lib/features/rooms/presentation/providers/room_view_models.dart +++ b/lib/features/rooms/presentation/providers/room_view_models.dart @@ -3,7 +3,14 @@ import 'package:rgnets_fdk/features/devices/domain/entities/room.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; import 'package:rgnets_fdk/features/room_readiness/domain/entities/room_readiness.dart'; import 'package:rgnets_fdk/features/room_readiness/presentation/providers/room_readiness_provider.dart'; +<<<<<<< HEAD +<<<<<<< HEAD import 'package:rgnets_fdk/features/rooms/presentation/providers/room_ui_state_provider.dart'; +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +import 'package:rgnets_fdk/features/rooms/presentation/providers/room_ui_state_provider.dart'; +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -161,6 +168,10 @@ List filteredRoomViewModels( filtered = List.from(viewModels); } +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) // Apply search filter if (searchQuery.isNotEmpty) { filtered = filtered.where((vm) { @@ -169,6 +180,11 @@ List filteredRoomViewModels( }).toList(); } +<<<<<<< HEAD +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) // Sort by room name alphabetically filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); diff --git a/lib/features/rooms/presentation/providers/room_view_models.g.dart b/lib/features/rooms/presentation/providers/room_view_models.g.dart index 932578e..f8820c2 100644 --- a/lib/features/rooms/presentation/providers/room_view_models.g.dart +++ b/lib/features/rooms/presentation/providers/room_view_models.g.dart @@ -187,7 +187,15 @@ class _RoomViewModelByIdProviderElement } String _$filteredRoomViewModelsHash() => +<<<<<<< HEAD +<<<<<<< HEAD r'6fe8852fd0d485183dde0dee1d150dba626a92bc'; +======= + r'c4f8d9f36ebbf2eea084e9f4a5d45494abf7819a'; +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= + r'6fe8852fd0d485183dde0dee1d150dba626a92bc'; +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) /// Provider for filtered room view models /// diff --git a/lib/features/rooms/presentation/screens/room_detail_screen.dart b/lib/features/rooms/presentation/screens/room_detail_screen.dart index a8f1dbf..26b0b73 100644 --- a/lib/features/rooms/presentation/screens/room_detail_screen.dart +++ b/lib/features/rooms/presentation/screens/room_detail_screen.dart @@ -2,14 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/core/widgets/widgets.dart'; import 'package:rgnets_fdk/features/devices/domain/constants/device_types.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; +<<<<<<< HEAD +<<<<<<< HEAD import 'package:rgnets_fdk/features/onboarding/presentation/widgets/onboarding_stage_badge.dart'; +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +import 'package:rgnets_fdk/features/onboarding/presentation/widgets/onboarding_stage_badge.dart'; +>>>>>>> 6a559fa (Draft for device onboarding) import 'package:rgnets_fdk/features/room_readiness/domain/entities/room_readiness.dart'; import 'package:rgnets_fdk/features/rooms/presentation/providers/room_device_view_model.dart'; import 'package:rgnets_fdk/features/rooms/presentation/providers/room_view_models.dart'; import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart'; +import 'package:rgnets_fdk/features/speed_test/presentation/widgets/room_speed_test_selector.dart'; /// Room detail screen with device management class RoomDetailScreen extends ConsumerStatefulWidget { @@ -310,7 +321,15 @@ class _RoomHeader extends StatelessWidget { } } +<<<<<<< HEAD +<<<<<<< HEAD +class _OverviewTab extends ConsumerWidget { +======= +class _OverviewTab extends StatelessWidget { +>>>>>>> 24906fa (Add pms speed test) +======= class _OverviewTab extends ConsumerWidget { +>>>>>>> 47e623e (Json credential and room readiness (#18)) const _OverviewTab({required this.roomVm}); final RoomViewModel roomVm; @@ -325,6 +344,14 @@ class _OverviewTab extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Speed Test Results for this room + RoomSpeedTestSelector( + pmsRoomId: roomVm.room.id, + roomName: roomVm.name, + apIds: const [], // TODO: Get AP IDs from room devices + ), + const SizedBox(height: 16), + // Room Information _SectionCard( title: 'Room Information', @@ -415,6 +442,7 @@ class _DevicesTab extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Use the new RoomDeviceNotifier for proper MVVM architecture final roomDeviceState = ref.watch(roomDeviceNotifierProvider(roomVm.id)); + final cacheIntegration = ref.watch(webSocketCacheIntegrationProvider); if (roomDeviceState.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -524,6 +552,14 @@ class _DevicesTab extends ConsumerWidget { itemCount: filteredDevices.length, itemBuilder: (context, index) { final device = filteredDevices[index]; + Color? titleColor; + if (device.type == DeviceTypes.accessPoint) { + final apId = _extractApId(device.id); + if (apId != null) { + ref.watch(apUplinkInfoProvider(apId)); + titleColor = _getAPNameColor(apId, cacheIntegration); + } + } return _DeviceListItem( device: { 'id': device.id, @@ -532,6 +568,7 @@ class _DevicesTab extends ConsumerWidget { 'status': device.status, 'ipAddress': device.ipAddress, }, + titleColor: titleColor, onTap: () { // Navigate to device detail context.push('/devices/${device.id}'); @@ -660,6 +697,23 @@ class _AnalyticsTab extends StatelessWidget { } } +int? _extractApId(String deviceId) { + final parts = deviceId.split('_'); + final rawId = parts.length >= 2 ? parts.sublist(1).join('_') : deviceId; + return int.tryParse(rawId); +} + +Color _getAPNameColor(int apId, WebSocketCacheIntegration cache) { + final uplink = cache.getCachedAPUplink(apId); + if (uplink == null) { + return AppColors.error; + } + if (uplink.speedInBps != null && uplink.speedInBps! < 2500000000) { + return AppColors.error; + } + return AppColors.textPrimary; +} + class _DeviceTypeChip extends StatelessWidget { const _DeviceTypeChip({ @@ -700,9 +754,11 @@ class _DeviceListItem extends StatelessWidget { const _DeviceListItem({ required this.device, + this.titleColor, this.onTap, }); final Map device; + final Color? titleColor; final VoidCallback? onTap; @override @@ -734,7 +790,10 @@ class _DeviceListItem extends StatelessWidget { ), title: Text( device['name'] as String, - style: const TextStyle(fontWeight: FontWeight.w600), + style: TextStyle( + fontWeight: FontWeight.w600, + color: titleColor, + ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1042,4 +1101,4 @@ class _QuickActionButton extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/rooms/presentation/screens/rooms_screen.dart b/lib/features/rooms/presentation/screens/rooms_screen.dart index 68881a1..76fb918 100644 --- a/lib/features/rooms/presentation/screens/rooms_screen.dart +++ b/lib/features/rooms/presentation/screens/rooms_screen.dart @@ -6,7 +6,14 @@ import 'package:rgnets_fdk/core/widgets/hud_tab_bar.dart'; import 'package:rgnets_fdk/core/widgets/unified_list/unified_list_item.dart'; import 'package:rgnets_fdk/core/widgets/widgets.dart'; import 'package:rgnets_fdk/features/room_readiness/domain/entities/room_readiness.dart'; +<<<<<<< HEAD +<<<<<<< HEAD import 'package:rgnets_fdk/features/rooms/presentation/providers/room_ui_state_provider.dart'; +======= +>>>>>>> da0b3f7 (Integrate room readiness status labels into Locations UI (#12)) +======= +import 'package:rgnets_fdk/features/rooms/presentation/providers/room_ui_state_provider.dart'; +>>>>>>> 7aa372b (Fix ui bug and set up websockets for updating notes) import 'package:rgnets_fdk/features/rooms/presentation/providers/room_view_models.dart'; import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart'; diff --git a/lib/features/speed_test/data/datasources/speed_test_data_source.dart b/lib/features/speed_test/data/datasources/speed_test_data_source.dart new file mode 100644 index 0000000..ee9d7ba --- /dev/null +++ b/lib/features/speed_test/data/datasources/speed_test_data_source.dart @@ -0,0 +1,36 @@ +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; + +/// Abstract interface for speed test data source. +abstract class SpeedTestDataSource { + // ============================================================================ + // Speed Test Config Operations + // ============================================================================ + + /// Fetch all speed test configurations from remote + Future> getSpeedTestConfigs(); + + /// Fetch a specific speed test configuration by ID + Future getSpeedTestConfig(int id); + + // ============================================================================ + // Speed Test Result Operations + // ============================================================================ + + /// Fetch speed test results with optional filtering + Future> getSpeedTestResults({ + int? speedTestId, + int? accessPointId, + int? limit, + int? offset, + }); + + /// Fetch a specific speed test result by ID + Future getSpeedTestResult(int id); + + /// Create a new speed test result + Future createSpeedTestResult(SpeedTestResult result); + + /// Update an existing speed test result + Future updateSpeedTestResult(SpeedTestResult result); +} diff --git a/lib/features/speed_test/data/datasources/speed_test_websocket_data_source.dart b/lib/features/speed_test/data/datasources/speed_test_websocket_data_source.dart new file mode 100644 index 0000000..6aaf9f1 --- /dev/null +++ b/lib/features/speed_test/data/datasources/speed_test_websocket_data_source.dart @@ -0,0 +1,262 @@ +import 'package:logger/logger.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/services/websocket_service.dart'; +import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_data_source.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; + +/// WebSocket-based data source for speed test operations. +class SpeedTestWebSocketDataSource implements SpeedTestDataSource { + SpeedTestWebSocketDataSource({ + required WebSocketService webSocketService, + Logger? logger, + }) : _webSocketService = webSocketService, + _logger = logger ?? Logger(); + + final WebSocketService _webSocketService; + final Logger _logger; + + static const String _speedTestConfigResourceType = 'speed_tests'; + static const String _speedTestResultResourceType = 'speed_test_results'; + + // ============================================================================ + // Speed Test Config Operations + // ============================================================================ + + @override + Future> getSpeedTestConfigs() async { + _logger.i('SpeedTestWebSocketDataSource: getSpeedTestConfigs() called'); + + if (!_webSocketService.isConnected) { + _logger.w('SpeedTestWebSocketDataSource: WebSocket not connected'); + return []; + } + + try { + final response = await _webSocketService.requestActionCable( + action: 'index', + resourceType: _speedTestConfigResourceType, + ); + + final data = response.payload['data']; + LoggerService.info( + 'SpeedTestConfigs raw response: ${response.payload}', + tag: 'SpeedTestWS', + ); + if (data is List) { + LoggerService.info( + 'SpeedTestConfigs received ${data.length} configs', + tag: 'SpeedTestWS', + ); + for (int i = 0; i < data.length; i++) { + final json = data[i]; + LoggerService.info( + 'Config[$i]: id=${json['id']}, name=${json['name']}, target=${json['target']}', + tag: 'SpeedTestWS', + ); + } + return data + .map((dynamic json) => SpeedTestConfig.fromJson( + Map.from(json as Map), + )) + .toList(); + } + + LoggerService.warning('SpeedTestConfigs: data is not a List', tag: 'SpeedTestWS'); + return []; + } catch (e) { + _logger.e('SpeedTestWebSocketDataSource: Failed to get configs: $e'); + return []; + } + } + + @override + Future getSpeedTestConfig(int id) async { + _logger.i('SpeedTestWebSocketDataSource: getSpeedTestConfig($id) called'); + + if (!_webSocketService.isConnected) { + throw StateError('WebSocket not connected'); + } + + final response = await _webSocketService.requestActionCable( + action: 'show', + resourceType: _speedTestConfigResourceType, + additionalData: {'id': id}, + ); + + final data = response.payload['data']; + if (data != null) { + return SpeedTestConfig.fromJson(Map.from(data as Map)); + } + + throw Exception('Speed test config with id $id not found'); + } + + // ============================================================================ + // Speed Test Result Operations + // ============================================================================ + + @override + Future> getSpeedTestResults({ + int? speedTestId, + int? accessPointId, + int? limit, + int? offset, + }) async { + _logger.i( + 'SpeedTestWebSocketDataSource: getSpeedTestResults(' + 'speedTestId: $speedTestId, accessPointId: $accessPointId, ' + 'limit: $limit, offset: $offset) called', + ); + + if (!_webSocketService.isConnected) { + _logger.w('SpeedTestWebSocketDataSource: WebSocket not connected'); + return []; + } + + try { + final additionalData = {}; + if (speedTestId != null) additionalData['speed_test_id'] = speedTestId; + if (accessPointId != null) { + additionalData['access_point_id'] = accessPointId; + } + if (limit != null) additionalData['limit'] = limit; + if (offset != null) additionalData['offset'] = offset; + + final response = await _webSocketService.requestActionCable( + action: 'index', + resourceType: _speedTestResultResourceType, + additionalData: additionalData.isNotEmpty ? additionalData : null, + ); + + final data = response.payload['data']; + LoggerService.info( + 'SpeedTestResults raw response: ${response.payload}', + tag: 'SpeedTestWS', + ); + if (data is List) { + LoggerService.info( + 'SpeedTestResults received ${data.length} results', + tag: 'SpeedTestWS', + ); + for (var i = 0; i < data.length && i < 5; i++) { + final json = data[i] as Map; + LoggerService.info( + 'Result[$i]: id=${json['id']}, speed_test_id=${json['speed_test_id']}, ' + 'download=${json['download_mbps']}, upload=${json['upload_mbps']}', + tag: 'SpeedTestWS', + ); + } + if (data.length > 5) { + LoggerService.info('... and ${data.length - 5} more results', tag: 'SpeedTestWS'); + } + return data + .map((dynamic json) => SpeedTestResult.fromJsonWithValidation( + Map.from(json as Map), + )) + .toList(); + } + + LoggerService.warning('SpeedTestResults: data is not a List', tag: 'SpeedTestWS'); + return []; + } catch (e) { + _logger.e('SpeedTestWebSocketDataSource: Failed to get results: $e'); + return []; + } + } + + @override + Future getSpeedTestResult(int id) async { + _logger.i('SpeedTestWebSocketDataSource: getSpeedTestResult($id) called'); + + if (!_webSocketService.isConnected) { + throw StateError('WebSocket not connected'); + } + + final response = await _webSocketService.requestActionCable( + action: 'show', + resourceType: _speedTestResultResourceType, + additionalData: {'id': id}, + ); + + final data = response.payload['data']; + if (data != null) { + return SpeedTestResult.fromJsonWithValidation( + Map.from(data as Map), + ); + } + + throw Exception('Speed test result with id $id not found'); + } + + @override + Future createSpeedTestResult(SpeedTestResult result) async { + _logger.i('SpeedTestWebSocketDataSource: createSpeedTestResult() called'); + + if (!_webSocketService.isConnected) { + throw StateError('WebSocket not connected'); + } + + final jsonToSend = result.toJson(); + LoggerService.info( + 'createSpeedTestResult sending: $jsonToSend', + tag: 'SpeedTestWS', + ); + + final response = await _webSocketService.requestActionCable( + action: 'create', + resourceType: _speedTestResultResourceType, + additionalData: jsonToSend, + ); + + LoggerService.info( + 'createSpeedTestResult response: ${response.payload}', + tag: 'SpeedTestWS', + ); + + final data = response.payload['data']; + if (data != null) { + return SpeedTestResult.fromJsonWithValidation( + Map.from(data as Map), + ); + } + + throw Exception( + response.payload['error']?.toString() ?? + 'Failed to create speed test result', + ); + } + + @override + Future updateSpeedTestResult(SpeedTestResult result) async { + _logger.i( + 'SpeedTestWebSocketDataSource: updateSpeedTestResult(${result.id}) called', + ); + + if (!_webSocketService.isConnected) { + throw StateError('WebSocket not connected'); + } + + if (result.id == null) { + throw ArgumentError('Cannot update speed test result without id'); + } + + final response = await _webSocketService.requestActionCable( + action: 'update', + resourceType: _speedTestResultResourceType, + additionalData: result.toJson(), + ); + + final data = response.payload['data']; + if (data != null) { + return SpeedTestResult.fromJsonWithValidation( + Map.from(data as Map), + ); + } + + throw Exception( + response.payload['error']?.toString() ?? + 'Failed to update speed test result', + ); + } +} diff --git a/lib/features/speed_test/data/repositories/speed_test_repository_impl.dart b/lib/features/speed_test/data/repositories/speed_test_repository_impl.dart new file mode 100644 index 0000000..60e58cf --- /dev/null +++ b/lib/features/speed_test/data/repositories/speed_test_repository_impl.dart @@ -0,0 +1,224 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:logger/logger.dart'; +import 'package:rgnets_fdk/core/errors/failures.dart'; +import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_data_source.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_with_results.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/repositories/speed_test_repository.dart'; + +/// Implementation of [SpeedTestRepository] using WebSocket data source. +class SpeedTestRepositoryImpl implements SpeedTestRepository { + SpeedTestRepositoryImpl({ + required SpeedTestDataSource dataSource, + Logger? logger, + }) : _dataSource = dataSource, + _logger = logger ?? Logger(); + + final SpeedTestDataSource _dataSource; + final Logger _logger; + + // ============================================================================ + // Speed Test Config Operations + // ============================================================================ + + @override + Future>> getSpeedTestConfigs() async { + try { + _logger.i('SpeedTestRepositoryImpl: getSpeedTestConfigs() called'); + final configs = await _dataSource.getSpeedTestConfigs(); + _logger.i( + 'SpeedTestRepositoryImpl: Got ${configs.length} speed test configs', + ); + return Right(configs); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to get configs: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + @override + Future> getSpeedTestConfig(int id) async { + try { + _logger.i('SpeedTestRepositoryImpl: getSpeedTestConfig($id) called'); + final config = await _dataSource.getSpeedTestConfig(id); + return Right(config); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to get config $id: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + // ============================================================================ + // Speed Test Result Operations + // ============================================================================ + + @override + Future>> getSpeedTestResults({ + int? speedTestId, + int? accessPointId, + int? limit, + int? offset, + }) async { + try { + _logger.i( + 'SpeedTestRepositoryImpl: getSpeedTestResults(' + 'speedTestId: $speedTestId, accessPointId: $accessPointId) called', + ); + final results = await _dataSource.getSpeedTestResults( + speedTestId: speedTestId, + accessPointId: accessPointId, + limit: limit, + offset: offset, + ); + _logger.i( + 'SpeedTestRepositoryImpl: Got ${results.length} speed test results', + ); + return Right(results); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to get results: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + @override + Future> getSpeedTestResult(int id) async { + try { + _logger.i('SpeedTestRepositoryImpl: getSpeedTestResult($id) called'); + final result = await _dataSource.getSpeedTestResult(id); + return Right(result); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to get result $id: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + @override + Future> createSpeedTestResult( + SpeedTestResult result, + ) async { + try { + _logger.i('SpeedTestRepositoryImpl: createSpeedTestResult() called'); + final created = await _dataSource.createSpeedTestResult(result); + _logger.i('SpeedTestRepositoryImpl: Created result with id ${created.id}'); + return Right(created); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to create result: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + @override + Future> updateSpeedTestResult( + SpeedTestResult result, + ) async { + try { + _logger.i( + 'SpeedTestRepositoryImpl: updateSpeedTestResult(${result.id}) called', + ); + final updated = await _dataSource.updateSpeedTestResult(result); + return Right(updated); + } on Exception catch (e) { + _logger.e('SpeedTestRepositoryImpl: Failed to update result: $e'); + return Left(_mapExceptionToFailure(e)); + } + } + + // ============================================================================ + // Joined Operations + // ============================================================================ + + @override + Future> getSpeedTestWithResults( + int id, + ) async { + try { + _logger.i('SpeedTestRepositoryImpl: getSpeedTestWithResults($id) called'); + + // Fetch config and results in parallel + final configFuture = _dataSource.getSpeedTestConfig(id); + final resultsFuture = _dataSource.getSpeedTestResults(speedTestId: id); + + final config = await configFuture; + final results = await resultsFuture; + + final joined = SpeedTestWithResults( + config: config, + results: results, + ); + + _logger.i( + 'SpeedTestRepositoryImpl: Got config $id with ${results.length} results', + ); + return Right(joined); + } on Exception catch (e) { + _logger.e( + 'SpeedTestRepositoryImpl: Failed to get speed test with results: $e', + ); + return Left(_mapExceptionToFailure(e)); + } + } + + @override + Future>> + getAllSpeedTestsWithResults() async { + try { + _logger.i( + 'SpeedTestRepositoryImpl: getAllSpeedTestsWithResults() called', + ); + + // Fetch all configs and results + final configs = await _dataSource.getSpeedTestConfigs(); + final allResults = await _dataSource.getSpeedTestResults(); + + // Group results by speedTestId + final resultsByConfigId = >{}; + for (final result in allResults) { + if (result.speedTestId != null) { + resultsByConfigId + .putIfAbsent(result.speedTestId!, () => []) + .add(result); + } + } + + // Join configs with their results + final joined = configs.map((config) { + final results = config.id != null + ? (resultsByConfigId[config.id!] ?? []) + : []; + return SpeedTestWithResults(config: config, results: results); + }).toList(); + + _logger.i( + 'SpeedTestRepositoryImpl: Got ${joined.length} speed tests with results', + ); + return Right(joined); + } on Exception catch (e) { + _logger.e( + 'SpeedTestRepositoryImpl: Failed to get all speed tests with results: $e', + ); + return Left(_mapExceptionToFailure(e)); + } + } + + // ============================================================================ + // Helper Methods + // ============================================================================ + + Failure _mapExceptionToFailure(Exception exception) { + final message = exception.toString(); + + if (message.contains('not found') || message.contains('404')) { + return NotFoundFailure(message: message); + } else if (message.contains('not connected') || + message.contains('network')) { + return NetworkFailure(message: message); + } else if (message.contains('timeout')) { + return TimeoutFailure(message: message); + } else if (message.contains('server') || message.contains('500')) { + return ServerFailure(message: message); + } + + return ServerFailure(message: 'Speed test operation failed: $message'); + } +} diff --git a/lib/features/speed_test/data/services/speed_test_service.dart b/lib/features/speed_test/data/services/speed_test_service.dart index d35bfc0..0e94fc4 100644 --- a/lib/features/speed_test/data/services/speed_test_service.dart +++ b/lib/features/speed_test/data/services/speed_test_service.dart @@ -176,11 +176,11 @@ class SpeedTestService { // Create a partial result for live updates based on current phase // Preserve completed phase speed so UI shows both download AND upload final liveResult = SpeedTestResult( - downloadSpeed: + downloadMbps: _isDownloadPhase ? speedMbps : _completedDownloadSpeed, - uploadSpeed: !_isDownloadPhase ? speedMbps : _completedUploadSpeed, - latency: 0.0, - timestamp: DateTime.now(), + uploadMbps: !_isDownloadPhase ? speedMbps : _completedUploadSpeed, + rtt: 0.0, + completedAt: DateTime.now(), ); _resultController.add(liveResult); @@ -413,10 +413,10 @@ class SpeedTestService { // Create result return SpeedTestResult( - downloadSpeed: (downloadSpeed as num).toDouble(), - uploadSpeed: (uploadSpeed as num).toDouble(), - latency: (latency as num).toDouble(), - timestamp: DateTime.now(), + downloadMbps: (downloadSpeed as num).toDouble(), + uploadMbps: (uploadSpeed as num).toDouble(), + rtt: (latency as num).toDouble(), + completedAt: DateTime.now(), localIpAddress: localIp, serverHost: serverHost, ); diff --git a/lib/features/speed_test/domain/entities/speed_test_config.dart b/lib/features/speed_test/domain/entities/speed_test_config.dart index 7bf2bdb..bd5b5fe 100644 --- a/lib/features/speed_test/domain/entities/speed_test_config.dart +++ b/lib/features/speed_test/domain/entities/speed_test_config.dart @@ -3,31 +3,51 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'speed_test_config.freezed.dart'; part 'speed_test_config.g.dart'; +/// Safely converts a value to int, handling strings and nulls +int? _toInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is String) return int.tryParse(value); + return null; +} + +/// Safely converts a value to double, handling strings and nulls +double? _toDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value); + return null; +} + @freezed class SpeedTestConfig with _$SpeedTestConfig { const factory SpeedTestConfig({ - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, @Default(false) bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') @Default(false) bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, diff --git a/lib/features/speed_test/domain/entities/speed_test_config.freezed.dart b/lib/features/speed_test/domain/entities/speed_test_config.freezed.dart index 7e3fdaf..dc77a23 100644 --- a/lib/features/speed_test/domain/entities/speed_test_config.freezed.dart +++ b/lib/features/speed_test/domain/entities/speed_test_config.freezed.dart @@ -20,18 +20,21 @@ SpeedTestConfig _$SpeedTestConfigFromJson(Map json) { /// @nodoc mixin _$SpeedTestConfig { + @JsonKey(fromJson: _toInt) int? get id => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; @JsonKey(name: 'test_type') String? get testType => throw _privateConstructorUsedError; String? get target => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toInt) int? get port => throw _privateConstructorUsedError; @JsonKey(name: 'iperf_protocol') String? get iperfProtocol => throw _privateConstructorUsedError; - @JsonKey(name: 'min_download_mbps') + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) double? get minDownloadMbps => throw _privateConstructorUsedError; - @JsonKey(name: 'min_upload_mbps') + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) double? get minUploadMbps => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toInt) int? get period => throw _privateConstructorUsedError; @JsonKey(name: 'period_unit') String? get periodUnit => throw _privateConstructorUsedError; @@ -44,15 +47,15 @@ mixin _$SpeedTestConfig { bool get passing => throw _privateConstructorUsedError; @JsonKey(name: 'last_result') String? get lastResult => throw _privateConstructorUsedError; - @JsonKey(name: 'max_failures') + @JsonKey(name: 'max_failures', fromJson: _toInt) int? get maxFailures => throw _privateConstructorUsedError; @JsonKey(name: 'disable_uplink_on_failure') bool get disableUplinkOnFailure => throw _privateConstructorUsedError; - @JsonKey(name: 'sample_size_pct') + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) int? get sampleSizePct => throw _privateConstructorUsedError; @JsonKey(name: 'psk_override') String? get pskOverride => throw _privateConstructorUsedError; - @JsonKey(name: 'wlan_id') + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? get wlanId => throw _privateConstructorUsedError; String? get note => throw _privateConstructorUsedError; String? get scratch => throw _privateConstructorUsedError; @@ -67,27 +70,30 @@ mixin _$SpeedTestConfig { @optionalTypeArgs TResult when( TResult Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -100,27 +106,30 @@ mixin _$SpeedTestConfig { @optionalTypeArgs TResult? whenOrNull( TResult? Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -133,27 +142,30 @@ mixin _$SpeedTestConfig { @optionalTypeArgs TResult maybeWhen( TResult Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -193,26 +205,28 @@ abstract class $SpeedTestConfigCopyWith<$Res> { _$SpeedTestConfigCopyWithImpl<$Res, SpeedTestConfig>; @useResult $Res call( - {int? id, + {@JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -379,26 +393,28 @@ abstract class _$$SpeedTestConfigImplCopyWith<$Res> @override @useResult $Res call( - {int? id, + {@JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -558,27 +574,28 @@ class __$$SpeedTestConfigImplCopyWithImpl<$Res> @JsonSerializable() class _$SpeedTestConfigImpl extends _SpeedTestConfig { const _$SpeedTestConfigImpl( - {this.id, + {@JsonKey(fromJson: _toInt) this.id, this.name, @JsonKey(name: 'test_type') this.testType, this.target, - this.port, + @JsonKey(fromJson: _toInt) this.port, @JsonKey(name: 'iperf_protocol') this.iperfProtocol, - @JsonKey(name: 'min_download_mbps') this.minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') this.minUploadMbps, - this.period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + this.minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) this.minUploadMbps, + @JsonKey(fromJson: _toInt) this.period, @JsonKey(name: 'period_unit') this.periodUnit, @JsonKey(name: 'starts_at') this.startsAt, @JsonKey(name: 'next_check_at') this.nextCheckAt, @JsonKey(name: 'last_checked_at') this.lastCheckedAt, this.passing = false, @JsonKey(name: 'last_result') this.lastResult, - @JsonKey(name: 'max_failures') this.maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) this.maxFailures, @JsonKey(name: 'disable_uplink_on_failure') this.disableUplinkOnFailure = false, - @JsonKey(name: 'sample_size_pct') this.sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) this.sampleSizePct, @JsonKey(name: 'psk_override') this.pskOverride, - @JsonKey(name: 'wlan_id') this.wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) this.wlanId, this.note, this.scratch, @JsonKey(name: 'created_by') this.createdBy, @@ -591,6 +608,7 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { _$$SpeedTestConfigImplFromJson(json); @override + @JsonKey(fromJson: _toInt) final int? id; @override final String? name; @@ -600,17 +618,19 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { @override final String? target; @override + @JsonKey(fromJson: _toInt) final int? port; @override @JsonKey(name: 'iperf_protocol') final String? iperfProtocol; @override - @JsonKey(name: 'min_download_mbps') + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) final double? minDownloadMbps; @override - @JsonKey(name: 'min_upload_mbps') + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) final double? minUploadMbps; @override + @JsonKey(fromJson: _toInt) final int? period; @override @JsonKey(name: 'period_unit') @@ -631,19 +651,19 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { @JsonKey(name: 'last_result') final String? lastResult; @override - @JsonKey(name: 'max_failures') + @JsonKey(name: 'max_failures', fromJson: _toInt) final int? maxFailures; @override @JsonKey(name: 'disable_uplink_on_failure') final bool disableUplinkOnFailure; @override - @JsonKey(name: 'sample_size_pct') + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) final int? sampleSizePct; @override @JsonKey(name: 'psk_override') final String? pskOverride; @override - @JsonKey(name: 'wlan_id') + @JsonKey(name: 'wlan_id', fromJson: _toInt) final int? wlanId; @override final String? note; @@ -760,27 +780,30 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { @optionalTypeArgs TResult when( TResult Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -822,27 +845,30 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { @optionalTypeArgs TResult? whenOrNull( TResult? Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -884,27 +910,30 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { @optionalTypeArgs TResult maybeWhen( TResult Function( - int? id, + @JsonKey(fromJson: _toInt) int? id, String? name, @JsonKey(name: 'test_type') String? testType, String? target, - int? port, + @JsonKey(fromJson: _toInt) int? port, @JsonKey(name: 'iperf_protocol') String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') double? minUploadMbps, - int? period, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + double? minUploadMbps, + @JsonKey(fromJson: _toInt) int? period, @JsonKey(name: 'period_unit') String? periodUnit, @JsonKey(name: 'starts_at') DateTime? startsAt, @JsonKey(name: 'next_check_at') DateTime? nextCheckAt, @JsonKey(name: 'last_checked_at') DateTime? lastCheckedAt, bool passing, @JsonKey(name: 'last_result') String? lastResult, - @JsonKey(name: 'max_failures') int? maxFailures, + @JsonKey(name: 'max_failures', fromJson: _toInt) int? maxFailures, @JsonKey(name: 'disable_uplink_on_failure') bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') int? sampleSizePct, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + int? sampleSizePct, @JsonKey(name: 'psk_override') String? pskOverride, - @JsonKey(name: 'wlan_id') int? wlanId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, String? note, String? scratch, @JsonKey(name: 'created_by') String? createdBy, @@ -984,40 +1013,44 @@ class _$SpeedTestConfigImpl extends _SpeedTestConfig { abstract class _SpeedTestConfig extends SpeedTestConfig { const factory _SpeedTestConfig( - {final int? id, - final String? name, - @JsonKey(name: 'test_type') final String? testType, - final String? target, - final int? port, - @JsonKey(name: 'iperf_protocol') final String? iperfProtocol, - @JsonKey(name: 'min_download_mbps') final double? minDownloadMbps, - @JsonKey(name: 'min_upload_mbps') final double? minUploadMbps, - final int? period, - @JsonKey(name: 'period_unit') final String? periodUnit, - @JsonKey(name: 'starts_at') final DateTime? startsAt, - @JsonKey(name: 'next_check_at') final DateTime? nextCheckAt, - @JsonKey(name: 'last_checked_at') final DateTime? lastCheckedAt, - final bool passing, - @JsonKey(name: 'last_result') final String? lastResult, - @JsonKey(name: 'max_failures') final int? maxFailures, - @JsonKey(name: 'disable_uplink_on_failure') - final bool disableUplinkOnFailure, - @JsonKey(name: 'sample_size_pct') final int? sampleSizePct, - @JsonKey(name: 'psk_override') final String? pskOverride, - @JsonKey(name: 'wlan_id') final int? wlanId, - final String? note, - final String? scratch, - @JsonKey(name: 'created_by') final String? createdBy, - @JsonKey(name: 'updated_by') final String? updatedBy, - @JsonKey(name: 'created_at') final DateTime? createdAt, - @JsonKey(name: 'updated_at') final DateTime? updatedAt}) = - _$SpeedTestConfigImpl; + {@JsonKey(fromJson: _toInt) final int? id, + final String? name, + @JsonKey(name: 'test_type') final String? testType, + final String? target, + @JsonKey(fromJson: _toInt) final int? port, + @JsonKey(name: 'iperf_protocol') final String? iperfProtocol, + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) + final double? minDownloadMbps, + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) + final double? minUploadMbps, + @JsonKey(fromJson: _toInt) final int? period, + @JsonKey(name: 'period_unit') final String? periodUnit, + @JsonKey(name: 'starts_at') final DateTime? startsAt, + @JsonKey(name: 'next_check_at') final DateTime? nextCheckAt, + @JsonKey(name: 'last_checked_at') final DateTime? lastCheckedAt, + final bool passing, + @JsonKey(name: 'last_result') final String? lastResult, + @JsonKey(name: 'max_failures', fromJson: _toInt) final int? maxFailures, + @JsonKey(name: 'disable_uplink_on_failure') + final bool disableUplinkOnFailure, + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) + final int? sampleSizePct, + @JsonKey(name: 'psk_override') final String? pskOverride, + @JsonKey(name: 'wlan_id', fromJson: _toInt) final int? wlanId, + final String? note, + final String? scratch, + @JsonKey(name: 'created_by') final String? createdBy, + @JsonKey(name: 'updated_by') final String? updatedBy, + @JsonKey(name: 'created_at') final DateTime? createdAt, + @JsonKey(name: 'updated_at') + final DateTime? updatedAt}) = _$SpeedTestConfigImpl; const _SpeedTestConfig._() : super._(); factory _SpeedTestConfig.fromJson(Map json) = _$SpeedTestConfigImpl.fromJson; @override + @JsonKey(fromJson: _toInt) int? get id; @override String? get name; @@ -1027,17 +1060,19 @@ abstract class _SpeedTestConfig extends SpeedTestConfig { @override String? get target; @override + @JsonKey(fromJson: _toInt) int? get port; @override @JsonKey(name: 'iperf_protocol') String? get iperfProtocol; @override - @JsonKey(name: 'min_download_mbps') + @JsonKey(name: 'min_download_mbps', fromJson: _toDouble) double? get minDownloadMbps; @override - @JsonKey(name: 'min_upload_mbps') + @JsonKey(name: 'min_upload_mbps', fromJson: _toDouble) double? get minUploadMbps; @override + @JsonKey(fromJson: _toInt) int? get period; @override @JsonKey(name: 'period_unit') @@ -1057,19 +1092,19 @@ abstract class _SpeedTestConfig extends SpeedTestConfig { @JsonKey(name: 'last_result') String? get lastResult; @override - @JsonKey(name: 'max_failures') + @JsonKey(name: 'max_failures', fromJson: _toInt) int? get maxFailures; @override @JsonKey(name: 'disable_uplink_on_failure') bool get disableUplinkOnFailure; @override - @JsonKey(name: 'sample_size_pct') + @JsonKey(name: 'sample_size_pct', fromJson: _toInt) int? get sampleSizePct; @override @JsonKey(name: 'psk_override') String? get pskOverride; @override - @JsonKey(name: 'wlan_id') + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? get wlanId; @override String? get note; diff --git a/lib/features/speed_test/domain/entities/speed_test_config.g.dart b/lib/features/speed_test/domain/entities/speed_test_config.g.dart index b927236..6e45d66 100644 --- a/lib/features/speed_test/domain/entities/speed_test_config.g.dart +++ b/lib/features/speed_test/domain/entities/speed_test_config.g.dart @@ -9,15 +9,15 @@ part of 'speed_test_config.dart'; _$SpeedTestConfigImpl _$$SpeedTestConfigImplFromJson( Map json) => _$SpeedTestConfigImpl( - id: (json['id'] as num?)?.toInt(), + id: _toInt(json['id']), name: json['name'] as String?, testType: json['test_type'] as String?, target: json['target'] as String?, - port: (json['port'] as num?)?.toInt(), + port: _toInt(json['port']), iperfProtocol: json['iperf_protocol'] as String?, - minDownloadMbps: (json['min_download_mbps'] as num?)?.toDouble(), - minUploadMbps: (json['min_upload_mbps'] as num?)?.toDouble(), - period: (json['period'] as num?)?.toInt(), + minDownloadMbps: _toDouble(json['min_download_mbps']), + minUploadMbps: _toDouble(json['min_upload_mbps']), + period: _toInt(json['period']), periodUnit: json['period_unit'] as String?, startsAt: json['starts_at'] == null ? null @@ -30,12 +30,12 @@ _$SpeedTestConfigImpl _$$SpeedTestConfigImplFromJson( : DateTime.parse(json['last_checked_at'] as String), passing: json['passing'] as bool? ?? false, lastResult: json['last_result'] as String?, - maxFailures: (json['max_failures'] as num?)?.toInt(), + maxFailures: _toInt(json['max_failures']), disableUplinkOnFailure: json['disable_uplink_on_failure'] as bool? ?? false, - sampleSizePct: (json['sample_size_pct'] as num?)?.toInt(), + sampleSizePct: _toInt(json['sample_size_pct']), pskOverride: json['psk_override'] as String?, - wlanId: (json['wlan_id'] as num?)?.toInt(), + wlanId: _toInt(json['wlan_id']), note: json['note'] as String?, scratch: json['scratch'] as String?, createdBy: json['created_by'] as String?, diff --git a/lib/features/speed_test/domain/entities/speed_test_result.dart b/lib/features/speed_test/domain/entities/speed_test_result.dart index f1a876e..ace229b 100644 --- a/lib/features/speed_test/domain/entities/speed_test_result.dart +++ b/lib/features/speed_test/domain/entities/speed_test_result.dart @@ -1,55 +1,249 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; part 'speed_test_result.freezed.dart'; part 'speed_test_result.g.dart'; +/// Safely converts a value to int, handling strings and nulls +int? _toInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is String) return int.tryParse(value); + return null; +} + +/// Safely converts a value to double, handling strings and nulls +double? _toDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value); + return null; +} + @freezed class SpeedTestResult with _$SpeedTestResult { const factory SpeedTestResult({ - required double downloadSpeed, - required double uploadSpeed, - required double latency, - required DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) double? packetLoss, + @Default(false) bool passed, + @JsonKey(name: 'is_applicable') @Default(true) bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + // Legacy fields for backwards compatibility @Default(false) bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost, + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost, }) = _SpeedTestResult; const SpeedTestResult._(); + /// Standard fromJson generated by json_serializable factory SpeedTestResult.fromJson(Map json) => _$SpeedTestResultFromJson(json); + /// FromJson with validation that corrects potentially swapped speeds + /// Use this when parsing data from the API to ensure correct values + static SpeedTestResult fromJsonWithValidation(Map json) { + // Pre-process the JSON to fix swapped speeds before Freezed parsing + final processedJson = _preprocessJson(json); + return _$SpeedTestResultFromJson(processedJson); + } + + /// Factory for creating an error result factory SpeedTestResult.error(String message) { return SpeedTestResult( - downloadSpeed: 0, - uploadSpeed: 0, - latency: 0, - timestamp: DateTime.now(), hasError: true, errorMessage: message, + passed: false, ); } + /// Pre-process JSON to detect and correct swapped download/upload values + static Map _preprocessJson(Map json) { + final normalizedJson = _normalizeTestedViaAccessPointId(json); + final download = _parseDecimal(normalizedJson['download_mbps']); + final upload = _parseDecimal(normalizedJson['upload_mbps']); + + if (download == null || upload == null) { + return json; + } + + // Both are 0 - likely incomplete test, don't swap + if (download == 0 && upload == 0) { + return json; + } + + bool shouldSwap = false; + String? reason; + + // Heuristic 1: Download is suspiciously low AND upload is suspiciously high + if (download < 5.0 && upload > 50.0) { + shouldSwap = true; + reason = 'download too low (<5) and upload too high (>50)'; + } + // Heuristic 2: Upload is significantly higher than download (10x or more) + else if (download > 0 && upload > download * 10) { + shouldSwap = true; + reason = 'upload is 10x higher than download'; + } + + if (shouldSwap) { + LoggerService.warning( + 'Detected swapped speeds in result ${json['id'] ?? "unknown"}: ' + '$reason. Original: down=$download up=$upload, ' + 'Corrected: down=$upload up=$download', + tag: 'SpeedTestResult', + ); + // Create a new map with swapped values + return { + ...normalizedJson, + 'download_mbps': upload, + 'upload_mbps': download, + }; + } + + return normalizedJson; + } + + static Map _normalizeTestedViaAccessPointId( + Map json, + ) { + final value = json['tested_via_access_point_id']; + if (value is String) { + final parsed = int.tryParse(value); + if (parsed != null) { + return { + ...json, + 'tested_via_access_point_id': parsed, + }; + } + } + return json; + } + + /// Helper to parse decimal values from various formats + static double? _parseDecimal(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value); + return null; + } + + /// Check if this is an iperf3 test + bool get isIperfTest => + testType?.toLowerCase() == 'iperf3' || testType?.toLowerCase() == 'iperf'; + + /// Check if this uses UDP protocol + bool get isUdp => iperfProtocol?.toLowerCase() == 'udp'; + + /// Check if this uses TCP protocol + bool get isTcp => iperfProtocol?.toLowerCase() == 'tcp'; + + /// Get test duration if both timestamps are available + Duration? get testDuration { + if (initiatedAt == null || completedAt == null) return null; + return completedAt!.difference(initiatedAt!); + } + + /// Check if test is completed + bool get isCompleted => completedAt != null; + + /// Check if test is still running + bool get isRunning => initiatedAt != null && completedAt == null; + + /// Check if test has good performance (passed and applicable) + bool get isHealthy => passed && isApplicable; + + /// Get average speed (mean of download and upload) + double? get averageSpeedMbps { + if (downloadMbps == null && uploadMbps == null) return null; + final down = downloadMbps ?? 0.0; + final up = uploadMbps ?? 0.0; + return (down + up) / 2; + } + /// Get formatted download speed String get formattedDownloadSpeed { - if (downloadSpeed < 1000.0) { - return '${downloadSpeed.toStringAsFixed(2)} Mbps'; - } else { - return '${(downloadSpeed / 1000).toStringAsFixed(2)} Gbps'; + if (downloadMbps == null) return 'N/A'; + if (downloadMbps! < 1000.0) { + return '${downloadMbps!.toStringAsFixed(2)} Mbps'; } + return '${(downloadMbps! / 1000).toStringAsFixed(2)} Gbps'; } /// Get formatted upload speed String get formattedUploadSpeed { - if (uploadSpeed < 1000.0) { - return '${uploadSpeed.toStringAsFixed(2)} Mbps'; - } else { - return '${(uploadSpeed / 1000).toStringAsFixed(2)} Gbps'; + if (uploadMbps == null) return 'N/A'; + if (uploadMbps! < 1000.0) { + return '${uploadMbps!.toStringAsFixed(2)} Mbps'; } + return '${(uploadMbps! / 1000).toStringAsFixed(2)} Gbps'; + } + + /// Get formatted RTT (Round Trip Time) + String get formattedRtt { + if (rtt == null) return 'N/A'; + return '${rtt!.toStringAsFixed(2)} ms'; } - /// Get formatted latency - String get formattedLatency => '${latency.toStringAsFixed(0)} ms'; + /// Get formatted jitter + String get formattedJitter { + if (jitter == null) return 'N/A'; + return '${jitter!.toStringAsFixed(2)} ms'; + } + + /// Get formatted packet loss + String get formattedPacketLoss { + if (packetLoss == null) return 'N/A'; + return '${packetLoss!.toStringAsFixed(2)}%'; + } + + /// Get formatted latency (alias for RTT for backwards compatibility) + String get formattedLatency => formattedRtt; + + /// Legacy getter for downloadSpeed (maps to downloadMbps) + double get downloadSpeed => downloadMbps ?? 0.0; + + /// Legacy getter for uploadSpeed (maps to uploadMbps) + double get uploadSpeed => uploadMbps ?? 0.0; + + /// Legacy getter for latency (maps to rtt) + double get latency => rtt ?? 0.0; + + /// Legacy getter for timestamp (maps to completedAt or createdAt) + DateTime get timestamp => completedAt ?? createdAt ?? DateTime.now(); } diff --git a/lib/features/speed_test/domain/entities/speed_test_result.freezed.dart b/lib/features/speed_test/domain/entities/speed_test_result.freezed.dart index 510272d..9eedc6a 100644 --- a/lib/features/speed_test/domain/entities/speed_test_result.freezed.dart +++ b/lib/features/speed_test/domain/entities/speed_test_result.freezed.dart @@ -20,53 +20,220 @@ SpeedTestResult _$SpeedTestResultFromJson(Map json) { /// @nodoc mixin _$SpeedTestResult { - double get downloadSpeed => throw _privateConstructorUsedError; - double get uploadSpeed => throw _privateConstructorUsedError; - double get latency => throw _privateConstructorUsedError; - DateTime get timestamp => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toInt) + int? get id => throw _privateConstructorUsedError; + @JsonKey(name: 'speed_test_id', fromJson: _toInt) + int? get speedTestId => throw _privateConstructorUsedError; + @JsonKey(name: 'test_type') + String? get testType => throw _privateConstructorUsedError; + String? get source => throw _privateConstructorUsedError; + String? get destination => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toInt) + int? get port => throw _privateConstructorUsedError; + @JsonKey(name: 'iperf_protocol') + String? get iperfProtocol => throw _privateConstructorUsedError; + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? get downloadMbps => throw _privateConstructorUsedError; + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? get uploadMbps => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toDouble) + double? get rtt => throw _privateConstructorUsedError; + @JsonKey(fromJson: _toDouble) + double? get jitter => throw _privateConstructorUsedError; + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? get packetLoss => throw _privateConstructorUsedError; + bool get passed => throw _privateConstructorUsedError; + @JsonKey(name: 'is_applicable') + bool get isApplicable => throw _privateConstructorUsedError; + @JsonKey(name: 'initiated_at') + DateTime? get initiatedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'completed_at') + DateTime? get completedAt => throw _privateConstructorUsedError; + String? get raw => throw _privateConstructorUsedError; + @JsonKey(name: 'image_url') + String? get imageUrl => throw _privateConstructorUsedError; + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? get accessPointId => throw _privateConstructorUsedError; + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? get testedViaAccessPointId => throw _privateConstructorUsedError; + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? get testedViaAccessPointRadioId => throw _privateConstructorUsedError; + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? get testedViaMediaConverterId => throw _privateConstructorUsedError; + @JsonKey(name: 'uplink_id', fromJson: _toInt) + int? get uplinkId => throw _privateConstructorUsedError; + @JsonKey(name: 'wlan_id', fromJson: _toInt) + int? get wlanId => throw _privateConstructorUsedError; + @JsonKey(name: 'pms_room_id', fromJson: _toInt) + int? get pmsRoomId => throw _privateConstructorUsedError; + @JsonKey(name: 'room_type') + String? get roomType => throw _privateConstructorUsedError; + @JsonKey(name: 'admin_id', fromJson: _toInt) + int? get adminId => throw _privateConstructorUsedError; + String? get note => throw _privateConstructorUsedError; + String? get scratch => throw _privateConstructorUsedError; + @JsonKey(name: 'created_by') + String? get createdBy => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_by') + String? get updatedBy => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + DateTime? get updatedAt => + throw _privateConstructorUsedError; // Legacy fields for backwards compatibility bool get hasError => throw _privateConstructorUsedError; String? get errorMessage => throw _privateConstructorUsedError; + @JsonKey(name: 'local_ip_address') String? get localIpAddress => throw _privateConstructorUsedError; + @JsonKey(name: 'server_host') String? get serverHost => throw _privateConstructorUsedError; @optionalTypeArgs TResult when( TResult Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost) + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost) $default, ) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull( TResult? Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost)? + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost)? $default, ) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen( TResult Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost)? + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost)? $default, { required TResult orElse(), }) => @@ -100,14 +267,46 @@ abstract class $SpeedTestResultCopyWith<$Res> { _$SpeedTestResultCopyWithImpl<$Res, SpeedTestResult>; @useResult $Res call( - {double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + {@JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost}); + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost}); } /// @nodoc @@ -123,32 +322,177 @@ class _$SpeedTestResultCopyWithImpl<$Res, $Val extends SpeedTestResult> @pragma('vm:prefer-inline') @override $Res call({ - Object? downloadSpeed = null, - Object? uploadSpeed = null, - Object? latency = null, - Object? timestamp = null, + Object? id = freezed, + Object? speedTestId = freezed, + Object? testType = freezed, + Object? source = freezed, + Object? destination = freezed, + Object? port = freezed, + Object? iperfProtocol = freezed, + Object? downloadMbps = freezed, + Object? uploadMbps = freezed, + Object? rtt = freezed, + Object? jitter = freezed, + Object? packetLoss = freezed, + Object? passed = null, + Object? isApplicable = null, + Object? initiatedAt = freezed, + Object? completedAt = freezed, + Object? raw = freezed, + Object? imageUrl = freezed, + Object? accessPointId = freezed, + Object? testedViaAccessPointId = freezed, + Object? testedViaAccessPointRadioId = freezed, + Object? testedViaMediaConverterId = freezed, + Object? uplinkId = freezed, + Object? wlanId = freezed, + Object? pmsRoomId = freezed, + Object? roomType = freezed, + Object? adminId = freezed, + Object? note = freezed, + Object? scratch = freezed, + Object? createdBy = freezed, + Object? updatedBy = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, Object? hasError = null, Object? errorMessage = freezed, Object? localIpAddress = freezed, Object? serverHost = freezed, }) { return _then(_value.copyWith( - downloadSpeed: null == downloadSpeed - ? _value.downloadSpeed - : downloadSpeed // ignore: cast_nullable_to_non_nullable - as double, - uploadSpeed: null == uploadSpeed - ? _value.uploadSpeed - : uploadSpeed // ignore: cast_nullable_to_non_nullable - as double, - latency: null == latency - ? _value.latency - : latency // ignore: cast_nullable_to_non_nullable - as double, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable - as DateTime, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + speedTestId: freezed == speedTestId + ? _value.speedTestId + : speedTestId // ignore: cast_nullable_to_non_nullable + as int?, + testType: freezed == testType + ? _value.testType + : testType // ignore: cast_nullable_to_non_nullable + as String?, + source: freezed == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String?, + destination: freezed == destination + ? _value.destination + : destination // ignore: cast_nullable_to_non_nullable + as String?, + port: freezed == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int?, + iperfProtocol: freezed == iperfProtocol + ? _value.iperfProtocol + : iperfProtocol // ignore: cast_nullable_to_non_nullable + as String?, + downloadMbps: freezed == downloadMbps + ? _value.downloadMbps + : downloadMbps // ignore: cast_nullable_to_non_nullable + as double?, + uploadMbps: freezed == uploadMbps + ? _value.uploadMbps + : uploadMbps // ignore: cast_nullable_to_non_nullable + as double?, + rtt: freezed == rtt + ? _value.rtt + : rtt // ignore: cast_nullable_to_non_nullable + as double?, + jitter: freezed == jitter + ? _value.jitter + : jitter // ignore: cast_nullable_to_non_nullable + as double?, + packetLoss: freezed == packetLoss + ? _value.packetLoss + : packetLoss // ignore: cast_nullable_to_non_nullable + as double?, + passed: null == passed + ? _value.passed + : passed // ignore: cast_nullable_to_non_nullable + as bool, + isApplicable: null == isApplicable + ? _value.isApplicable + : isApplicable // ignore: cast_nullable_to_non_nullable + as bool, + initiatedAt: freezed == initiatedAt + ? _value.initiatedAt + : initiatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + completedAt: freezed == completedAt + ? _value.completedAt + : completedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + raw: freezed == raw + ? _value.raw + : raw // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, + accessPointId: freezed == accessPointId + ? _value.accessPointId + : accessPointId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaAccessPointId: freezed == testedViaAccessPointId + ? _value.testedViaAccessPointId + : testedViaAccessPointId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaAccessPointRadioId: freezed == testedViaAccessPointRadioId + ? _value.testedViaAccessPointRadioId + : testedViaAccessPointRadioId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaMediaConverterId: freezed == testedViaMediaConverterId + ? _value.testedViaMediaConverterId + : testedViaMediaConverterId // ignore: cast_nullable_to_non_nullable + as int?, + uplinkId: freezed == uplinkId + ? _value.uplinkId + : uplinkId // ignore: cast_nullable_to_non_nullable + as int?, + wlanId: freezed == wlanId + ? _value.wlanId + : wlanId // ignore: cast_nullable_to_non_nullable + as int?, + pmsRoomId: freezed == pmsRoomId + ? _value.pmsRoomId + : pmsRoomId // ignore: cast_nullable_to_non_nullable + as int?, + roomType: freezed == roomType + ? _value.roomType + : roomType // ignore: cast_nullable_to_non_nullable + as String?, + adminId: freezed == adminId + ? _value.adminId + : adminId // ignore: cast_nullable_to_non_nullable + as int?, + note: freezed == note + ? _value.note + : note // ignore: cast_nullable_to_non_nullable + as String?, + scratch: freezed == scratch + ? _value.scratch + : scratch // ignore: cast_nullable_to_non_nullable + as String?, + createdBy: freezed == createdBy + ? _value.createdBy + : createdBy // ignore: cast_nullable_to_non_nullable + as String?, + updatedBy: freezed == updatedBy + ? _value.updatedBy + : updatedBy // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, hasError: null == hasError ? _value.hasError : hasError // ignore: cast_nullable_to_non_nullable @@ -178,14 +522,46 @@ abstract class _$$SpeedTestResultImplCopyWith<$Res> @override @useResult $Res call( - {double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + {@JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost}); + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost}); } /// @nodoc @@ -199,32 +575,177 @@ class __$$SpeedTestResultImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? downloadSpeed = null, - Object? uploadSpeed = null, - Object? latency = null, - Object? timestamp = null, + Object? id = freezed, + Object? speedTestId = freezed, + Object? testType = freezed, + Object? source = freezed, + Object? destination = freezed, + Object? port = freezed, + Object? iperfProtocol = freezed, + Object? downloadMbps = freezed, + Object? uploadMbps = freezed, + Object? rtt = freezed, + Object? jitter = freezed, + Object? packetLoss = freezed, + Object? passed = null, + Object? isApplicable = null, + Object? initiatedAt = freezed, + Object? completedAt = freezed, + Object? raw = freezed, + Object? imageUrl = freezed, + Object? accessPointId = freezed, + Object? testedViaAccessPointId = freezed, + Object? testedViaAccessPointRadioId = freezed, + Object? testedViaMediaConverterId = freezed, + Object? uplinkId = freezed, + Object? wlanId = freezed, + Object? pmsRoomId = freezed, + Object? roomType = freezed, + Object? adminId = freezed, + Object? note = freezed, + Object? scratch = freezed, + Object? createdBy = freezed, + Object? updatedBy = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, Object? hasError = null, Object? errorMessage = freezed, Object? localIpAddress = freezed, Object? serverHost = freezed, }) { return _then(_$SpeedTestResultImpl( - downloadSpeed: null == downloadSpeed - ? _value.downloadSpeed - : downloadSpeed // ignore: cast_nullable_to_non_nullable - as double, - uploadSpeed: null == uploadSpeed - ? _value.uploadSpeed - : uploadSpeed // ignore: cast_nullable_to_non_nullable - as double, - latency: null == latency - ? _value.latency - : latency // ignore: cast_nullable_to_non_nullable - as double, - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable - as DateTime, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + speedTestId: freezed == speedTestId + ? _value.speedTestId + : speedTestId // ignore: cast_nullable_to_non_nullable + as int?, + testType: freezed == testType + ? _value.testType + : testType // ignore: cast_nullable_to_non_nullable + as String?, + source: freezed == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String?, + destination: freezed == destination + ? _value.destination + : destination // ignore: cast_nullable_to_non_nullable + as String?, + port: freezed == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int?, + iperfProtocol: freezed == iperfProtocol + ? _value.iperfProtocol + : iperfProtocol // ignore: cast_nullable_to_non_nullable + as String?, + downloadMbps: freezed == downloadMbps + ? _value.downloadMbps + : downloadMbps // ignore: cast_nullable_to_non_nullable + as double?, + uploadMbps: freezed == uploadMbps + ? _value.uploadMbps + : uploadMbps // ignore: cast_nullable_to_non_nullable + as double?, + rtt: freezed == rtt + ? _value.rtt + : rtt // ignore: cast_nullable_to_non_nullable + as double?, + jitter: freezed == jitter + ? _value.jitter + : jitter // ignore: cast_nullable_to_non_nullable + as double?, + packetLoss: freezed == packetLoss + ? _value.packetLoss + : packetLoss // ignore: cast_nullable_to_non_nullable + as double?, + passed: null == passed + ? _value.passed + : passed // ignore: cast_nullable_to_non_nullable + as bool, + isApplicable: null == isApplicable + ? _value.isApplicable + : isApplicable // ignore: cast_nullable_to_non_nullable + as bool, + initiatedAt: freezed == initiatedAt + ? _value.initiatedAt + : initiatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + completedAt: freezed == completedAt + ? _value.completedAt + : completedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + raw: freezed == raw + ? _value.raw + : raw // ignore: cast_nullable_to_non_nullable + as String?, + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, + accessPointId: freezed == accessPointId + ? _value.accessPointId + : accessPointId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaAccessPointId: freezed == testedViaAccessPointId + ? _value.testedViaAccessPointId + : testedViaAccessPointId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaAccessPointRadioId: freezed == testedViaAccessPointRadioId + ? _value.testedViaAccessPointRadioId + : testedViaAccessPointRadioId // ignore: cast_nullable_to_non_nullable + as int?, + testedViaMediaConverterId: freezed == testedViaMediaConverterId + ? _value.testedViaMediaConverterId + : testedViaMediaConverterId // ignore: cast_nullable_to_non_nullable + as int?, + uplinkId: freezed == uplinkId + ? _value.uplinkId + : uplinkId // ignore: cast_nullable_to_non_nullable + as int?, + wlanId: freezed == wlanId + ? _value.wlanId + : wlanId // ignore: cast_nullable_to_non_nullable + as int?, + pmsRoomId: freezed == pmsRoomId + ? _value.pmsRoomId + : pmsRoomId // ignore: cast_nullable_to_non_nullable + as int?, + roomType: freezed == roomType + ? _value.roomType + : roomType // ignore: cast_nullable_to_non_nullable + as String?, + adminId: freezed == adminId + ? _value.adminId + : adminId // ignore: cast_nullable_to_non_nullable + as int?, + note: freezed == note + ? _value.note + : note // ignore: cast_nullable_to_non_nullable + as String?, + scratch: freezed == scratch + ? _value.scratch + : scratch // ignore: cast_nullable_to_non_nullable + as String?, + createdBy: freezed == createdBy + ? _value.createdBy + : createdBy // ignore: cast_nullable_to_non_nullable + as String?, + updatedBy: freezed == updatedBy + ? _value.updatedBy + : updatedBy // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, hasError: null == hasError ? _value.hasError : hasError // ignore: cast_nullable_to_non_nullable @@ -249,40 +770,161 @@ class __$$SpeedTestResultImplCopyWithImpl<$Res> @JsonSerializable() class _$SpeedTestResultImpl extends _SpeedTestResult { const _$SpeedTestResultImpl( - {required this.downloadSpeed, - required this.uploadSpeed, - required this.latency, - required this.timestamp, + {@JsonKey(fromJson: _toInt) this.id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) this.speedTestId, + @JsonKey(name: 'test_type') this.testType, + this.source, + this.destination, + @JsonKey(fromJson: _toInt) this.port, + @JsonKey(name: 'iperf_protocol') this.iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) this.downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) this.uploadMbps, + @JsonKey(fromJson: _toDouble) this.rtt, + @JsonKey(fromJson: _toDouble) this.jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) this.packetLoss, + this.passed = false, + @JsonKey(name: 'is_applicable') this.isApplicable = true, + @JsonKey(name: 'initiated_at') this.initiatedAt, + @JsonKey(name: 'completed_at') this.completedAt, + this.raw, + @JsonKey(name: 'image_url') this.imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) this.accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + this.testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + this.testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + this.testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) this.uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) this.wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) this.pmsRoomId, + @JsonKey(name: 'room_type') this.roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) this.adminId, + this.note, + this.scratch, + @JsonKey(name: 'created_by') this.createdBy, + @JsonKey(name: 'updated_by') this.updatedBy, + @JsonKey(name: 'created_at') this.createdAt, + @JsonKey(name: 'updated_at') this.updatedAt, this.hasError = false, this.errorMessage, - this.localIpAddress, - this.serverHost}) + @JsonKey(name: 'local_ip_address') this.localIpAddress, + @JsonKey(name: 'server_host') this.serverHost}) : super._(); factory _$SpeedTestResultImpl.fromJson(Map json) => _$$SpeedTestResultImplFromJson(json); @override - final double downloadSpeed; + @JsonKey(fromJson: _toInt) + final int? id; + @override + @JsonKey(name: 'speed_test_id', fromJson: _toInt) + final int? speedTestId; + @override + @JsonKey(name: 'test_type') + final String? testType; + @override + final String? source; + @override + final String? destination; + @override + @JsonKey(fromJson: _toInt) + final int? port; + @override + @JsonKey(name: 'iperf_protocol') + final String? iperfProtocol; + @override + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + final double? downloadMbps; + @override + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + final double? uploadMbps; + @override + @JsonKey(fromJson: _toDouble) + final double? rtt; + @override + @JsonKey(fromJson: _toDouble) + final double? jitter; + @override + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + final double? packetLoss; + @override + @JsonKey() + final bool passed; + @override + @JsonKey(name: 'is_applicable') + final bool isApplicable; + @override + @JsonKey(name: 'initiated_at') + final DateTime? initiatedAt; + @override + @JsonKey(name: 'completed_at') + final DateTime? completedAt; @override - final double uploadSpeed; + final String? raw; @override - final double latency; + @JsonKey(name: 'image_url') + final String? imageUrl; @override - final DateTime timestamp; + @JsonKey(name: 'access_point_id', fromJson: _toInt) + final int? accessPointId; + @override + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + final int? testedViaAccessPointId; + @override + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + final int? testedViaAccessPointRadioId; + @override + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + final int? testedViaMediaConverterId; + @override + @JsonKey(name: 'uplink_id', fromJson: _toInt) + final int? uplinkId; + @override + @JsonKey(name: 'wlan_id', fromJson: _toInt) + final int? wlanId; + @override + @JsonKey(name: 'pms_room_id', fromJson: _toInt) + final int? pmsRoomId; + @override + @JsonKey(name: 'room_type') + final String? roomType; + @override + @JsonKey(name: 'admin_id', fromJson: _toInt) + final int? adminId; + @override + final String? note; + @override + final String? scratch; + @override + @JsonKey(name: 'created_by') + final String? createdBy; + @override + @JsonKey(name: 'updated_by') + final String? updatedBy; + @override + @JsonKey(name: 'created_at') + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at') + final DateTime? updatedAt; +// Legacy fields for backwards compatibility @override @JsonKey() final bool hasError; @override final String? errorMessage; @override + @JsonKey(name: 'local_ip_address') final String? localIpAddress; @override + @JsonKey(name: 'server_host') final String? serverHost; @override String toString() { - return 'SpeedTestResult(downloadSpeed: $downloadSpeed, uploadSpeed: $uploadSpeed, latency: $latency, timestamp: $timestamp, hasError: $hasError, errorMessage: $errorMessage, localIpAddress: $localIpAddress, serverHost: $serverHost)'; + return 'SpeedTestResult(id: $id, speedTestId: $speedTestId, testType: $testType, source: $source, destination: $destination, port: $port, iperfProtocol: $iperfProtocol, downloadMbps: $downloadMbps, uploadMbps: $uploadMbps, rtt: $rtt, jitter: $jitter, packetLoss: $packetLoss, passed: $passed, isApplicable: $isApplicable, initiatedAt: $initiatedAt, completedAt: $completedAt, raw: $raw, imageUrl: $imageUrl, accessPointId: $accessPointId, testedViaAccessPointId: $testedViaAccessPointId, testedViaAccessPointRadioId: $testedViaAccessPointRadioId, testedViaMediaConverterId: $testedViaMediaConverterId, uplinkId: $uplinkId, wlanId: $wlanId, pmsRoomId: $pmsRoomId, roomType: $roomType, adminId: $adminId, note: $note, scratch: $scratch, createdBy: $createdBy, updatedBy: $updatedBy, createdAt: $createdAt, updatedAt: $updatedAt, hasError: $hasError, errorMessage: $errorMessage, localIpAddress: $localIpAddress, serverHost: $serverHost)'; } @override @@ -290,13 +932,64 @@ class _$SpeedTestResultImpl extends _SpeedTestResult { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SpeedTestResultImpl && - (identical(other.downloadSpeed, downloadSpeed) || - other.downloadSpeed == downloadSpeed) && - (identical(other.uploadSpeed, uploadSpeed) || - other.uploadSpeed == uploadSpeed) && - (identical(other.latency, latency) || other.latency == latency) && - (identical(other.timestamp, timestamp) || - other.timestamp == timestamp) && + (identical(other.id, id) || other.id == id) && + (identical(other.speedTestId, speedTestId) || + other.speedTestId == speedTestId) && + (identical(other.testType, testType) || + other.testType == testType) && + (identical(other.source, source) || other.source == source) && + (identical(other.destination, destination) || + other.destination == destination) && + (identical(other.port, port) || other.port == port) && + (identical(other.iperfProtocol, iperfProtocol) || + other.iperfProtocol == iperfProtocol) && + (identical(other.downloadMbps, downloadMbps) || + other.downloadMbps == downloadMbps) && + (identical(other.uploadMbps, uploadMbps) || + other.uploadMbps == uploadMbps) && + (identical(other.rtt, rtt) || other.rtt == rtt) && + (identical(other.jitter, jitter) || other.jitter == jitter) && + (identical(other.packetLoss, packetLoss) || + other.packetLoss == packetLoss) && + (identical(other.passed, passed) || other.passed == passed) && + (identical(other.isApplicable, isApplicable) || + other.isApplicable == isApplicable) && + (identical(other.initiatedAt, initiatedAt) || + other.initiatedAt == initiatedAt) && + (identical(other.completedAt, completedAt) || + other.completedAt == completedAt) && + (identical(other.raw, raw) || other.raw == raw) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl) && + (identical(other.accessPointId, accessPointId) || + other.accessPointId == accessPointId) && + (identical(other.testedViaAccessPointId, testedViaAccessPointId) || + other.testedViaAccessPointId == testedViaAccessPointId) && + (identical(other.testedViaAccessPointRadioId, + testedViaAccessPointRadioId) || + other.testedViaAccessPointRadioId == + testedViaAccessPointRadioId) && + (identical(other.testedViaMediaConverterId, + testedViaMediaConverterId) || + other.testedViaMediaConverterId == testedViaMediaConverterId) && + (identical(other.uplinkId, uplinkId) || + other.uplinkId == uplinkId) && + (identical(other.wlanId, wlanId) || other.wlanId == wlanId) && + (identical(other.pmsRoomId, pmsRoomId) || + other.pmsRoomId == pmsRoomId) && + (identical(other.roomType, roomType) || + other.roomType == roomType) && + (identical(other.adminId, adminId) || other.adminId == adminId) && + (identical(other.note, note) || other.note == note) && + (identical(other.scratch, scratch) || other.scratch == scratch) && + (identical(other.createdBy, createdBy) || + other.createdBy == createdBy) && + (identical(other.updatedBy, updatedBy) || + other.updatedBy == updatedBy) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && (identical(other.hasError, hasError) || other.hasError == hasError) && (identical(other.errorMessage, errorMessage) || @@ -309,8 +1002,46 @@ class _$SpeedTestResultImpl extends _SpeedTestResult { @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, downloadSpeed, uploadSpeed, - latency, timestamp, hasError, errorMessage, localIpAddress, serverHost); + int get hashCode => Object.hashAll([ + runtimeType, + id, + speedTestId, + testType, + source, + destination, + port, + iperfProtocol, + downloadMbps, + uploadMbps, + rtt, + jitter, + packetLoss, + passed, + isApplicable, + initiatedAt, + completedAt, + raw, + imageUrl, + accessPointId, + testedViaAccessPointId, + testedViaAccessPointRadioId, + testedViaMediaConverterId, + uplinkId, + wlanId, + pmsRoomId, + roomType, + adminId, + note, + scratch, + createdBy, + updatedBy, + createdAt, + updatedAt, + hasError, + errorMessage, + localIpAddress, + serverHost + ]); @JsonKey(ignore: true) @override @@ -323,56 +1054,272 @@ class _$SpeedTestResultImpl extends _SpeedTestResult { @optionalTypeArgs TResult when( TResult Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost) + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost) $default, ) { - return $default(downloadSpeed, uploadSpeed, latency, timestamp, hasError, - errorMessage, localIpAddress, serverHost); + return $default( + id, + speedTestId, + testType, + source, + destination, + port, + iperfProtocol, + downloadMbps, + uploadMbps, + rtt, + jitter, + packetLoss, + passed, + isApplicable, + initiatedAt, + completedAt, + raw, + imageUrl, + accessPointId, + testedViaAccessPointId, + testedViaAccessPointRadioId, + testedViaMediaConverterId, + uplinkId, + wlanId, + pmsRoomId, + roomType, + adminId, + note, + scratch, + createdBy, + updatedBy, + createdAt, + updatedAt, + hasError, + errorMessage, + localIpAddress, + serverHost); } @override @optionalTypeArgs TResult? whenOrNull( TResult? Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost)? + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost)? $default, ) { - return $default?.call(downloadSpeed, uploadSpeed, latency, timestamp, - hasError, errorMessage, localIpAddress, serverHost); + return $default?.call( + id, + speedTestId, + testType, + source, + destination, + port, + iperfProtocol, + downloadMbps, + uploadMbps, + rtt, + jitter, + packetLoss, + passed, + isApplicable, + initiatedAt, + completedAt, + raw, + imageUrl, + accessPointId, + testedViaAccessPointId, + testedViaAccessPointRadioId, + testedViaMediaConverterId, + uplinkId, + wlanId, + pmsRoomId, + roomType, + adminId, + note, + scratch, + createdBy, + updatedBy, + createdAt, + updatedAt, + hasError, + errorMessage, + localIpAddress, + serverHost); } @override @optionalTypeArgs TResult maybeWhen( TResult Function( - double downloadSpeed, - double uploadSpeed, - double latency, - DateTime timestamp, + @JsonKey(fromJson: _toInt) int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) int? speedTestId, + @JsonKey(name: 'test_type') String? testType, + String? source, + String? destination, + @JsonKey(fromJson: _toInt) int? port, + @JsonKey(name: 'iperf_protocol') String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? uploadMbps, + @JsonKey(fromJson: _toDouble) double? rtt, + @JsonKey(fromJson: _toDouble) double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? packetLoss, + bool passed, + @JsonKey(name: 'is_applicable') bool isApplicable, + @JsonKey(name: 'initiated_at') DateTime? initiatedAt, + @JsonKey(name: 'completed_at') DateTime? completedAt, + String? raw, + @JsonKey(name: 'image_url') String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) int? pmsRoomId, + @JsonKey(name: 'room_type') String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) int? adminId, + String? note, + String? scratch, + @JsonKey(name: 'created_by') String? createdBy, + @JsonKey(name: 'updated_by') String? updatedBy, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, bool hasError, String? errorMessage, - String? localIpAddress, - String? serverHost)? + @JsonKey(name: 'local_ip_address') String? localIpAddress, + @JsonKey(name: 'server_host') String? serverHost)? $default, { required TResult orElse(), }) { if ($default != null) { - return $default(downloadSpeed, uploadSpeed, latency, timestamp, hasError, - errorMessage, localIpAddress, serverHost); + return $default( + id, + speedTestId, + testType, + source, + destination, + port, + iperfProtocol, + downloadMbps, + uploadMbps, + rtt, + jitter, + packetLoss, + passed, + isApplicable, + initiatedAt, + completedAt, + raw, + imageUrl, + accessPointId, + testedViaAccessPointId, + testedViaAccessPointRadioId, + testedViaMediaConverterId, + uplinkId, + wlanId, + pmsRoomId, + roomType, + adminId, + note, + scratch, + createdBy, + updatedBy, + createdAt, + updatedAt, + hasError, + errorMessage, + localIpAddress, + serverHost); } return orElse(); } @@ -415,13 +1362,50 @@ class _$SpeedTestResultImpl extends _SpeedTestResult { abstract class _SpeedTestResult extends SpeedTestResult { const factory _SpeedTestResult( - {required final double downloadSpeed, - required final double uploadSpeed, - required final double latency, - required final DateTime timestamp, + {@JsonKey(fromJson: _toInt) final int? id, + @JsonKey(name: 'speed_test_id', fromJson: _toInt) final int? speedTestId, + @JsonKey(name: 'test_type') final String? testType, + final String? source, + final String? destination, + @JsonKey(fromJson: _toInt) final int? port, + @JsonKey(name: 'iperf_protocol') final String? iperfProtocol, + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + final double? downloadMbps, + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + final double? uploadMbps, + @JsonKey(fromJson: _toDouble) final double? rtt, + @JsonKey(fromJson: _toDouble) final double? jitter, + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + final double? packetLoss, + final bool passed, + @JsonKey(name: 'is_applicable') final bool isApplicable, + @JsonKey(name: 'initiated_at') final DateTime? initiatedAt, + @JsonKey(name: 'completed_at') final DateTime? completedAt, + final String? raw, + @JsonKey(name: 'image_url') final String? imageUrl, + @JsonKey(name: 'access_point_id', fromJson: _toInt) + final int? accessPointId, + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + final int? testedViaAccessPointId, + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + final int? testedViaAccessPointRadioId, + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + final int? testedViaMediaConverterId, + @JsonKey(name: 'uplink_id', fromJson: _toInt) final int? uplinkId, + @JsonKey(name: 'wlan_id', fromJson: _toInt) final int? wlanId, + @JsonKey(name: 'pms_room_id', fromJson: _toInt) final int? pmsRoomId, + @JsonKey(name: 'room_type') final String? roomType, + @JsonKey(name: 'admin_id', fromJson: _toInt) final int? adminId, + final String? note, + final String? scratch, + @JsonKey(name: 'created_by') final String? createdBy, + @JsonKey(name: 'updated_by') final String? updatedBy, + @JsonKey(name: 'created_at') final DateTime? createdAt, + @JsonKey(name: 'updated_at') final DateTime? updatedAt, final bool hasError, final String? errorMessage, - final String? localIpAddress, + @JsonKey(name: 'local_ip_address') final String? localIpAddress, + @JsonKey(name: 'server_host') final String? serverHost}) = _$SpeedTestResultImpl; const _SpeedTestResult._() : super._(); @@ -429,20 +1413,107 @@ abstract class _SpeedTestResult extends SpeedTestResult { _$SpeedTestResultImpl.fromJson; @override - double get downloadSpeed; + @JsonKey(fromJson: _toInt) + int? get id; + @override + @JsonKey(name: 'speed_test_id', fromJson: _toInt) + int? get speedTestId; + @override + @JsonKey(name: 'test_type') + String? get testType; + @override + String? get source; + @override + String? get destination; + @override + @JsonKey(fromJson: _toInt) + int? get port; + @override + @JsonKey(name: 'iperf_protocol') + String? get iperfProtocol; + @override + @JsonKey(name: 'download_mbps', fromJson: _toDouble) + double? get downloadMbps; + @override + @JsonKey(name: 'upload_mbps', fromJson: _toDouble) + double? get uploadMbps; + @override + @JsonKey(fromJson: _toDouble) + double? get rtt; + @override + @JsonKey(fromJson: _toDouble) + double? get jitter; + @override + @JsonKey(name: 'packet_loss', fromJson: _toDouble) + double? get packetLoss; + @override + bool get passed; + @override + @JsonKey(name: 'is_applicable') + bool get isApplicable; + @override + @JsonKey(name: 'initiated_at') + DateTime? get initiatedAt; + @override + @JsonKey(name: 'completed_at') + DateTime? get completedAt; + @override + String? get raw; + @override + @JsonKey(name: 'image_url') + String? get imageUrl; + @override + @JsonKey(name: 'access_point_id', fromJson: _toInt) + int? get accessPointId; + @override + @JsonKey(name: 'tested_via_access_point_id', fromJson: _toInt) + int? get testedViaAccessPointId; + @override + @JsonKey(name: 'tested_via_access_point_radio_id', fromJson: _toInt) + int? get testedViaAccessPointRadioId; + @override + @JsonKey(name: 'tested_via_media_converter_id', fromJson: _toInt) + int? get testedViaMediaConverterId; + @override + @JsonKey(name: 'uplink_id', fromJson: _toInt) + int? get uplinkId; + @override + @JsonKey(name: 'wlan_id', fromJson: _toInt) + int? get wlanId; + @override + @JsonKey(name: 'pms_room_id', fromJson: _toInt) + int? get pmsRoomId; + @override + @JsonKey(name: 'room_type') + String? get roomType; + @override + @JsonKey(name: 'admin_id', fromJson: _toInt) + int? get adminId; + @override + String? get note; + @override + String? get scratch; @override - double get uploadSpeed; + @JsonKey(name: 'created_by') + String? get createdBy; @override - double get latency; + @JsonKey(name: 'updated_by') + String? get updatedBy; @override - DateTime get timestamp; + @JsonKey(name: 'created_at') + DateTime? get createdAt; @override + @JsonKey(name: 'updated_at') + DateTime? get updatedAt; + @override // Legacy fields for backwards compatibility bool get hasError; @override String? get errorMessage; @override + @JsonKey(name: 'local_ip_address') String? get localIpAddress; @override + @JsonKey(name: 'server_host') String? get serverHost; @override @JsonKey(ignore: true) diff --git a/lib/features/speed_test/domain/entities/speed_test_result.g.dart b/lib/features/speed_test/domain/entities/speed_test_result.g.dart index 865cf42..2304093 100644 --- a/lib/features/speed_test/domain/entities/speed_test_result.g.dart +++ b/lib/features/speed_test/domain/entities/speed_test_result.g.dart @@ -9,10 +9,48 @@ part of 'speed_test_result.dart'; _$SpeedTestResultImpl _$$SpeedTestResultImplFromJson( Map json) => _$SpeedTestResultImpl( - downloadSpeed: (json['download_speed'] as num).toDouble(), - uploadSpeed: (json['upload_speed'] as num).toDouble(), - latency: (json['latency'] as num).toDouble(), - timestamp: DateTime.parse(json['timestamp'] as String), + id: _toInt(json['id']), + speedTestId: _toInt(json['speed_test_id']), + testType: json['test_type'] as String?, + source: json['source'] as String?, + destination: json['destination'] as String?, + port: _toInt(json['port']), + iperfProtocol: json['iperf_protocol'] as String?, + downloadMbps: _toDouble(json['download_mbps']), + uploadMbps: _toDouble(json['upload_mbps']), + rtt: _toDouble(json['rtt']), + jitter: _toDouble(json['jitter']), + packetLoss: _toDouble(json['packet_loss']), + passed: json['passed'] as bool? ?? false, + isApplicable: json['is_applicable'] as bool? ?? true, + initiatedAt: json['initiated_at'] == null + ? null + : DateTime.parse(json['initiated_at'] as String), + completedAt: json['completed_at'] == null + ? null + : DateTime.parse(json['completed_at'] as String), + raw: json['raw'] as String?, + imageUrl: json['image_url'] as String?, + accessPointId: _toInt(json['access_point_id']), + testedViaAccessPointId: _toInt(json['tested_via_access_point_id']), + testedViaAccessPointRadioId: + _toInt(json['tested_via_access_point_radio_id']), + testedViaMediaConverterId: _toInt(json['tested_via_media_converter_id']), + uplinkId: _toInt(json['uplink_id']), + wlanId: _toInt(json['wlan_id']), + pmsRoomId: _toInt(json['pms_room_id']), + roomType: json['room_type'] as String?, + adminId: _toInt(json['admin_id']), + note: json['note'] as String?, + scratch: json['scratch'] as String?, + createdBy: json['created_by'] as String?, + updatedBy: json['updated_by'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), hasError: json['has_error'] as bool? ?? false, errorMessage: json['error_message'] as String?, localIpAddress: json['local_ip_address'] as String?, @@ -21,13 +59,7 @@ _$SpeedTestResultImpl _$$SpeedTestResultImplFromJson( Map _$$SpeedTestResultImplToJson( _$SpeedTestResultImpl instance) { - final val = { - 'download_speed': instance.downloadSpeed, - 'upload_speed': instance.uploadSpeed, - 'latency': instance.latency, - 'timestamp': instance.timestamp.toIso8601String(), - 'has_error': instance.hasError, - }; + final val = {}; void writeNotNull(String key, dynamic value) { if (value != null) { @@ -35,6 +67,42 @@ Map _$$SpeedTestResultImplToJson( } } + writeNotNull('id', instance.id); + writeNotNull('speed_test_id', instance.speedTestId); + writeNotNull('test_type', instance.testType); + writeNotNull('source', instance.source); + writeNotNull('destination', instance.destination); + writeNotNull('port', instance.port); + writeNotNull('iperf_protocol', instance.iperfProtocol); + writeNotNull('download_mbps', instance.downloadMbps); + writeNotNull('upload_mbps', instance.uploadMbps); + writeNotNull('rtt', instance.rtt); + writeNotNull('jitter', instance.jitter); + writeNotNull('packet_loss', instance.packetLoss); + val['passed'] = instance.passed; + val['is_applicable'] = instance.isApplicable; + writeNotNull('initiated_at', instance.initiatedAt?.toIso8601String()); + writeNotNull('completed_at', instance.completedAt?.toIso8601String()); + writeNotNull('raw', instance.raw); + writeNotNull('image_url', instance.imageUrl); + writeNotNull('access_point_id', instance.accessPointId); + writeNotNull('tested_via_access_point_id', instance.testedViaAccessPointId); + writeNotNull( + 'tested_via_access_point_radio_id', instance.testedViaAccessPointRadioId); + writeNotNull( + 'tested_via_media_converter_id', instance.testedViaMediaConverterId); + writeNotNull('uplink_id', instance.uplinkId); + writeNotNull('wlan_id', instance.wlanId); + writeNotNull('pms_room_id', instance.pmsRoomId); + writeNotNull('room_type', instance.roomType); + writeNotNull('admin_id', instance.adminId); + writeNotNull('note', instance.note); + writeNotNull('scratch', instance.scratch); + writeNotNull('created_by', instance.createdBy); + writeNotNull('updated_by', instance.updatedBy); + writeNotNull('created_at', instance.createdAt?.toIso8601String()); + writeNotNull('updated_at', instance.updatedAt?.toIso8601String()); + val['has_error'] = instance.hasError; writeNotNull('error_message', instance.errorMessage); writeNotNull('local_ip_address', instance.localIpAddress); writeNotNull('server_host', instance.serverHost); diff --git a/lib/features/speed_test/domain/entities/speed_test_with_results.dart b/lib/features/speed_test/domain/entities/speed_test_with_results.dart new file mode 100644 index 0000000..8c0822d --- /dev/null +++ b/lib/features/speed_test/domain/entities/speed_test_with_results.dart @@ -0,0 +1,69 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; + +part 'speed_test_with_results.freezed.dart'; + +/// A joined entity containing a speed test configuration with its associated results. +/// Note: This is a view model created in code, not from JSON. +@Freezed(toJson: false, fromJson: false) +class SpeedTestWithResults with _$SpeedTestWithResults { + const factory SpeedTestWithResults({ + required SpeedTestConfig config, + @Default([]) List results, + }) = _SpeedTestWithResults; + + const SpeedTestWithResults._(); + + /// Get the most recent result + SpeedTestResult? get latestResult { + if (results.isEmpty) return null; + return results.reduce((a, b) { + final aTime = a.completedAt ?? a.createdAt ?? DateTime(1970); + final bTime = b.completedAt ?? b.createdAt ?? DateTime(1970); + return aTime.isAfter(bTime) ? a : b; + }); + } + + /// Get the number of results + int get resultCount => results.length; + + /// Check if there are any results + bool get hasResults => results.isNotEmpty; + + /// Get passing results only + List get passingResults => + results.where((r) => r.passed).toList(); + + /// Get failing results only + List get failingResults => + results.where((r) => !r.passed).toList(); + + /// Calculate pass rate as percentage + double get passRate { + if (results.isEmpty) return 0.0; + return (passingResults.length / results.length) * 100; + } + + /// Check if the test is currently passing (based on latest result) + bool get isCurrentlyPassing => latestResult?.passed ?? false; + + /// Check if meets minimum download requirement + bool get meetsDownloadRequirement { + final latest = latestResult; + if (latest?.downloadMbps == null || config.minDownloadMbps == null) { + return true; + } + return latest!.downloadMbps! >= config.minDownloadMbps!; + } + + /// Check if meets minimum upload requirement + bool get meetsUploadRequirement { + final latest = latestResult; + if (latest?.uploadMbps == null || config.minUploadMbps == null) { + return true; + } + return latest!.uploadMbps! >= config.minUploadMbps!; + } +} diff --git a/lib/features/speed_test/domain/entities/speed_test_with_results.freezed.dart b/lib/features/speed_test/domain/entities/speed_test_with_results.freezed.dart new file mode 100644 index 0000000..05095ea --- /dev/null +++ b/lib/features/speed_test/domain/entities/speed_test_with_results.freezed.dart @@ -0,0 +1,271 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'speed_test_with_results.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SpeedTestWithResults { + SpeedTestConfig get config => throw _privateConstructorUsedError; + List get results => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when( + TResult Function(SpeedTestConfig config, List results) + $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(SpeedTestConfig config, List results)? + $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function(SpeedTestConfig config, List results)? + $default, { + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(_SpeedTestWithResults value) $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_SpeedTestWithResults value)? $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(_SpeedTestWithResults value)? $default, { + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $SpeedTestWithResultsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpeedTestWithResultsCopyWith<$Res> { + factory $SpeedTestWithResultsCopyWith(SpeedTestWithResults value, + $Res Function(SpeedTestWithResults) then) = + _$SpeedTestWithResultsCopyWithImpl<$Res, SpeedTestWithResults>; + @useResult + $Res call({SpeedTestConfig config, List results}); + + $SpeedTestConfigCopyWith<$Res> get config; +} + +/// @nodoc +class _$SpeedTestWithResultsCopyWithImpl<$Res, + $Val extends SpeedTestWithResults> + implements $SpeedTestWithResultsCopyWith<$Res> { + _$SpeedTestWithResultsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? config = null, + Object? results = null, + }) { + return _then(_value.copyWith( + config: null == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as SpeedTestConfig, + results: null == results + ? _value.results + : results // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $SpeedTestConfigCopyWith<$Res> get config { + return $SpeedTestConfigCopyWith<$Res>(_value.config, (value) { + return _then(_value.copyWith(config: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SpeedTestWithResultsImplCopyWith<$Res> + implements $SpeedTestWithResultsCopyWith<$Res> { + factory _$$SpeedTestWithResultsImplCopyWith(_$SpeedTestWithResultsImpl value, + $Res Function(_$SpeedTestWithResultsImpl) then) = + __$$SpeedTestWithResultsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({SpeedTestConfig config, List results}); + + @override + $SpeedTestConfigCopyWith<$Res> get config; +} + +/// @nodoc +class __$$SpeedTestWithResultsImplCopyWithImpl<$Res> + extends _$SpeedTestWithResultsCopyWithImpl<$Res, _$SpeedTestWithResultsImpl> + implements _$$SpeedTestWithResultsImplCopyWith<$Res> { + __$$SpeedTestWithResultsImplCopyWithImpl(_$SpeedTestWithResultsImpl _value, + $Res Function(_$SpeedTestWithResultsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? config = null, + Object? results = null, + }) { + return _then(_$SpeedTestWithResultsImpl( + config: null == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as SpeedTestConfig, + results: null == results + ? _value._results + : results // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$SpeedTestWithResultsImpl extends _SpeedTestWithResults { + const _$SpeedTestWithResultsImpl( + {required this.config, final List results = const []}) + : _results = results, + super._(); + + @override + final SpeedTestConfig config; + final List _results; + @override + @JsonKey() + List get results { + if (_results is EqualUnmodifiableListView) return _results; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_results); + } + + @override + String toString() { + return 'SpeedTestWithResults(config: $config, results: $results)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpeedTestWithResultsImpl && + (identical(other.config, config) || other.config == config) && + const DeepCollectionEquality().equals(other._results, _results)); + } + + @override + int get hashCode => Object.hash( + runtimeType, config, const DeepCollectionEquality().hash(_results)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SpeedTestWithResultsImplCopyWith<_$SpeedTestWithResultsImpl> + get copyWith => + __$$SpeedTestWithResultsImplCopyWithImpl<_$SpeedTestWithResultsImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function(SpeedTestConfig config, List results) + $default, + ) { + return $default(config, results); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function(SpeedTestConfig config, List results)? + $default, + ) { + return $default?.call(config, results); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function(SpeedTestConfig config, List results)? + $default, { + required TResult orElse(), + }) { + if ($default != null) { + return $default(config, results); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_SpeedTestWithResults value) $default, + ) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_SpeedTestWithResults value)? $default, + ) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_SpeedTestWithResults value)? $default, { + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class _SpeedTestWithResults extends SpeedTestWithResults { + const factory _SpeedTestWithResults( + {required final SpeedTestConfig config, + final List results}) = _$SpeedTestWithResultsImpl; + const _SpeedTestWithResults._() : super._(); + + @override + SpeedTestConfig get config; + @override + List get results; + @override + @JsonKey(ignore: true) + _$$SpeedTestWithResultsImplCopyWith<_$SpeedTestWithResultsImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/speed_test/domain/repositories/speed_test_repository.dart b/lib/features/speed_test/domain/repositories/speed_test_repository.dart new file mode 100644 index 0000000..fd4fb7f --- /dev/null +++ b/lib/features/speed_test/domain/repositories/speed_test_repository.dart @@ -0,0 +1,55 @@ +import 'package:fpdart/fpdart.dart'; + +import 'package:rgnets_fdk/core/errors/failures.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_with_results.dart'; + +/// Repository interface for speed test configurations and results. +abstract class SpeedTestRepository { + // ============================================================================ + // Speed Test Config Operations (Read-only) + // ============================================================================ + + /// Get all speed test configurations + Future>> getSpeedTestConfigs(); + + /// Get a specific speed test configuration by ID + Future> getSpeedTestConfig(int id); + + // ============================================================================ + // Speed Test Result Operations + // ============================================================================ + + /// Get all speed test results with optional filtering + Future>> getSpeedTestResults({ + int? speedTestId, + int? accessPointId, + int? limit, + int? offset, + }); + + /// Get a specific speed test result by ID + Future> getSpeedTestResult(int id); + + /// Create a new speed test result + Future> createSpeedTestResult( + SpeedTestResult result, + ); + + /// Update an existing speed test result + Future> updateSpeedTestResult( + SpeedTestResult result, + ); + + // ============================================================================ + // Joined Operations + // ============================================================================ + + /// Get a speed test configuration with all its results + Future> getSpeedTestWithResults(int id); + + /// Get all speed test configurations with their results + Future>> + getAllSpeedTestsWithResults(); +} diff --git a/lib/features/speed_test/presentation/providers/speed_test_providers.dart b/lib/features/speed_test/presentation/providers/speed_test_providers.dart new file mode 100644 index 0000000..d303341 --- /dev/null +++ b/lib/features/speed_test/presentation/providers/speed_test_providers.dart @@ -0,0 +1,259 @@ +import 'package:logger/logger.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:rgnets_fdk/core/providers/core_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_data_source.dart'; +import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_websocket_data_source.dart'; +import 'package:rgnets_fdk/features/speed_test/data/repositories/speed_test_repository_impl.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_with_results.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/repositories/speed_test_repository.dart'; + +part 'speed_test_providers.g.dart'; + +// ============================================================================ +// Data Source Provider +// ============================================================================ + +@Riverpod(keepAlive: true) +SpeedTestDataSource speedTestDataSource(SpeedTestDataSourceRef ref) { + final webSocketService = ref.watch(webSocketServiceProvider); + return SpeedTestWebSocketDataSource( + webSocketService: webSocketService, + logger: Logger(), + ); +} + +// ============================================================================ +// Repository Provider +// ============================================================================ + +@Riverpod(keepAlive: true) +SpeedTestRepository speedTestRepository(SpeedTestRepositoryRef ref) { + final dataSource = ref.watch(speedTestDataSourceProvider); + return SpeedTestRepositoryImpl( + dataSource: dataSource, + logger: Logger(), + ); +} + +// ============================================================================ +// Speed Test Configs Provider +// ============================================================================ + +@Riverpod(keepAlive: true) +class SpeedTestConfigsNotifier extends _$SpeedTestConfigsNotifier { + Logger get _logger => ref.read(loggerProvider); + + @override + Future> build() async { + _logger.i('SpeedTestConfigsNotifier: Loading speed test configs'); + + final repository = ref.watch(speedTestRepositoryProvider); + final result = await repository.getSpeedTestConfigs(); + + return result.fold( + (failure) { + _logger.e('SpeedTestConfigsNotifier: Failed - ${failure.message}'); + throw Exception(failure.message); + }, + (configs) { + _logger.i( + 'SpeedTestConfigsNotifier: Loaded ${configs.length} configs', + ); + return configs; + }, + ); + } + + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = ref.read(speedTestRepositoryProvider); + final result = await repository.getSpeedTestConfigs(); + return result.fold( + (failure) => throw Exception(failure.message), + (configs) => configs, + ); + }); + } +} + +// ============================================================================ +// Speed Test Results Provider +// ============================================================================ + +@Riverpod(keepAlive: true) +class SpeedTestResultsNotifier extends _$SpeedTestResultsNotifier { + Logger get _logger => ref.read(loggerProvider); + + @override + Future> build({ + int? speedTestId, + int? accessPointId, + }) async { + _logger.i( + 'SpeedTestResultsNotifier: Loading results ' + '(speedTestId: $speedTestId, accessPointId: $accessPointId)', + ); + + final repository = ref.watch(speedTestRepositoryProvider); + final result = await repository.getSpeedTestResults( + speedTestId: speedTestId, + accessPointId: accessPointId, + ); + + return result.fold( + (failure) { + _logger.e('SpeedTestResultsNotifier: Failed - ${failure.message}'); + throw Exception(failure.message); + }, + (results) { + _logger.i( + 'SpeedTestResultsNotifier: Loaded ${results.length} results', + ); + return results; + }, + ); + } + + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = ref.read(speedTestRepositoryProvider); + final result = await repository.getSpeedTestResults( + speedTestId: speedTestId, + accessPointId: accessPointId, + ); + return result.fold( + (failure) => throw Exception(failure.message), + (results) => results, + ); + }); + } + + Future createResult(SpeedTestResult result) async { + final repository = ref.read(speedTestRepositoryProvider); + final createResult = await repository.createSpeedTestResult(result); + + return createResult.fold( + (failure) { + _logger.e('Failed to create result: ${failure.message}'); + return null; + }, + (created) { + // Refresh the list + refresh(); + return created; + }, + ); + } + + Future updateResult(SpeedTestResult result) async { + final repository = ref.read(speedTestRepositoryProvider); + final updateResult = await repository.updateSpeedTestResult(result); + + return updateResult.fold( + (failure) { + _logger.e('Failed to update result: ${failure.message}'); + return null; + }, + (updated) { + // Refresh the list + refresh(); + return updated; + }, + ); + } +} + +// ============================================================================ +// Speed Test With Results Provider (Joined) +// ============================================================================ + +@Riverpod(keepAlive: true) +class SpeedTestWithResultsNotifier extends _$SpeedTestWithResultsNotifier { + Logger get _logger => ref.read(loggerProvider); + + @override + Future build(int configId) async { + _logger.i('SpeedTestWithResultsNotifier: Loading config $configId'); + + final repository = ref.watch(speedTestRepositoryProvider); + final result = await repository.getSpeedTestWithResults(configId); + + return result.fold( + (failure) { + _logger.e( + 'SpeedTestWithResultsNotifier: Failed - ${failure.message}', + ); + throw Exception(failure.message); + }, + (joined) { + _logger.i( + 'SpeedTestWithResultsNotifier: Loaded config $configId ' + 'with ${joined.resultCount} results', + ); + return joined; + }, + ); + } + + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = ref.read(speedTestRepositoryProvider); + final result = await repository.getSpeedTestWithResults(configId); + return result.fold( + (failure) => throw Exception(failure.message), + (joined) => joined, + ); + }); + } +} + +// ============================================================================ +// All Speed Tests With Results Provider +// ============================================================================ + +@Riverpod(keepAlive: true) +class AllSpeedTestsWithResultsNotifier + extends _$AllSpeedTestsWithResultsNotifier { + Logger get _logger => ref.read(loggerProvider); + + @override + Future> build() async { + _logger.i('AllSpeedTestsWithResultsNotifier: Loading all speed tests'); + + final repository = ref.watch(speedTestRepositoryProvider); + final result = await repository.getAllSpeedTestsWithResults(); + + return result.fold( + (failure) { + _logger.e( + 'AllSpeedTestsWithResultsNotifier: Failed - ${failure.message}', + ); + throw Exception(failure.message); + }, + (joined) { + _logger.i( + 'AllSpeedTestsWithResultsNotifier: Loaded ${joined.length} speed tests', + ); + return joined; + }, + ); + } + + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = ref.read(speedTestRepositoryProvider); + final result = await repository.getAllSpeedTestsWithResults(); + return result.fold( + (failure) => throw Exception(failure.message), + (joined) => joined, + ); + }); + } +} diff --git a/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart b/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart new file mode 100644 index 0000000..ac308b8 --- /dev/null +++ b/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart @@ -0,0 +1,419 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'speed_test_providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$speedTestDataSourceHash() => + r'04b217e04a9b8ef1dfca0744f8a1e97e12964d82'; + +/// See also [speedTestDataSource]. +@ProviderFor(speedTestDataSource) +final speedTestDataSourceProvider = Provider.internal( + speedTestDataSource, + name: r'speedTestDataSourceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$speedTestDataSourceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SpeedTestDataSourceRef = ProviderRef; +String _$speedTestRepositoryHash() => + r'7460c9da6a8c0b1775d45e3b79a76a62b11d4c05'; + +/// See also [speedTestRepository]. +@ProviderFor(speedTestRepository) +final speedTestRepositoryProvider = Provider.internal( + speedTestRepository, + name: r'speedTestRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$speedTestRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SpeedTestRepositoryRef = ProviderRef; +String _$speedTestConfigsNotifierHash() => + r'99fe29c99e231a1a2bf70e065b22c02a5a2806a4'; + +/// See also [SpeedTestConfigsNotifier]. +@ProviderFor(SpeedTestConfigsNotifier) +final speedTestConfigsNotifierProvider = AsyncNotifierProvider< + SpeedTestConfigsNotifier, List>.internal( + SpeedTestConfigsNotifier.new, + name: r'speedTestConfigsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$speedTestConfigsNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SpeedTestConfigsNotifier = AsyncNotifier>; +String _$speedTestResultsNotifierHash() => + r'1e035a7a5d6105cc2577309ba1a1469642de508d'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$SpeedTestResultsNotifier + extends BuildlessAsyncNotifier> { + late final int? speedTestId; + late final int? accessPointId; + + FutureOr> build({ + int? speedTestId, + int? accessPointId, + }); +} + +/// See also [SpeedTestResultsNotifier]. +@ProviderFor(SpeedTestResultsNotifier) +const speedTestResultsNotifierProvider = SpeedTestResultsNotifierFamily(); + +/// See also [SpeedTestResultsNotifier]. +class SpeedTestResultsNotifierFamily + extends Family>> { + /// See also [SpeedTestResultsNotifier]. + const SpeedTestResultsNotifierFamily(); + + /// See also [SpeedTestResultsNotifier]. + SpeedTestResultsNotifierProvider call({ + int? speedTestId, + int? accessPointId, + }) { + return SpeedTestResultsNotifierProvider( + speedTestId: speedTestId, + accessPointId: accessPointId, + ); + } + + @override + SpeedTestResultsNotifierProvider getProviderOverride( + covariant SpeedTestResultsNotifierProvider provider, + ) { + return call( + speedTestId: provider.speedTestId, + accessPointId: provider.accessPointId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'speedTestResultsNotifierProvider'; +} + +/// See also [SpeedTestResultsNotifier]. +class SpeedTestResultsNotifierProvider extends AsyncNotifierProviderImpl< + SpeedTestResultsNotifier, List> { + /// See also [SpeedTestResultsNotifier]. + SpeedTestResultsNotifierProvider({ + int? speedTestId, + int? accessPointId, + }) : this._internal( + () => SpeedTestResultsNotifier() + ..speedTestId = speedTestId + ..accessPointId = accessPointId, + from: speedTestResultsNotifierProvider, + name: r'speedTestResultsNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$speedTestResultsNotifierHash, + dependencies: SpeedTestResultsNotifierFamily._dependencies, + allTransitiveDependencies: + SpeedTestResultsNotifierFamily._allTransitiveDependencies, + speedTestId: speedTestId, + accessPointId: accessPointId, + ); + + SpeedTestResultsNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.speedTestId, + required this.accessPointId, + }) : super.internal(); + + final int? speedTestId; + final int? accessPointId; + + @override + FutureOr> runNotifierBuild( + covariant SpeedTestResultsNotifier notifier, + ) { + return notifier.build( + speedTestId: speedTestId, + accessPointId: accessPointId, + ); + } + + @override + Override overrideWith(SpeedTestResultsNotifier Function() create) { + return ProviderOverride( + origin: this, + override: SpeedTestResultsNotifierProvider._internal( + () => create() + ..speedTestId = speedTestId + ..accessPointId = accessPointId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + speedTestId: speedTestId, + accessPointId: accessPointId, + ), + ); + } + + @override + AsyncNotifierProviderElement> + createElement() { + return _SpeedTestResultsNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SpeedTestResultsNotifierProvider && + other.speedTestId == speedTestId && + other.accessPointId == accessPointId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, speedTestId.hashCode); + hash = _SystemHash.combine(hash, accessPointId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin SpeedTestResultsNotifierRef + on AsyncNotifierProviderRef> { + /// The parameter `speedTestId` of this provider. + int? get speedTestId; + + /// The parameter `accessPointId` of this provider. + int? get accessPointId; +} + +class _SpeedTestResultsNotifierProviderElement + extends AsyncNotifierProviderElement> with SpeedTestResultsNotifierRef { + _SpeedTestResultsNotifierProviderElement(super.provider); + + @override + int? get speedTestId => + (origin as SpeedTestResultsNotifierProvider).speedTestId; + @override + int? get accessPointId => + (origin as SpeedTestResultsNotifierProvider).accessPointId; +} + +String _$speedTestWithResultsNotifierHash() => + r'ca7c8b8e92c543d1ca0e47ee04a15a7a328c6d40'; + +abstract class _$SpeedTestWithResultsNotifier + extends BuildlessAsyncNotifier { + late final int configId; + + FutureOr build( + int configId, + ); +} + +/// See also [SpeedTestWithResultsNotifier]. +@ProviderFor(SpeedTestWithResultsNotifier) +const speedTestWithResultsNotifierProvider = + SpeedTestWithResultsNotifierFamily(); + +/// See also [SpeedTestWithResultsNotifier]. +class SpeedTestWithResultsNotifierFamily + extends Family> { + /// See also [SpeedTestWithResultsNotifier]. + const SpeedTestWithResultsNotifierFamily(); + + /// See also [SpeedTestWithResultsNotifier]. + SpeedTestWithResultsNotifierProvider call( + int configId, + ) { + return SpeedTestWithResultsNotifierProvider( + configId, + ); + } + + @override + SpeedTestWithResultsNotifierProvider getProviderOverride( + covariant SpeedTestWithResultsNotifierProvider provider, + ) { + return call( + provider.configId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'speedTestWithResultsNotifierProvider'; +} + +/// See also [SpeedTestWithResultsNotifier]. +class SpeedTestWithResultsNotifierProvider extends AsyncNotifierProviderImpl< + SpeedTestWithResultsNotifier, SpeedTestWithResults> { + /// See also [SpeedTestWithResultsNotifier]. + SpeedTestWithResultsNotifierProvider( + int configId, + ) : this._internal( + () => SpeedTestWithResultsNotifier()..configId = configId, + from: speedTestWithResultsNotifierProvider, + name: r'speedTestWithResultsNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$speedTestWithResultsNotifierHash, + dependencies: SpeedTestWithResultsNotifierFamily._dependencies, + allTransitiveDependencies: + SpeedTestWithResultsNotifierFamily._allTransitiveDependencies, + configId: configId, + ); + + SpeedTestWithResultsNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.configId, + }) : super.internal(); + + final int configId; + + @override + FutureOr runNotifierBuild( + covariant SpeedTestWithResultsNotifier notifier, + ) { + return notifier.build( + configId, + ); + } + + @override + Override overrideWith(SpeedTestWithResultsNotifier Function() create) { + return ProviderOverride( + origin: this, + override: SpeedTestWithResultsNotifierProvider._internal( + () => create()..configId = configId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + configId: configId, + ), + ); + } + + @override + AsyncNotifierProviderElement createElement() { + return _SpeedTestWithResultsNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SpeedTestWithResultsNotifierProvider && + other.configId == configId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, configId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin SpeedTestWithResultsNotifierRef + on AsyncNotifierProviderRef { + /// The parameter `configId` of this provider. + int get configId; +} + +class _SpeedTestWithResultsNotifierProviderElement + extends AsyncNotifierProviderElement with SpeedTestWithResultsNotifierRef { + _SpeedTestWithResultsNotifierProviderElement(super.provider); + + @override + int get configId => (origin as SpeedTestWithResultsNotifierProvider).configId; +} + +String _$allSpeedTestsWithResultsNotifierHash() => + r'd773bec35269df06902eed57df641eb48a46c935'; + +/// See also [AllSpeedTestsWithResultsNotifier]. +@ProviderFor(AllSpeedTestsWithResultsNotifier) +final allSpeedTestsWithResultsNotifierProvider = AsyncNotifierProvider< + AllSpeedTestsWithResultsNotifier, List>.internal( + AllSpeedTestsWithResultsNotifier.new, + name: r'allSpeedTestsWithResultsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$allSpeedTestsWithResultsNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AllSpeedTestsWithResultsNotifier + = AsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/speed_test/presentation/state/speed_test_run_state.dart b/lib/features/speed_test/presentation/state/speed_test_run_state.dart new file mode 100644 index 0000000..8787f0e --- /dev/null +++ b/lib/features/speed_test/presentation/state/speed_test_run_state.dart @@ -0,0 +1,63 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_status.dart'; + +part 'speed_test_run_state.freezed.dart'; + +@freezed +class SpeedTestRunState with _$SpeedTestRunState { + const factory SpeedTestRunState({ + // Execution status (idle, running, completed, error) + @Default(SpeedTestStatus.idle) SpeedTestStatus executionStatus, + + // Progress (0-100) + @Default(0.0) double progress, + + // Status message (for UI display) + String? statusMessage, + + // Result data (from result stream) + @Default(0.0) double downloadSpeed, + @Default(0.0) double uploadSpeed, + @Default(0.0) double latency, + + // Validation status: null = not run, true = passed, false = failed + bool? testPassed, + + // Error state + String? errorMessage, + + // Network info + String? localIpAddress, + String? gatewayAddress, + + // Server configuration + @Default('') String serverHost, + @Default(5201) int serverPort, + + // Test configuration + @Default(10) int testDuration, + @Default(0) int bandwidthMbps, + @Default(1) int parallelStreams, + @Default(false) bool useUdp, + + // Full result object (for submission) + SpeedTestResult? completedResult, + SpeedTestConfig? config, + + // Initialization flag + @Default(false) bool isInitialized, + }) = _SpeedTestRunState; + + const SpeedTestRunState._(); + + /// Derived validation status: not run, passed, or failed + String get validationStatus { + if (testPassed == null) return 'not run'; + return testPassed! ? 'passed' : 'failed'; + } + + /// Whether a test is currently running + bool get isRunning => executionStatus == SpeedTestStatus.running; +} diff --git a/lib/features/speed_test/presentation/state/speed_test_run_state.freezed.dart b/lib/features/speed_test/presentation/state/speed_test_run_state.freezed.dart new file mode 100644 index 0000000..6a9ea3e --- /dev/null +++ b/lib/features/speed_test/presentation/state/speed_test_run_state.freezed.dart @@ -0,0 +1,866 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'speed_test_run_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SpeedTestRunState { +// Execution status (idle, running, completed, error) + SpeedTestStatus get executionStatus => + throw _privateConstructorUsedError; // Progress (0-100) + double get progress => + throw _privateConstructorUsedError; // Status message (for UI display) + String? get statusMessage => + throw _privateConstructorUsedError; // Result data (from result stream) + double get downloadSpeed => throw _privateConstructorUsedError; + double get uploadSpeed => throw _privateConstructorUsedError; + double get latency => + throw _privateConstructorUsedError; // Validation status: null = not run, true = passed, false = failed + bool? get testPassed => throw _privateConstructorUsedError; // Error state + String? get errorMessage => + throw _privateConstructorUsedError; // Network info + String? get localIpAddress => throw _privateConstructorUsedError; + String? get gatewayAddress => + throw _privateConstructorUsedError; // Server configuration + String get serverHost => throw _privateConstructorUsedError; + int get serverPort => + throw _privateConstructorUsedError; // Test configuration + int get testDuration => throw _privateConstructorUsedError; + int get bandwidthMbps => throw _privateConstructorUsedError; + int get parallelStreams => throw _privateConstructorUsedError; + bool get useUdp => + throw _privateConstructorUsedError; // Full result object (for submission) + SpeedTestResult? get completedResult => throw _privateConstructorUsedError; + SpeedTestConfig? get config => + throw _privateConstructorUsedError; // Initialization flag + bool get isInitialized => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when( + TResult Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized) + $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized)? + $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized)? + $default, { + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(_SpeedTestRunState value) $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_SpeedTestRunState value)? $default, + ) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(_SpeedTestRunState value)? $default, { + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $SpeedTestRunStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SpeedTestRunStateCopyWith<$Res> { + factory $SpeedTestRunStateCopyWith( + SpeedTestRunState value, $Res Function(SpeedTestRunState) then) = + _$SpeedTestRunStateCopyWithImpl<$Res, SpeedTestRunState>; + @useResult + $Res call( + {SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized}); + + $SpeedTestResultCopyWith<$Res>? get completedResult; + $SpeedTestConfigCopyWith<$Res>? get config; +} + +/// @nodoc +class _$SpeedTestRunStateCopyWithImpl<$Res, $Val extends SpeedTestRunState> + implements $SpeedTestRunStateCopyWith<$Res> { + _$SpeedTestRunStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? executionStatus = null, + Object? progress = null, + Object? statusMessage = freezed, + Object? downloadSpeed = null, + Object? uploadSpeed = null, + Object? latency = null, + Object? testPassed = freezed, + Object? errorMessage = freezed, + Object? localIpAddress = freezed, + Object? gatewayAddress = freezed, + Object? serverHost = null, + Object? serverPort = null, + Object? testDuration = null, + Object? bandwidthMbps = null, + Object? parallelStreams = null, + Object? useUdp = null, + Object? completedResult = freezed, + Object? config = freezed, + Object? isInitialized = null, + }) { + return _then(_value.copyWith( + executionStatus: null == executionStatus + ? _value.executionStatus + : executionStatus // ignore: cast_nullable_to_non_nullable + as SpeedTestStatus, + progress: null == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as double, + statusMessage: freezed == statusMessage + ? _value.statusMessage + : statusMessage // ignore: cast_nullable_to_non_nullable + as String?, + downloadSpeed: null == downloadSpeed + ? _value.downloadSpeed + : downloadSpeed // ignore: cast_nullable_to_non_nullable + as double, + uploadSpeed: null == uploadSpeed + ? _value.uploadSpeed + : uploadSpeed // ignore: cast_nullable_to_non_nullable + as double, + latency: null == latency + ? _value.latency + : latency // ignore: cast_nullable_to_non_nullable + as double, + testPassed: freezed == testPassed + ? _value.testPassed + : testPassed // ignore: cast_nullable_to_non_nullable + as bool?, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + localIpAddress: freezed == localIpAddress + ? _value.localIpAddress + : localIpAddress // ignore: cast_nullable_to_non_nullable + as String?, + gatewayAddress: freezed == gatewayAddress + ? _value.gatewayAddress + : gatewayAddress // ignore: cast_nullable_to_non_nullable + as String?, + serverHost: null == serverHost + ? _value.serverHost + : serverHost // ignore: cast_nullable_to_non_nullable + as String, + serverPort: null == serverPort + ? _value.serverPort + : serverPort // ignore: cast_nullable_to_non_nullable + as int, + testDuration: null == testDuration + ? _value.testDuration + : testDuration // ignore: cast_nullable_to_non_nullable + as int, + bandwidthMbps: null == bandwidthMbps + ? _value.bandwidthMbps + : bandwidthMbps // ignore: cast_nullable_to_non_nullable + as int, + parallelStreams: null == parallelStreams + ? _value.parallelStreams + : parallelStreams // ignore: cast_nullable_to_non_nullable + as int, + useUdp: null == useUdp + ? _value.useUdp + : useUdp // ignore: cast_nullable_to_non_nullable + as bool, + completedResult: freezed == completedResult + ? _value.completedResult + : completedResult // ignore: cast_nullable_to_non_nullable + as SpeedTestResult?, + config: freezed == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as SpeedTestConfig?, + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $SpeedTestResultCopyWith<$Res>? get completedResult { + if (_value.completedResult == null) { + return null; + } + + return $SpeedTestResultCopyWith<$Res>(_value.completedResult!, (value) { + return _then(_value.copyWith(completedResult: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $SpeedTestConfigCopyWith<$Res>? get config { + if (_value.config == null) { + return null; + } + + return $SpeedTestConfigCopyWith<$Res>(_value.config!, (value) { + return _then(_value.copyWith(config: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SpeedTestRunStateImplCopyWith<$Res> + implements $SpeedTestRunStateCopyWith<$Res> { + factory _$$SpeedTestRunStateImplCopyWith(_$SpeedTestRunStateImpl value, + $Res Function(_$SpeedTestRunStateImpl) then) = + __$$SpeedTestRunStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized}); + + @override + $SpeedTestResultCopyWith<$Res>? get completedResult; + @override + $SpeedTestConfigCopyWith<$Res>? get config; +} + +/// @nodoc +class __$$SpeedTestRunStateImplCopyWithImpl<$Res> + extends _$SpeedTestRunStateCopyWithImpl<$Res, _$SpeedTestRunStateImpl> + implements _$$SpeedTestRunStateImplCopyWith<$Res> { + __$$SpeedTestRunStateImplCopyWithImpl(_$SpeedTestRunStateImpl _value, + $Res Function(_$SpeedTestRunStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? executionStatus = null, + Object? progress = null, + Object? statusMessage = freezed, + Object? downloadSpeed = null, + Object? uploadSpeed = null, + Object? latency = null, + Object? testPassed = freezed, + Object? errorMessage = freezed, + Object? localIpAddress = freezed, + Object? gatewayAddress = freezed, + Object? serverHost = null, + Object? serverPort = null, + Object? testDuration = null, + Object? bandwidthMbps = null, + Object? parallelStreams = null, + Object? useUdp = null, + Object? completedResult = freezed, + Object? config = freezed, + Object? isInitialized = null, + }) { + return _then(_$SpeedTestRunStateImpl( + executionStatus: null == executionStatus + ? _value.executionStatus + : executionStatus // ignore: cast_nullable_to_non_nullable + as SpeedTestStatus, + progress: null == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as double, + statusMessage: freezed == statusMessage + ? _value.statusMessage + : statusMessage // ignore: cast_nullable_to_non_nullable + as String?, + downloadSpeed: null == downloadSpeed + ? _value.downloadSpeed + : downloadSpeed // ignore: cast_nullable_to_non_nullable + as double, + uploadSpeed: null == uploadSpeed + ? _value.uploadSpeed + : uploadSpeed // ignore: cast_nullable_to_non_nullable + as double, + latency: null == latency + ? _value.latency + : latency // ignore: cast_nullable_to_non_nullable + as double, + testPassed: freezed == testPassed + ? _value.testPassed + : testPassed // ignore: cast_nullable_to_non_nullable + as bool?, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + localIpAddress: freezed == localIpAddress + ? _value.localIpAddress + : localIpAddress // ignore: cast_nullable_to_non_nullable + as String?, + gatewayAddress: freezed == gatewayAddress + ? _value.gatewayAddress + : gatewayAddress // ignore: cast_nullable_to_non_nullable + as String?, + serverHost: null == serverHost + ? _value.serverHost + : serverHost // ignore: cast_nullable_to_non_nullable + as String, + serverPort: null == serverPort + ? _value.serverPort + : serverPort // ignore: cast_nullable_to_non_nullable + as int, + testDuration: null == testDuration + ? _value.testDuration + : testDuration // ignore: cast_nullable_to_non_nullable + as int, + bandwidthMbps: null == bandwidthMbps + ? _value.bandwidthMbps + : bandwidthMbps // ignore: cast_nullable_to_non_nullable + as int, + parallelStreams: null == parallelStreams + ? _value.parallelStreams + : parallelStreams // ignore: cast_nullable_to_non_nullable + as int, + useUdp: null == useUdp + ? _value.useUdp + : useUdp // ignore: cast_nullable_to_non_nullable + as bool, + completedResult: freezed == completedResult + ? _value.completedResult + : completedResult // ignore: cast_nullable_to_non_nullable + as SpeedTestResult?, + config: freezed == config + ? _value.config + : config // ignore: cast_nullable_to_non_nullable + as SpeedTestConfig?, + isInitialized: null == isInitialized + ? _value.isInitialized + : isInitialized // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$SpeedTestRunStateImpl extends _SpeedTestRunState { + const _$SpeedTestRunStateImpl( + {this.executionStatus = SpeedTestStatus.idle, + this.progress = 0.0, + this.statusMessage, + this.downloadSpeed = 0.0, + this.uploadSpeed = 0.0, + this.latency = 0.0, + this.testPassed, + this.errorMessage, + this.localIpAddress, + this.gatewayAddress, + this.serverHost = '', + this.serverPort = 5201, + this.testDuration = 10, + this.bandwidthMbps = 0, + this.parallelStreams = 1, + this.useUdp = false, + this.completedResult, + this.config, + this.isInitialized = false}) + : super._(); + +// Execution status (idle, running, completed, error) + @override + @JsonKey() + final SpeedTestStatus executionStatus; +// Progress (0-100) + @override + @JsonKey() + final double progress; +// Status message (for UI display) + @override + final String? statusMessage; +// Result data (from result stream) + @override + @JsonKey() + final double downloadSpeed; + @override + @JsonKey() + final double uploadSpeed; + @override + @JsonKey() + final double latency; +// Validation status: null = not run, true = passed, false = failed + @override + final bool? testPassed; +// Error state + @override + final String? errorMessage; +// Network info + @override + final String? localIpAddress; + @override + final String? gatewayAddress; +// Server configuration + @override + @JsonKey() + final String serverHost; + @override + @JsonKey() + final int serverPort; +// Test configuration + @override + @JsonKey() + final int testDuration; + @override + @JsonKey() + final int bandwidthMbps; + @override + @JsonKey() + final int parallelStreams; + @override + @JsonKey() + final bool useUdp; +// Full result object (for submission) + @override + final SpeedTestResult? completedResult; + @override + final SpeedTestConfig? config; +// Initialization flag + @override + @JsonKey() + final bool isInitialized; + + @override + String toString() { + return 'SpeedTestRunState(executionStatus: $executionStatus, progress: $progress, statusMessage: $statusMessage, downloadSpeed: $downloadSpeed, uploadSpeed: $uploadSpeed, latency: $latency, testPassed: $testPassed, errorMessage: $errorMessage, localIpAddress: $localIpAddress, gatewayAddress: $gatewayAddress, serverHost: $serverHost, serverPort: $serverPort, testDuration: $testDuration, bandwidthMbps: $bandwidthMbps, parallelStreams: $parallelStreams, useUdp: $useUdp, completedResult: $completedResult, config: $config, isInitialized: $isInitialized)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SpeedTestRunStateImpl && + (identical(other.executionStatus, executionStatus) || + other.executionStatus == executionStatus) && + (identical(other.progress, progress) || + other.progress == progress) && + (identical(other.statusMessage, statusMessage) || + other.statusMessage == statusMessage) && + (identical(other.downloadSpeed, downloadSpeed) || + other.downloadSpeed == downloadSpeed) && + (identical(other.uploadSpeed, uploadSpeed) || + other.uploadSpeed == uploadSpeed) && + (identical(other.latency, latency) || other.latency == latency) && + (identical(other.testPassed, testPassed) || + other.testPassed == testPassed) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage) && + (identical(other.localIpAddress, localIpAddress) || + other.localIpAddress == localIpAddress) && + (identical(other.gatewayAddress, gatewayAddress) || + other.gatewayAddress == gatewayAddress) && + (identical(other.serverHost, serverHost) || + other.serverHost == serverHost) && + (identical(other.serverPort, serverPort) || + other.serverPort == serverPort) && + (identical(other.testDuration, testDuration) || + other.testDuration == testDuration) && + (identical(other.bandwidthMbps, bandwidthMbps) || + other.bandwidthMbps == bandwidthMbps) && + (identical(other.parallelStreams, parallelStreams) || + other.parallelStreams == parallelStreams) && + (identical(other.useUdp, useUdp) || other.useUdp == useUdp) && + (identical(other.completedResult, completedResult) || + other.completedResult == completedResult) && + (identical(other.config, config) || other.config == config) && + (identical(other.isInitialized, isInitialized) || + other.isInitialized == isInitialized)); + } + + @override + int get hashCode => Object.hashAll([ + runtimeType, + executionStatus, + progress, + statusMessage, + downloadSpeed, + uploadSpeed, + latency, + testPassed, + errorMessage, + localIpAddress, + gatewayAddress, + serverHost, + serverPort, + testDuration, + bandwidthMbps, + parallelStreams, + useUdp, + completedResult, + config, + isInitialized + ]); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SpeedTestRunStateImplCopyWith<_$SpeedTestRunStateImpl> get copyWith => + __$$SpeedTestRunStateImplCopyWithImpl<_$SpeedTestRunStateImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized) + $default, + ) { + return $default( + executionStatus, + progress, + statusMessage, + downloadSpeed, + uploadSpeed, + latency, + testPassed, + errorMessage, + localIpAddress, + gatewayAddress, + serverHost, + serverPort, + testDuration, + bandwidthMbps, + parallelStreams, + useUdp, + completedResult, + config, + isInitialized); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult? Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized)? + $default, + ) { + return $default?.call( + executionStatus, + progress, + statusMessage, + downloadSpeed, + uploadSpeed, + latency, + testPassed, + errorMessage, + localIpAddress, + gatewayAddress, + serverHost, + serverPort, + testDuration, + bandwidthMbps, + parallelStreams, + useUdp, + completedResult, + config, + isInitialized); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + SpeedTestStatus executionStatus, + double progress, + String? statusMessage, + double downloadSpeed, + double uploadSpeed, + double latency, + bool? testPassed, + String? errorMessage, + String? localIpAddress, + String? gatewayAddress, + String serverHost, + int serverPort, + int testDuration, + int bandwidthMbps, + int parallelStreams, + bool useUdp, + SpeedTestResult? completedResult, + SpeedTestConfig? config, + bool isInitialized)? + $default, { + required TResult orElse(), + }) { + if ($default != null) { + return $default( + executionStatus, + progress, + statusMessage, + downloadSpeed, + uploadSpeed, + latency, + testPassed, + errorMessage, + localIpAddress, + gatewayAddress, + serverHost, + serverPort, + testDuration, + bandwidthMbps, + parallelStreams, + useUdp, + completedResult, + config, + isInitialized); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(_SpeedTestRunState value) $default, + ) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult? Function(_SpeedTestRunState value)? $default, + ) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(_SpeedTestRunState value)? $default, { + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class _SpeedTestRunState extends SpeedTestRunState { + const factory _SpeedTestRunState( + {final SpeedTestStatus executionStatus, + final double progress, + final String? statusMessage, + final double downloadSpeed, + final double uploadSpeed, + final double latency, + final bool? testPassed, + final String? errorMessage, + final String? localIpAddress, + final String? gatewayAddress, + final String serverHost, + final int serverPort, + final int testDuration, + final int bandwidthMbps, + final int parallelStreams, + final bool useUdp, + final SpeedTestResult? completedResult, + final SpeedTestConfig? config, + final bool isInitialized}) = _$SpeedTestRunStateImpl; + const _SpeedTestRunState._() : super._(); + + @override // Execution status (idle, running, completed, error) + SpeedTestStatus get executionStatus; + @override // Progress (0-100) + double get progress; + @override // Status message (for UI display) + String? get statusMessage; + @override // Result data (from result stream) + double get downloadSpeed; + @override + double get uploadSpeed; + @override + double get latency; + @override // Validation status: null = not run, true = passed, false = failed + bool? get testPassed; + @override // Error state + String? get errorMessage; + @override // Network info + String? get localIpAddress; + @override + String? get gatewayAddress; + @override // Server configuration + String get serverHost; + @override + int get serverPort; + @override // Test configuration + int get testDuration; + @override + int get bandwidthMbps; + @override + int get parallelStreams; + @override + bool get useUdp; + @override // Full result object (for submission) + SpeedTestResult? get completedResult; + @override + SpeedTestConfig? get config; + @override // Initialization flag + bool get isInitialized; + @override + @JsonKey(ignore: true) + _$$SpeedTestRunStateImplCopyWith<_$SpeedTestRunStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart b/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart new file mode 100644 index 0000000..6f30f25 --- /dev/null +++ b/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart @@ -0,0 +1,1024 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; +import 'package:rgnets_fdk/features/speed_test/presentation/providers/speed_test_providers.dart'; +import 'package:rgnets_fdk/features/speed_test/presentation/widgets/speed_test_popup.dart'; + +/// Helper class to group test configuration with its results +class SpeedTestWithResults { + final SpeedTestConfig config; + final List results; + + SpeedTestWithResults({ + required this.config, + required this.results, + }); +} + +/// Widget that displays speed test results for a PMS room with a dropdown selector. +/// +/// Shows a card with a dropdown to select from available speed test results, +/// grouped by test configuration. Each result displays pass/fail status and metrics. +class RoomSpeedTestSelector extends ConsumerStatefulWidget { + final int pmsRoomId; + final String roomName; + final String? roomType; + final List apIds; + + const RoomSpeedTestSelector({ + super.key, + required this.pmsRoomId, + required this.roomName, + this.roomType, + this.apIds = const [], + }); + + @override + ConsumerState createState() => + _RoomSpeedTestSelectorState(); +} + +class _RoomSpeedTestSelectorState extends ConsumerState { + List _speedTests = []; + bool _isLoading = true; + bool _isUpdating = false; + String? _errorMessage; + SpeedTestResult? _selectedResult; + + @override + void initState() { + super.initState(); + _loadSpeedTests(); + } + + Future _loadSpeedTests() async { + if (!mounted) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + + // Get all speed test results from cache + final allResults = cacheIntegration.getCachedSpeedTestResults(); + + LoggerService.info( + 'Loading speed tests for room ${widget.pmsRoomId}: Found ${allResults.length} total results', + tag: 'RoomSpeedTestSelector', + ); + + if (allResults.isEmpty) { + setState(() { + _speedTests = []; + _selectedResult = null; + _isLoading = false; + }); + return; + } + + // Filter results by pms_room_id and group by speed_test_id + final Map> resultsByTestId = {}; + + for (final result in allResults) { + // Only include results for this room + if (result.pmsRoomId != widget.pmsRoomId) continue; + + final speedTestId = result.speedTestId; + if (speedTestId == null) continue; + + resultsByTestId.putIfAbsent(speedTestId, () => []); + resultsByTestId[speedTestId]!.add(result); + } + + LoggerService.info( + 'Filtered results for room ${widget.pmsRoomId}: ${resultsByTestId.length} unique tests', + tag: 'RoomSpeedTestSelector', + ); + + if (resultsByTestId.isEmpty) { + setState(() { + _speedTests = []; + _selectedResult = null; + _isLoading = false; + }); + return; + } + + // Get speed test configurations + final configs = cacheIntegration.getCachedSpeedTestConfigs(); + final Map configsById = {}; + for (final config in configs) { + if (config.id != null) { + configsById[config.id!] = config; + } + } + + // Build SpeedTestWithResults for each speed_test_id + final List speedTestsWithResults = []; + + for (final entry in resultsByTestId.entries) { + final speedTestId = entry.key; + final results = entry.value; + + // Sort results by timestamp (most recent first) + results.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + // Get the config + final config = configsById[speedTestId]; + if (config == null) continue; + + speedTestsWithResults.add(SpeedTestWithResults( + config: config, + results: results, + )); + } + + // Restore selection if possible + SpeedTestResult? updatedSelection; + if (_selectedResult != null) { + for (final group in speedTestsWithResults) { + for (final result in group.results) { + if (result.id == _selectedResult!.id) { + updatedSelection = result; + break; + } + } + if (updatedSelection != null) break; + } + } + + setState(() { + _speedTests = speedTestsWithResults; + if (updatedSelection != null) { + _selectedResult = updatedSelection; + } else if (speedTestsWithResults.isNotEmpty && + speedTestsWithResults.first.results.isNotEmpty) { + _selectedResult = speedTestsWithResults.first.results.first; + } + _isLoading = false; + }); + } catch (e) { + LoggerService.error( + 'Failed to load speed tests: $e', + tag: 'RoomSpeedTestSelector', + ); + setState(() { + _errorMessage = 'Failed to load speed tests: $e'; + _speedTests = []; + _selectedResult = null; + _isLoading = false; + }); + } + } + + bool _metricMeetsThreshold(double? value, double? minRequired) { + if (minRequired == null) return true; + if (value == null) return false; + return value >= minRequired; + } + + bool _resultPassesForConfig(SpeedTestResult result, SpeedTestConfig config) { + final downloadPass = + _metricMeetsThreshold(result.downloadMbps, config.minDownloadMbps); + final uploadPass = + _metricMeetsThreshold(result.uploadMbps, config.minUploadMbps); + + return result.passed || (downloadPass && uploadPass); + } + + Future _toggleApplicable(SpeedTestResult result) async { + if (_isUpdating) return; + + setState(() { + _isUpdating = true; + }); + + try { + // Create updated result with toggled isApplicable + final updatedResult = result.copyWith( + isApplicable: !result.isApplicable, + ); + + LoggerService.info( + 'Toggling isApplicable for result ${result.id}: ' + '${result.isApplicable} -> ${!result.isApplicable}', + tag: 'RoomSpeedTestSelector', + ); + + // Update via provider + final notifier = ref.read( + speedTestResultsNotifierProvider( + speedTestId: result.speedTestId, + ).notifier, + ); + + final updated = await notifier.updateResult(updatedResult); + + if (updated != null) { + // Update the cache so _loadSpeedTests sees the new value + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + cacheIntegration.updateSpeedTestResultInCache(updated); + + // Update the selected result locally + setState(() { + _selectedResult = updated; + }); + // Reload to get fresh data from cache + await _loadSpeedTests(); + } else { + LoggerService.error( + 'Failed to update result ${result.id}', + tag: 'RoomSpeedTestSelector', + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to update result'), + backgroundColor: AppColors.error, + ), + ); + } + } + } catch (e) { + LoggerService.error( + 'Error toggling isApplicable: $e', + tag: 'RoomSpeedTestSelector', + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: AppColors.error, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isUpdating = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return Card( + margin: const EdgeInsets.all(16), + color: AppColors.cardDark, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primary, + ), + ), + const SizedBox(width: 12), + Text( + 'Loading speed tests...', + style: TextStyle(color: AppColors.textSecondary), + ), + ], + ), + ), + ); + } + + if (_errorMessage != null) { + return Card( + margin: const EdgeInsets.all(16), + color: AppColors.error.withValues(alpha: 0.15), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.error, color: AppColors.error), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: TextStyle(color: AppColors.error), + ), + ), + ], + ), + ), + ); + } + + if (_speedTests.isEmpty) { + return Card( + margin: const EdgeInsets.all(16), + color: AppColors.cardDark, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.info_outline, color: AppColors.textSecondary), + const SizedBox(width: 12), + Expanded( + child: Text( + 'No speed test results for this room', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + IconButton( + icon: Icon(Icons.refresh, color: AppColors.textSecondary), + tooltip: 'Refresh', + onPressed: _loadSpeedTests, + ), + ], + ), + ), + ); + } + + return _buildUnifiedSpeedTestSelector(); + } + + Widget _buildUnifiedSpeedTestSelector() { + // Collect all results with their config + List> allResultsWithConfig = []; + + for (final testWithResults in _speedTests) { + for (final result in testWithResults.results) { + final bool passed = + _resultPassesForConfig(result, testWithResults.config); + allResultsWithConfig.add({ + 'result': result, + 'config': testWithResults.config, + 'passed': passed, + }); + } + } + + // Sort: passed first, then by most recent + allResultsWithConfig.sort((a, b) { + final bool aPassed = a['passed'] as bool? ?? false; + final bool bPassed = b['passed'] as bool? ?? false; + + if (aPassed && !bPassed) return -1; + if (!aPassed && bPassed) return 1; + + final aTime = (a['result'] as SpeedTestResult).timestamp; + final bTime = (b['result'] as SpeedTestResult).timestamp; + return bTime.compareTo(aTime); + }); + + final Map selectedEntry = (_selectedResult != null) + ? allResultsWithConfig.firstWhere( + (item) => + (item['result'] as SpeedTestResult).id == _selectedResult!.id, + orElse: () => allResultsWithConfig.first, + ) + : allResultsWithConfig.first; + + final currentResult = selectedEntry['result'] as SpeedTestResult; + final selectedConfig = selectedEntry['config'] as SpeedTestConfig; + final bool selectedPassed = selectedEntry['passed'] as bool? ?? + _resultPassesForConfig(currentResult, selectedConfig); + final String resultLabel = + (currentResult.roomType != null && currentResult.roomType!.isNotEmpty) + ? currentResult.roomType! + : 'Result #${currentResult.id?.toString() ?? "-"}'; + + return Card( + margin: const EdgeInsets.all(16), + color: AppColors.cardDark, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Icon(Icons.speed, color: AppColors.primary), + const SizedBox(width: 8), + Text( + 'Speed Test Results', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const Spacer(), + IconButton( + icon: Icon(Icons.refresh, size: 20, color: AppColors.textSecondary), + tooltip: 'Refresh', + onPressed: _loadSpeedTests, + ), + ], + ), + const SizedBox(height: 16), + + // Dropdown button + OutlinedButton( + onPressed: () async { + final selected = + await _showAllResultsModal(allResultsWithConfig); + if (selected != null) { + setState(() { + _selectedResult = selected; + }); + } + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + side: BorderSide(color: AppColors.gray600), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Select Result (${allResultsWithConfig.length} total)', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: 4), + Text( + resultLabel, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 8), + Icon( + !currentResult.isApplicable + ? Icons.remove_circle_outline + : (selectedPassed ? Icons.check_circle : Icons.cancel), + size: 24, + color: !currentResult.isApplicable + ? AppColors.textSecondary + : (selectedPassed ? AppColors.success : AppColors.error), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_drop_down, + color: AppColors.textSecondary, + ), + ], + ), + ), + + const SizedBox(height: 16), + _buildResultDetails(currentResult, selectedConfig), + + // Run test button (hidden when result is not applicable) + if (currentResult.isApplicable) ...[ + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + int? testedViaAccessPointId = + currentResult.testedViaAccessPointId; + if (testedViaAccessPointId == null && + widget.apIds.isNotEmpty) { + testedViaAccessPointId = widget.apIds.first; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => SpeedTestPopup( + cachedTest: selectedConfig, + existingResult: currentResult, + apId: testedViaAccessPointId, + onCompleted: () { + _loadSpeedTests(); + }, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('Run Test'), + ), + ), + ], + ], + ), + ), + ); + } + + String _capitalizeEachWord(String text) { + return text.split(' ').map((word) { + if (word.isEmpty) return word; + return word[0].toUpperCase() + word.substring(1).toLowerCase(); + }).join(' '); + } + + String? _getSpeedTestIcon(String testName) { + final lowerName = testName.toLowerCase(); + + if (lowerName.contains('coverage')) { + return 'assets/speed_test_indicator_img/coverage.png'; + } else if (lowerName.contains('ont')) { + return 'assets/speed_test_indicator_img/validation_ont.png'; + } else if (lowerName.contains('access point') || lowerName.contains('ap')) { + return 'assets/speed_test_indicator_img/validation_ap.png'; + } + + return null; + } + + Future _showAllResultsModal( + List> allResults) async { + // Group results by speed test config + Map>> groupedResults = {}; + for (final item in allResults) { + final config = item['config'] as SpeedTestConfig; + if (config.id == null) continue; + groupedResults.putIfAbsent(config.id!, () => []); + groupedResults[config.id!]!.add(item); + } + + // Build list of widgets with headers + List listItems = []; + + groupedResults.forEach((speedTestId, items) { + final config = items.first['config'] as SpeedTestConfig; + final testName = config.name ?? 'Speed Test #$speedTestId'; + final iconPath = _getSpeedTestIcon(testName); + + // Section header + listItems.add( + Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + children: [ + iconPath != null + ? Image.asset( + iconPath, + width: 40, + height: 40, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon(Icons.speed, + size: 36, color: AppColors.textSecondary); + }, + ) + : Icon(Icons.speed, size: 36, color: AppColors.textSecondary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _capitalizeEachWord(testName), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + if (config.minDownloadMbps != null || + config.minUploadMbps != null) + Text( + 'Min: ${config.minDownloadMbps?.toStringAsFixed(0) ?? "?"} Mbps down / ${config.minUploadMbps?.toStringAsFixed(0) ?? "?"} Mbps up', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + ], + ), + ), + ); + + // Result items + for (final item in items) { + final result = item['result'] as SpeedTestResult; + final isSelected = result.id == _selectedResult?.id; + final bool passed = + item['passed'] as bool? ?? _resultPassesForConfig(result, config); + + String displayText = ''; + if (result.roomType != null && result.roomType!.isNotEmpty) { + displayText = result.roomType!; + } else { + displayText = config.name ?? 'Result #${result.id}'; + } + + listItems.add( + ListTile( + selected: isSelected, + selectedTileColor: AppColors.primary.withValues(alpha: 0.15), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: !result.isApplicable + ? AppColors.gray500 + : (passed ? AppColors.success : AppColors.error), + ), + ), + title: Text( + displayText, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontSize: 14, + color: AppColors.textPrimary, + ), + ), + subtitle: !result.isApplicable + ? Text( + 'Not Applicable', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary, + fontStyle: FontStyle.italic, + ), + ) + : (result.downloadMbps != null + ? Text( + '↓ ${result.downloadMbps!.toStringAsFixed(1)} Mbps ↑ ${result.uploadMbps?.toStringAsFixed(1) ?? "N/A"} Mbps', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary, + ), + ) + : null), + trailing: Icon( + !result.isApplicable + ? Icons.remove_circle_outline + : (passed ? Icons.check_circle : Icons.cancel), + size: 28, + color: !result.isApplicable + ? AppColors.textSecondary + : (passed ? AppColors.success : AppColors.error), + ), + onTap: () { + Navigator.pop(context, result); + }, + ), + ); + } + + // Separator + listItems.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Divider( + height: 1, + thickness: 1.5, + color: AppColors.gray700, + ), + ), + ); + }); + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.9, + minChildSize: 0.4, + expand: false, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: AppColors.surfaceDark, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + // Handle bar + Container( + margin: const EdgeInsets.only(top: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppColors.gray600, + borderRadius: BorderRadius.circular(2), + ), + ), + // Header + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.history, color: AppColors.primary), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Select Result (${allResults.length} total)', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, color: AppColors.textSecondary), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + Divider(height: 1, color: AppColors.gray700), + // Results list + Expanded( + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: listItems.length, + itemBuilder: (context, index) => listItems[index], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + Widget _buildResultDetails(SpeedTestResult result, SpeedTestConfig config) { + final downloadPassed = + _metricMeetsThreshold(result.downloadMbps, config.minDownloadMbps); + final uploadPassed = + _metricMeetsThreshold(result.uploadMbps, config.minUploadMbps); + final bool passed = _resultPassesForConfig(result, config); + + final bgColor = !result.isApplicable + ? AppColors.gray800 + : (passed ? AppColors.success.withValues(alpha: 0.15) : AppColors.error.withValues(alpha: 0.15)); + final borderColor = !result.isApplicable + ? AppColors.gray700 + : (passed ? AppColors.success.withValues(alpha: 0.4) : AppColors.error.withValues(alpha: 0.4)); + final statusColor = !result.isApplicable + ? AppColors.textSecondary + : (passed ? AppColors.success : AppColors.error); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: borderColor), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + !result.isApplicable + ? Icons.remove_circle_outline + : (passed ? Icons.check_circle : Icons.cancel), + color: statusColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + !result.isApplicable + ? 'Not Applicable' + : (passed ? 'Passed' : 'Failed'), + style: TextStyle( + fontWeight: FontWeight.bold, + color: statusColor, + ), + ), + ], + ), + if (result.completedAt != null) + Text( + _formatTimeSince(result.completedAt!), + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + const SizedBox(height: 12), + + // Metrics row + Row( + children: [ + Expanded( + child: _buildMetric( + 'Download', + result.downloadMbps != null + ? '${result.downloadMbps!.toStringAsFixed(1)} Mbps' + : 'N/A', + Icons.download, + passed: result.isApplicable ? downloadPassed : null, + minRequired: config.minDownloadMbps, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildMetric( + 'Upload', + result.uploadMbps != null + ? '${result.uploadMbps!.toStringAsFixed(1)} Mbps' + : 'N/A', + Icons.upload, + passed: result.isApplicable ? uploadPassed : null, + minRequired: config.minUploadMbps, + ), + ), + ], + ), + + // Latency row + if (result.rtt != null || result.jitter != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + if (result.rtt != null) + Expanded( + child: _buildMetric( + 'Latency', + '${result.rtt!.toStringAsFixed(1)} ms', + Icons.timer, + ), + ), + if (result.rtt != null && result.jitter != null) + const SizedBox(width: 12), + if (result.jitter != null) + Expanded( + child: _buildMetric( + 'Jitter', + '${result.jitter!.toStringAsFixed(1)} ms', + Icons.show_chart, + ), + ), + ], + ), + ], + + // Server info + if (config.target != null) ...[ + const SizedBox(height: 8), + Text( + 'Server: ${config.target}:${config.port ?? 5201}', + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary, + ), + ), + ], + + // Toggle applicable button + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton.icon( + onPressed: _isUpdating ? null : () => _toggleApplicable(result), + icon: _isUpdating + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.textSecondary, + ), + ) + : Icon( + result.isApplicable + ? Icons.block + : Icons.check_circle_outline, + size: 18, + color: AppColors.textSecondary, + ), + label: Text( + result.isApplicable + ? 'Mark as Not Applicable' + : 'Mark as Applicable', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + side: BorderSide(color: AppColors.gray600), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildMetric(String label, String value, IconData icon, + {bool? passed, double? minRequired}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: AppColors.textSecondary), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 11, + color: AppColors.textSecondary, + ), + ), + if (passed != null) ...[ + const SizedBox(width: 4), + Icon( + passed ? Icons.check_circle : Icons.cancel, + size: 14, + color: passed ? AppColors.success : AppColors.error, + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + if (minRequired != null) + Text( + 'Min: ${minRequired.toStringAsFixed(0)} Mbps', + style: TextStyle( + fontSize: 10, + color: AppColors.textSecondary, + fontStyle: FontStyle.italic, + ), + ), + ], + ); + } + + String _formatTimeSince(DateTime dateTime) { + final now = DateTime.now(); + final difference = now.difference(dateTime); + + if (difference.inDays > 0) { + return '${difference.inDays}d ago'; + } else if (difference.inHours > 0) { + return '${difference.inHours}h ago'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes}m ago'; + } else { + return 'Just now'; + } + } +} diff --git a/lib/features/speed_test/presentation/widgets/speed_test_card.dart b/lib/features/speed_test/presentation/widgets/speed_test_card.dart index c6acea4..df6c4e0 100644 --- a/lib/features/speed_test/presentation/widgets/speed_test_card.dart +++ b/lib/features/speed_test/presentation/widgets/speed_test_card.dart @@ -1,20 +1,22 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:rgnets_fdk/core/theme/app_colors.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/features/speed_test/data/services/speed_test_service.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_status.dart'; import 'package:rgnets_fdk/features/speed_test/presentation/widgets/speed_test_popup.dart'; -class SpeedTestCard extends StatefulWidget { +class SpeedTestCard extends ConsumerStatefulWidget { const SpeedTestCard({super.key}); @override - State createState() => _SpeedTestCardState(); + ConsumerState createState() => _SpeedTestCardState(); } -class _SpeedTestCardState extends State { +class _SpeedTestCardState extends ConsumerState { final SpeedTestService _speedTestService = SpeedTestService(); SpeedTestStatus _status = SpeedTestStatus.idle; SpeedTestResult? _lastResult; @@ -115,28 +117,97 @@ class _SpeedTestCardState extends State { Future _showSpeedTestPopup() async { if (!mounted) return; - showDialog( + // Get adhoc config from cache (pre-loaded at WebSocket connect) + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + final adhocConfig = cacheIntegration.getAdhocSpeedTestConfig(); + + if (adhocConfig != null) { + LoggerService.info( + 'Using adhoc config from cache: ${adhocConfig.name} (id: ${adhocConfig.id})', + tag: 'SpeedTestCard', + ); + } else { + LoggerService.info( + 'No configs in cache - running adhoc test without config', + tag: 'SpeedTestCard', + ); + } + + showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return SpeedTestPopup( - onCompleted: () async { + cachedTest: adhocConfig, + onCompleted: () { if (mounted) { LoggerService.info( - 'Speed test completed - reloading result for dashboard', - tag: 'SpeedTestCard'); + 'Speed test completed - reloading result for dashboard', + tag: 'SpeedTestCard', + ); + final result = _speedTestService.lastResult; setState(() { - _lastResult = _speedTestService.lastResult; + _lastResult = result; }); } }, + onResultSubmitted: (result) async { + if (!result.hasError) { + await _submitAdhocResult(result); + } + }, ); }, ); } + /// Submit adhoc speed test result to the server via WebSocket cache integration + Future _submitAdhocResult(SpeedTestResult result) async { + try { + LoggerService.info( + 'Submitting adhoc speed test result: ' + 'source=${result.source}, ' + 'destination=${result.destination}, ' + 'download=${result.downloadMbps}, ' + 'upload=${result.uploadMbps}, ' + 'ping=${result.rtt}', + tag: 'SpeedTestCard', + ); + + final cacheIntegration = ref.read(webSocketCacheIntegrationProvider); + final success = await cacheIntegration.createAdhocSpeedTestResult( + downloadSpeed: result.downloadMbps ?? 0, + uploadSpeed: result.uploadMbps ?? 0, + latency: result.rtt ?? 0, + source: result.source, + destination: result.destination, + port: result.port, + protocol: result.iperfProtocol, + passed: result.passed, + ); + + if (success) { + LoggerService.info( + 'Adhoc speed test result submitted successfully', + tag: 'SpeedTestCard', + ); + } else { + LoggerService.warning( + 'Failed to submit adhoc speed test result', + tag: 'SpeedTestCard', + ); + } + } catch (e) { + LoggerService.error( + 'Error submitting adhoc speed test result', + error: e, + tag: 'SpeedTestCard', + ); + } + } + void _showConfigDialog() { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return AlertDialog( diff --git a/lib/features/speed_test/presentation/widgets/speed_test_popup.dart b/lib/features/speed_test/presentation/widgets/speed_test_popup.dart index 07c7619..fc4fdaf 100644 --- a/lib/features/speed_test/presentation/widgets/speed_test_popup.dart +++ b/lib/features/speed_test/presentation/widgets/speed_test_popup.dart @@ -1,57 +1,58 @@ -import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:rgnets_fdk/core/theme/app_colors.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; -import 'package:rgnets_fdk/features/speed_test/data/services/speed_test_service.dart'; -import 'package:rgnets_fdk/features/speed_test/data/services/network_gateway_service.dart'; +import 'package:rgnets_fdk/core/theme/app_colors.dart'; +import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_status.dart'; -import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; +import 'package:rgnets_fdk/features/speed_test/presentation/providers/speed_test_providers.dart'; -class SpeedTestPopup extends StatefulWidget { +class SpeedTestPopup extends ConsumerStatefulWidget { + /// The speed test configuration final SpeedTestConfig? cachedTest; + final VoidCallback? onCompleted; + /// Callback when result should be submitted (auto-called when test passes) + final void Function(SpeedTestResult result)? onResultSubmitted; + + /// Existing result to update (instead of creating a new one) + final SpeedTestResult? existingResult; + + /// Optional AP ID to display uplink speed (for AP speed tests) + final int? apId; + const SpeedTestPopup({ super.key, this.cachedTest, this.onCompleted, + this.onResultSubmitted, + this.existingResult, + this.apId, }); @override - State createState() => _SpeedTestPopupState(); + ConsumerState createState() => _SpeedTestPopupState(); } -class _SpeedTestPopupState extends State +class _SpeedTestPopupState extends ConsumerState with SingleTickerProviderStateMixin { - final SpeedTestService _speedTestService = SpeedTestService(); - - SpeedTestStatus _status = SpeedTestStatus.idle; - double _downloadSpeed = 0.0; - double _uploadSpeed = 0.0; - double _latency = 0.0; - double _progress = 0.0; - String _currentPhase = 'Ready to start'; - String? _localIp; - String? _gatewayIp; - String? _serverHost; - String _serverLabel = 'Gateway'; - String? _errorMessage; - bool _testPassed = false; - - StreamSubscription? _statusSubscription; - StreamSubscription? _resultSubscription; - StreamSubscription? _progressSubscription; - StreamSubscription? _statusMessageSubscription; - late AnimationController _pulseController; late Animation _pulseAnimation; + bool _resultSubmitted = false; + bool _isSubmitting = false; + bool? _submissionSuccess; + String? _submissionError; @override void initState() { super.initState(); _initializePulseAnimation(); - _initializeService(); + // Initialize notifier (idempotent) + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(speedTestRunNotifierProvider.notifier).initialize(); + }); } void _initializePulseAnimation() { @@ -65,195 +66,184 @@ class _SpeedTestPopupState extends State ); } - Future _initializeService() async { - await _speedTestService.initialize(); + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } - _status = SpeedTestStatus.idle; + /// Get the effective config + SpeedTestConfig? get _effectiveConfig => widget.cachedTest; - final gatewayService = NetworkGatewayService(); - _localIp = await gatewayService.getWifiIP(); + double? _getMinDownload() => _effectiveConfig?.minDownloadMbps; + double? _getMinUpload() => _effectiveConfig?.minUploadMbps; + String? _getConfigTarget() => _effectiveConfig?.target; + String? _getConfigName() => _effectiveConfig?.name; - _gatewayIp = await gatewayService.getWifiGateway(); - _serverHost = _gatewayIp; - _serverLabel = 'Gateway'; + Future _startTest() async { + final notifier = ref.read(speedTestRunNotifierProvider.notifier); + final configTarget = _getConfigTarget(); - if (_localIp == null) { - LoggerService.warning( - 'Could not get device IP - location permission may be required on iOS', - tag: 'SpeedTestPopup'); - } + await notifier.startTest( + config: _effectiveConfig, + configTarget: configTarget, + ); + } + Future _cancelTest() async { + await ref.read(speedTestRunNotifierProvider.notifier).cancelTest(); if (mounted) { - setState(() {}); + Navigator.of(context).pop(); } + } - _statusSubscription = _speedTestService.statusStream.listen((status) { - if (!mounted) return; - setState(() { - _status = status; - _updatePhase(); - }); - }); + Future _handleTestCompleted() async { + if (_resultSubmitted) return; - _resultSubscription = _speedTestService.resultStream.listen((result) { - if (!mounted) return; - setState(() { - final serviceStatus = _speedTestService.status; - - if (result.hasError) { - _errorMessage = result.errorMessage; - _currentPhase = 'Test failed'; - } else { - // Update speeds (either live or final) - if (result.downloadSpeed > 0) _downloadSpeed = result.downloadSpeed; - if (result.uploadSpeed > 0) _uploadSpeed = result.uploadSpeed; - if (result.latency > 0) _latency = result.latency; - - // Only update connection info if it's a final result - if (result.localIpAddress != null) _localIp = result.localIpAddress; - if (result.serverHost != null) _serverHost = result.serverHost; - - // If the service finished but our local status hasn't updated yet, sync it - if (serviceStatus == SpeedTestStatus.completed && - _status != SpeedTestStatus.completed) { - _status = SpeedTestStatus.completed; - } + final testState = ref.read(speedTestRunNotifierProvider); + final result = testState.completedResult; - // Check if this is a complete result - if (result.localIpAddress != null || - _status == SpeedTestStatus.completed || - serviceStatus == SpeedTestStatus.completed) { - _validateTestResults(); - _currentPhase = - _testPassed ? 'Test completed - PASSED!' : 'Test completed'; - } - } - }); - }); + if (result == null) return; - _progressSubscription = _speedTestService.progressStream.listen((progress) { - if (!mounted) return; - setState(() { - _progress = progress; - _updatePhase(); - }); - }); + // Submit result via callback if provided + if (widget.onResultSubmitted != null) { + final submitResult = SpeedTestResult( + downloadMbps: testState.downloadSpeed, + uploadMbps: testState.uploadSpeed, + rtt: testState.latency, + localIpAddress: testState.localIpAddress, + serverHost: testState.serverHost, + speedTestId: _effectiveConfig?.id, + passed: testState.testPassed ?? false, + initiatedAt: result.initiatedAt, + completedAt: DateTime.now(), + testType: 'iperf3', + source: testState.localIpAddress, + destination: testState.serverHost, + port: testState.serverPort, + iperfProtocol: testState.useUdp ? 'udp' : 'tcp', + ); - _statusMessageSubscription = - _speedTestService.statusMessageStream.listen((message) { - if (!mounted) return; - setState(() { - _currentPhase = message; - - // Extract server info from fallback attempt messages - if (message.contains('Default gateway')) { - _serverLabel = 'Gateway'; - final match = RegExp(r'\(([^)]+)\)').firstMatch(message); - if (match != null) { - _serverHost = match.group(1); - } - } else if (message.contains('test configuration') || - message.contains('Test configuration')) { - _serverLabel = 'Target'; - final match = RegExp(r'\(([^)]+)\)').firstMatch(message); - if (match != null) { - _serverHost = match.group(1); - } - } else if (message.contains('external server') || - message.contains('External server')) { - _serverLabel = 'External'; - final match = RegExp(r'\(([^)]+)\)').firstMatch(message); - if (match != null) { - _serverHost = match.group(1); - } - } else if (message.contains('Testing download speed to') || - message.contains('Testing upload speed to')) { - final match = RegExp(r'to ([\w\.\-]+)').firstMatch(message); - if (match != null) { - _serverHost = match.group(1); - _serverLabel = (_serverHost == _gatewayIp) ? 'Gateway' : 'Target'; - } - } - }); - }); - } + LoggerService.info( + 'SpeedTestPopup: Auto-submitting result via callback - passed=${testState.testPassed}, ' + 'download=${testState.downloadSpeed}, upload=${testState.uploadSpeed}', + tag: 'SpeedTestPopup', + ); - void _updatePhase() { - if (_status == SpeedTestStatus.running && - _currentPhase == 'Ready to start') { - if (_progress < 50) { - _currentPhase = 'Testing download speed...'; - } else { - _currentPhase = 'Testing upload speed...'; - } - } else if (_status == SpeedTestStatus.completed && - _currentPhase != 'Test completed!') { - _currentPhase = 'Test completed!'; - } else if (_status == SpeedTestStatus.error && - _currentPhase != 'Test failed') { - _currentPhase = 'Test failed'; + widget.onResultSubmitted?.call(submitResult); + _resultSubmitted = true; + } else if (_effectiveConfig != null) { + // No callback provided, submit internally via provider + await _submitResultInternally(); } } - Future _startTest() async { - final gatewayService = NetworkGatewayService(); - final gatewayIp = await gatewayService.getWifiGateway(); + Future _submitResultInternally() async { + if (_isSubmitting || _resultSubmitted) return; setState(() { - _currentPhase = 'Starting test...'; - _serverLabel = 'Gateway'; - _serverHost = gatewayIp ?? 'Detecting...'; + _isSubmitting = true; + _submissionSuccess = null; + _submissionError = null; }); - String? configTarget; - final cachedTest = widget.cachedTest; - if (cachedTest != null) { - configTarget = cachedTest.target; - } + try { + final existingId = widget.existingResult?.id; + final isUpdate = existingId != null; - await _speedTestService.runSpeedTestWithFallback(configTarget: configTarget); - } + LoggerService.info( + 'SpeedTestPopup: existingResult=${widget.existingResult != null}, ' + 'existingId=$existingId, isUpdate=$isUpdate, ' + 'configId=${_effectiveConfig?.id}', + tag: 'SpeedTestPopup', + ); - void _cancelTest() async { - await _speedTestService.cancelTest(); - if (mounted) { - Navigator.of(context).pop(); - } - } + SpeedTestResult? resultFromServer; - double? _getMinDownload() { - return widget.cachedTest?.minDownloadMbps; - } + if (isUpdate) { + // Update existing result with new test data + final testState = ref.read(speedTestRunNotifierProvider); + final completedResult = testState.completedResult; - double? _getMinUpload() { - return widget.cachedTest?.minUploadMbps; - } + final updatedResult = widget.existingResult!.copyWith( + downloadMbps: testState.downloadSpeed, + uploadMbps: testState.uploadSpeed, + rtt: testState.latency, + passed: testState.testPassed ?? false, + initiatedAt: completedResult?.initiatedAt ?? DateTime.now(), + completedAt: DateTime.now(), + testType: 'iperf3', + source: testState.localIpAddress, + destination: testState.serverHost, + port: testState.serverPort, + iperfProtocol: testState.useUdp ? 'udp' : 'tcp', + ); - void _validateTestResults() { - final cachedTest = widget.cachedTest; + LoggerService.info( + 'SpeedTestPopup: Updating result id=$existingId with ' + 'download=${testState.downloadSpeed}, upload=${testState.uploadSpeed}, ' + 'source=${testState.localIpAddress}, destination=${testState.serverHost}', + tag: 'SpeedTestPopup', + ); - if (cachedTest == null) { - _testPassed = true; - return; - } + final notifier = ref.read( + speedTestResultsNotifierProvider( + speedTestId: updatedResult.speedTestId, + ).notifier, + ); + resultFromServer = await notifier.updateResult(updatedResult); + } else { + // Create new result + LoggerService.info( + 'SpeedTestPopup: Creating new result (no existing result to update)', + tag: 'SpeedTestPopup', + ); + resultFromServer = await ref + .read(speedTestRunNotifierProvider.notifier) + .submitResult(); + } - final minDownload = _getMinDownload(); - final minUpload = _getMinUpload(); + final success = resultFromServer != null; - final downloadPassed = minDownload == null || _downloadSpeed >= minDownload; - final uploadPassed = minUpload == null || _uploadSpeed >= minUpload; + // Update the WebSocket cache so it appears in the list + if (resultFromServer != null) { + ref + .read(webSocketCacheIntegrationProvider) + .updateSpeedTestResultInCache(resultFromServer); + LoggerService.info( + 'SpeedTestPopup: ${isUpdate ? "Updated" : "Added"} result ${resultFromServer.id} in cache', + tag: 'SpeedTestPopup', + ); + } - _testPassed = downloadPassed && uploadPassed; - } + if (mounted) { + setState(() { + _isSubmitting = false; + _submissionSuccess = success; + _resultSubmitted = true; + if (!success) { + _submissionError = 'Failed to ${isUpdate ? "update" : "submit"} result'; + } + }); + } - @override - void dispose() { - _statusSubscription?.cancel(); - _resultSubscription?.cancel(); - _progressSubscription?.cancel(); - _statusMessageSubscription?.cancel(); - _pulseController.dispose(); - super.dispose(); + LoggerService.info( + 'SpeedTestPopup: ${isUpdate ? "Update" : "Submission"} ${success ? "succeeded" : "failed"}', + tag: 'SpeedTestPopup', + ); + } catch (e) { + LoggerService.error( + 'SpeedTestPopup: Error submitting result: $e', + tag: 'SpeedTestPopup', + ); + if (mounted) { + setState(() { + _isSubmitting = false; + _submissionSuccess = false; + _submissionError = e.toString(); + }); + } + } } String _formatSpeed(double speed) { @@ -264,8 +254,8 @@ class _SpeedTestPopupState extends State } } - Color _getStatusColor() { - switch (_status) { + Color _getStatusColor(SpeedTestStatus status) { + switch (status) { case SpeedTestStatus.running: return AppColors.primary; case SpeedTestStatus.completed: @@ -277,8 +267,8 @@ class _SpeedTestPopupState extends State } } - IconData _getStatusIcon() { - switch (_status) { + IconData _getStatusIcon(SpeedTestStatus status) { + switch (status) { case SpeedTestStatus.running: return Icons.speed; case SpeedTestStatus.completed: @@ -336,28 +326,28 @@ class _SpeedTestPopupState extends State color: AppColors.gray500, ), ), - if (minRequired != null) ...[ - const SizedBox(height: 2), - SizedBox( - width: 110, - child: Text( - 'Min: ${_formatSpeed(minRequired)}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 8, - color: AppColors.gray400, - fontStyle: FontStyle.italic, - fontFamily: 'monospace', - ), + const SizedBox(height: 2), + SizedBox( + width: 110, + child: Text( + minRequired != null + ? 'Min: ${_formatSpeed(minRequired)}' + : 'Min: Not set', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 8, + color: AppColors.gray400, + fontStyle: FontStyle.italic, + fontFamily: 'monospace', ), ), - ], + ), ], ), ); } - Widget _buildLatencyIndicator() { + Widget _buildLatencyIndicator(double latency) { return Container( padding: const EdgeInsets.all(12), constraints: const BoxConstraints( @@ -377,7 +367,7 @@ class _SpeedTestPopupState extends State SizedBox( width: 110, child: Text( - '${_latency.toStringAsFixed(0)} ms', + '${latency.toStringAsFixed(0)} ms', textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, @@ -402,8 +392,19 @@ class _SpeedTestPopupState extends State @override Widget build(BuildContext context) { + final testState = ref.watch(speedTestRunNotifierProvider); + final status = testState.executionStatus; + final testPassed = testState.testPassed; + + // Auto-submit when test completes + if (status == SpeedTestStatus.completed && !_resultSubmitted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _handleTestCompleted(); + }); + } + return PopScope( - canPop: _status != SpeedTestStatus.running, + canPop: status != SpeedTestStatus.running, child: Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Container( @@ -420,18 +421,18 @@ class _SpeedTestPopupState extends State animation: _pulseAnimation, builder: (context, child) { return Transform.scale( - scale: _status == SpeedTestStatus.running + scale: status == SpeedTestStatus.running ? _pulseAnimation.value : 1.0, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: _getStatusColor().withOpacity(0.2), + color: _getStatusColor(status).withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( - _getStatusIcon(), - color: _getStatusColor(), + _getStatusIcon(status), + color: _getStatusColor(status), size: 32, ), ), @@ -451,7 +452,7 @@ class _SpeedTestPopupState extends State ), ), Text( - _currentPhase, + testState.statusMessage ?? 'Ready to start', style: TextStyle( fontSize: 14, color: AppColors.gray500, @@ -460,7 +461,7 @@ class _SpeedTestPopupState extends State ], ), ), - if (_status != SpeedTestStatus.running) + if (status != SpeedTestStatus.running) IconButton( icon: const Icon(Icons.close), onPressed: () { @@ -487,11 +488,11 @@ class _SpeedTestPopupState extends State Row( children: [ Icon( - _localIp != null + testState.localIpAddress != null ? Icons.computer : Icons.location_off, size: 16, - color: _localIp != null + color: testState.localIpAddress != null ? AppColors.gray500 : Colors.orange, ), @@ -503,9 +504,9 @@ class _SpeedTestPopupState extends State color: AppColors.gray500, ), ), - if (_localIp != null) + if (testState.localIpAddress != null) Text( - _localIp!, + testState.localIpAddress!, style: TextStyle( fontSize: 12, color: AppColors.gray300, @@ -538,18 +539,18 @@ class _SpeedTestPopupState extends State ), ), Text( - _serverLabel, + 'Target', style: TextStyle( fontSize: 12, color: AppColors.primary, fontWeight: FontWeight.bold, ), ), - if (_serverHost != null) ...[ + if (testState.serverHost.isNotEmpty) ...[ const SizedBox(width: 4), Flexible( child: Text( - '($_serverHost)', + '(${testState.serverHost})', style: TextStyle( fontSize: 11, color: AppColors.gray500, @@ -561,11 +562,218 @@ class _SpeedTestPopupState extends State ], ], ), + // Uplink speed row (for AP speed tests) + if (widget.apId != null) ...[ + const SizedBox(height: 6), + Consumer( + builder: (context, ref, _) { + final uplinkAsync = + ref.watch(apUplinkInfoProvider(widget.apId!)); + return Row( + children: [ + const Icon( + Icons.cable, + size: 16, + color: AppColors.gray500, + ), + const SizedBox(width: 8), + const Text( + 'Uplink: ', + style: TextStyle( + fontSize: 12, + color: AppColors.gray500, + ), + ), + uplinkAsync.when( + data: (uplink) { + if (uplink == null) { + return const Text( + 'Not available', + style: TextStyle( + fontSize: 12, + color: AppColors.warning, + ), + ); + } + final speedBps = uplink.speedInBps; + final speedGbps = speedBps != null + ? speedBps / 1000000000 + : null; + final isSlowUplink = speedBps != null && + speedBps < 2500000000; + return Text( + speedGbps != null + ? '${speedGbps.toStringAsFixed(1)} Gbps' + : 'Unknown', + style: TextStyle( + fontSize: 12, + color: isSlowUplink + ? AppColors.error + : AppColors.gray300, + fontWeight: FontWeight.w600, + fontFamily: 'monospace', + ), + ); + }, + loading: () => const Text( + 'Loading...', + style: TextStyle( + fontSize: 12, + color: AppColors.gray500, + ), + ), + error: (_, __) => const Text( + 'Error', + style: TextStyle( + fontSize: 12, + color: AppColors.error, + ), + ), + ), + ], + ); + }, + ), + ], ], ), ), - const SizedBox(height: 20), + const SizedBox(height: 12), + + // Requirements section (shown when config has thresholds) + if (_effectiveConfig != null && + (_getMinDownload() != null || _getMinUpload() != null)) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.3), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.assignment, + size: 16, + color: AppColors.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _getConfigName() ?? 'Speed Test Requirements', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + // Download requirement + Expanded( + child: Row( + children: [ + Icon( + Icons.download, + size: 14, + color: AppColors.success, + ), + const SizedBox(width: 4), + Text( + 'Min: ', + style: TextStyle( + fontSize: 11, + color: AppColors.gray400, + ), + ), + Text( + _getMinDownload() != null + ? _formatSpeed(_getMinDownload()!) + : 'None', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: AppColors.gray300, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + // Upload requirement + Expanded( + child: Row( + children: [ + Icon( + Icons.upload, + size: 14, + color: AppColors.info, + ), + const SizedBox(width: 4), + Text( + 'Min: ', + style: TextStyle( + fontSize: 11, + color: AppColors.gray400, + ), + ), + Text( + _getMinUpload() != null + ? _formatSpeed(_getMinUpload()!) + : 'None', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: AppColors.gray300, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + ], + ), + // Server fallback info + if (_getConfigTarget() != null) ...[ + const SizedBox(height: 6), + Row( + children: [ + Icon( + Icons.swap_horiz, + size: 14, + color: AppColors.gray500, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Gateway first, then ${_getConfigTarget()}', + style: TextStyle( + fontSize: 10, + color: AppColors.gray500, + fontStyle: FontStyle.italic, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(height: 12), + ], // Speed indicators SizedBox( @@ -575,7 +783,7 @@ class _SpeedTestPopupState extends State Expanded( child: _buildSpeedIndicator( 'Download', - _downloadSpeed, + testState.downloadSpeed, Icons.download, AppColors.success, minRequired: _getMinDownload(), @@ -585,7 +793,7 @@ class _SpeedTestPopupState extends State Expanded( child: _buildSpeedIndicator( 'Upload', - _uploadSpeed, + testState.uploadSpeed, Icons.upload, AppColors.info, minRequired: _getMinUpload(), @@ -600,13 +808,13 @@ class _SpeedTestPopupState extends State // Latency indicator SizedBox( height: 110, - child: _buildLatencyIndicator(), + child: _buildLatencyIndicator(testState.latency), ), const SizedBox(height: 20), // Progress indicator - if (_status == SpeedTestStatus.running) ...[ + if (status == SpeedTestStatus.running) ...[ Center( child: Column( children: [ @@ -614,18 +822,18 @@ class _SpeedTestPopupState extends State width: 48, height: 48, child: CircularProgressIndicator( - valueColor: - AlwaysStoppedAnimation(_getStatusColor()), + valueColor: AlwaysStoppedAnimation( + _getStatusColor(status)), strokeWidth: 4, ), ), const SizedBox(height: 12), Text( - _currentPhase, + testState.statusMessage ?? 'Testing...', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: _getStatusColor(), + color: _getStatusColor(status), ), ), ], @@ -634,14 +842,15 @@ class _SpeedTestPopupState extends State ], // Error message - if (_errorMessage != null) ...[ + if (testState.errorMessage != null) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.error.withOpacity(0.1), borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.error.withOpacity(0.3)), + border: + Border.all(color: AppColors.error.withOpacity(0.3)), ), child: Row( children: [ @@ -650,7 +859,7 @@ class _SpeedTestPopupState extends State const SizedBox(width: 8), Expanded( child: Text( - _errorMessage!, + testState.errorMessage!, style: const TextStyle( color: AppColors.error, fontSize: 12, @@ -663,15 +872,16 @@ class _SpeedTestPopupState extends State ], // Threshold failure alert - if (_status == SpeedTestStatus.completed && !_testPassed) ...[ + if (status == SpeedTestStatus.completed && + testPassed == false) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.warning.withOpacity(0.1), borderRadius: BorderRadius.circular(8), - border: - Border.all(color: AppColors.warning.withOpacity(0.3)), + border: Border.all( + color: AppColors.warning.withOpacity(0.3)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -707,10 +917,115 @@ class _SpeedTestPopupState extends State ), ], + // Submission status feedback + if (status == SpeedTestStatus.completed && _effectiveConfig != null) ...[ + const SizedBox(height: 16), + if (_isSubmitting) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.primary.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.primary, + ), + ), + const SizedBox(width: 12), + Text( + 'Submitting result...', + style: TextStyle( + color: AppColors.primary, + fontSize: 13, + ), + ), + ], + ), + ) + else if (_submissionSuccess == true) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.success.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.success.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.check_circle, + color: AppColors.success, + size: 20, + ), + const SizedBox(width: 12), + Text( + 'Result submitted successfully', + style: TextStyle( + color: AppColors.success, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ) + else if (_submissionSuccess == false) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.error.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.error.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: AppColors.error, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _submissionError ?? 'Failed to submit result', + style: TextStyle( + color: AppColors.error, + fontSize: 13, + ), + ), + ), + TextButton( + onPressed: _submitResultInternally, + child: Text( + 'Retry', + style: TextStyle( + color: AppColors.error, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + // Action buttons const SizedBox(height: 16), - if (_status == SpeedTestStatus.idle) ...[ + if (status == SpeedTestStatus.idle) ...[ Row( children: [ Expanded( @@ -744,7 +1059,7 @@ class _SpeedTestPopupState extends State ), ], - if (_status == SpeedTestStatus.running) ...[ + if (status == SpeedTestStatus.running) ...[ SizedBox( width: double.infinity, child: OutlinedButton.icon( @@ -759,7 +1074,7 @@ class _SpeedTestPopupState extends State ), ], - if (_status == SpeedTestStatus.completed) ...[ + if (status == SpeedTestStatus.completed) ...[ Row( children: [ Expanded( @@ -783,13 +1098,14 @@ class _SpeedTestPopupState extends State child: ElevatedButton.icon( onPressed: () { setState(() { - _downloadSpeed = 0.0; - _uploadSpeed = 0.0; - _latency = 0.0; - _progress = 0.0; - _errorMessage = null; - _testPassed = false; + _resultSubmitted = false; + _isSubmitting = false; + _submissionSuccess = null; + _submissionError = null; }); + ref + .read(speedTestRunNotifierProvider.notifier) + .reset(); _startTest(); }, icon: const Icon(Icons.refresh, size: 16), @@ -806,7 +1122,7 @@ class _SpeedTestPopupState extends State ), ], - if (_status == SpeedTestStatus.error) ...[ + if (status == SpeedTestStatus.error) ...[ Row( children: [ Expanded( @@ -829,12 +1145,14 @@ class _SpeedTestPopupState extends State child: ElevatedButton.icon( onPressed: () { setState(() { - _errorMessage = null; - _downloadSpeed = 0.0; - _uploadSpeed = 0.0; - _latency = 0.0; - _progress = 0.0; + _resultSubmitted = false; + _isSubmitting = false; + _submissionSuccess = null; + _submissionError = null; }); + ref + .read(speedTestRunNotifierProvider.notifier) + .reset(); _startTest(); }, icon: const Icon(Icons.refresh, size: 16), diff --git a/lib/main.dart b/lib/main.dart index d4796ba..c4c3fb9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,10 @@ import 'package:rgnets_fdk/features/auth/presentation/providers/auth_notifier.da import 'package:rgnets_fdk/features/auth/presentation/widgets/credential_approval_sheet.dart'; import 'package:rgnets_fdk/features/initialization/initialization.dart'; import 'package:rgnets_fdk/features/onboarding/data/config/onboarding_config.dart'; +<<<<<<< HEAD import 'package:sentry_flutter/sentry_flutter.dart'; +======= +>>>>>>> 6a559fa (Draft for device onboarding) import 'package:shared_preferences/shared_preferences.dart'; void _configureImageCache() { @@ -78,6 +81,7 @@ void main() async { } }; +<<<<<<< HEAD // Initialize providers with error handling late final SharedPreferences sharedPreferences; try { @@ -107,6 +111,24 @@ void main() async { }, (error, stackTrace) async { await ErrorReporter.report(error, stackTrace: stackTrace); }); +======= + // Initialize onboarding configuration + try { + await OnboardingConfig.initialize(); + } on Exception catch (e) { + debugPrint('Failed to initialize OnboardingConfig: $e'); + // Non-fatal - app can continue without onboarding UI + } + + runApp( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(sharedPreferences), + ], + child: const FDKApp(), + ), + ); +>>>>>>> 6a559fa (Draft for device onboarding) } class FDKApp extends ConsumerStatefulWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 7032461..4b2525b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,8 +123,21 @@ flutter: # MAC address database for OUI lookup - assets/mac_unified.csv +<<<<<<< HEAD +<<<<<<< HEAD # Configuration files - assets/config/onboarding_messages.json +======= + # Speed test indicator images + - assets/speed_test_indicator_img/ +>>>>>>> 24906fa (Add pms speed test) +======= + # Configuration files + - assets/config/onboarding_messages.json +>>>>>>> 6a559fa (Draft for device onboarding) + + # Speed test indicator images + - assets/speed_test_indicator_img/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images