# HMG_QLine - Project Context Documentation > **Last Updated:** July 30, 2026 > **Project Type:** Flutter Healthcare Queue Management System > **Target Platform:** Android AC-Powered Kiosk Displays > **Status:** Production (Running 24/7 in HMG Hospitals, Saudi Arabia) --- ## 📋 Table of Contents 1. [Project Overview](#project-overview) 2. [System Architecture](#system-architecture) 3. [Technology Stack](#technology-stack) 4. [Directory Structure](#directory-structure) 5. [Core Features](#core-features) 6. [Data Flow](#data-flow) 7. [Key Components](#key-components) 8. [Android Native Layer](#android-native-layer) 9. [API Integration](#api-integration) 10. [Logging System](#logging-system) 11. [Deployment](#deployment) 12. [Recent Critical Fixes](#recent-critical-fixes) 13. [Configuration System](#configuration-system) 14. [Known Issues & Solutions](#known-issues--solutions) 15. [Testing & Monitoring](#testing--monitoring) 16. [Development Guidelines](#development-guidelines) --- ## 📖 Project Overview ### Purpose HMG_QLine is an enterprise-grade hospital queue management system designed for continuous 24/7 operation on AC-powered Android LED kiosk displays across multiple HMG (Habib Medical Group) hospital branches in Saudi Arabia. ### Key Objectives - **Real-time Patient Queue Management**: Display and manage patient queues across waiting areas - **Multi-modal Patient Calling**: Audio tones + Text-to-Speech voice announcements - **Self-Service Kiosks**: Allow patients to generate tickets via QR scan or ID entry - **24/7 Reliability**: Run continuously without manual intervention - **Multi-Branch Support**: Centralized configuration for multiple hospital locations - **Multi-Language**: Full English/Arabic support with RTL ### Supported Branches | Branch | Project ID | Location | |--------|-----------|----------| | Takhasusi Main | 16 | Riyadh (uses legacy orientation detection) | | Suwaidi | 17 | Riyadh | | Khobar | 60 | Eastern Province | | Takhasusi Women | 140 | Riyadh | --- ## 🏗️ System Architecture ### Architecture Pattern **MVVM (Model-View-ViewModel)** with Provider for state management ``` ┌─────────────────────────────────────────────────────────────┐ │ Views (UI) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Splash │ │ Main Queue │ │ Kiosk │ │ │ │ Screen │ │ Screen │ │ Screen │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ViewModels (State) │ │ ┌────────────────────────┐ ┌────────────────────────┐ │ │ │ ScreenConfigViewModel │ │ QueuingViewModel │ │ │ │ - Configuration │ │ - Queue Management │ │ │ │ - Lifecycle │ │ - SignalR Hub │ │ │ │ - Health Checks │ │ - Audio/TTS │ │ │ └────────────────────────┘ └────────────────────────┘ │ └────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Services (Business Logic) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Logger │ │ Audio │ │ TTS │ │ Network │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Repositories (Data Layer) │ │ ┌────────────────────────┐ ┌────────────────────────┐ │ │ │ SignalR Repository │ │ Screen Details Repo │ │ │ │ - Real-time Hub │ │ - REST API Calls │ │ │ └────────────────────────┘ └────────────────────────┘ │ └────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Data Sources │ │ ┌────────────────────────┐ ┌────────────────────────┐ │ │ │ SignalR Hub (WSS) │ │ REST API (HTTPS) │ │ │ │ - Live updates │ │ - Configuration │ │ │ │ - Patient calls │ │ - Ticket generation │ │ │ └────────────────────────┘ └────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Dependency Injection **GetIt** is used for service location and dependency injection: ```dart // All singletons registered in lib/config/dependency_injection.dart - LoggerService - CrashHandlerService - ApiClient - SignalrRepo - ScreenDetailsRepo - NativeMethodChannelService - ConnectivityService - CacheService - AudioService - TextToSpeechService - ScreenConfigViewModel - QueuingViewModel ``` --- ## 🛠️ Technology Stack ### Flutter/Dart - **Flutter SDK**: >=3.5.4 - **Dart SDK**: ^3.5.4 - **State Management**: Provider 6.1.2 - **DI Container**: GetIt 8.0.2 ### Key Packages | Package | Version | Purpose | |---------|---------|---------| | `signalr_core` | 1.1.1 | Real-time WebSocket communication | | `provider` | 6.1.2 | State management | | `get_it` | 8.0.2 | Dependency injection | | `connectivity_plus` | 6.1.0 | Network status monitoring | | `wakelock_plus` | 1.2.10 | Keep device awake | | `flutter_tts` | 4.2.0 | Text-to-speech announcements | | `just_audio` | 0.9.42 | Audio tone playback | | `shared_preferences` | 2.3.5 | Local key-value storage | | `path_provider` | 2.1.5 | Access to file system | | `qr_code_scanner_plus` | 2.0.10+1 | QR code scanning | | `logger` | 2.4.0 | Logging framework | | `intl` | 0.19.0 | Internationalization | | `flutter_svg` | 2.0.14 | SVG rendering | | `marquee` | 2.2.3 | Scrolling text animation | ### Android Native (Kotlin) - **Kotlin**: JVM Target 1.8 - **Compile SDK**: 35 (Android 15) - **Min SDK**: Flutter default (likely 21+) - **Dependencies**: - `androidx.lifecycle:lifecycle-service:2.8.7` - `androidx.core:core-ktx:1.13.1` --- ## 📁 Directory Structure ``` HMG_QLine/ ├── lib/ │ ├── main.dart # Entry point with crash handling │ ├── api/ │ │ └── api_client.dart # HTTP client with logging │ ├── config/ │ │ ├── dependency_injection.dart # GetIt DI setup │ │ └── routes.dart # App routes configuration │ ├── constants/ │ │ └── app_constants.dart # All constants (API, colors, assets) │ ├── models/ │ │ ├── global_config_model.dart # Main configuration model │ │ ├── ticket_model.dart # Patient ticket data │ │ ├── kiosk_*.dart # Kiosk-specific models │ │ ├── prayers_widget_model.dart # Islamic prayer times │ │ ├── weathers_widget_model.dart # Weather data │ │ └── rss_feed_model.dart # News feed │ ├── repositories/ │ │ ├── signalR_repo.dart # SignalR hub management │ │ └── screen_details_repo.dart # REST API calls │ ├── services/ │ │ ├── logger_service.dart # File logging with rotation │ │ ├── crash_handler_service.dart # Global error handling │ │ ├── connectivity_service.dart # Network monitoring │ │ ├── audio_service.dart # Audio tone playback │ │ ├── text_to_speech_service.dart # TTS announcements │ │ └── cache_service.dart # SharedPreferences wrapper │ ├── utilities/ │ │ ├── enums.dart # All enums │ │ ├── extensions.dart # Dart extensions │ │ ├── native_method_handler.dart # Platform channel │ │ ├── foreground_task_handler.dart # Background service │ │ └── lifecycle_handler.dart # App lifecycle │ ├── view_models/ │ │ ├── screen_config_view_model.dart # Configuration & lifecycle VM │ │ └── queuing_view_model.dart # Queue management VM │ └── views/ │ ├── splash_screen/ # Loading screen │ ├── main_queue_screen/ # Main display (waiting area) │ ├── kiosk_screens/ # Self-service kiosk UI │ ├── common_widgets/ # Reusable components │ └── view_helpers/ # UI utilities ├── android/ │ ├── app/ │ │ ├── build.gradle # Android build config │ │ └── src/main/ │ │ ├── AndroidManifest.xml # Permissions & components │ │ └── kotlin/com/example/hmg_qline/hmg_qline/ │ │ ├── MainActivity.kt # Main activity with kiosk flags │ │ ├── AlarmScheduler.kt # Daily restart scheduler │ │ ├── BootReceiver.kt # Auto-start on boot │ │ ├── RestartAlarmReceiver.kt # Scheduled restart handler │ │ └── BootForegroundService.kt # Boot service │ ├── build.gradle # Project-level build │ └── gradle.properties # Gradle properties ├── assets/ │ ├── fonts/ # Poppins, Cairo, GE_SS_Two │ ├── images/ # Logos, icons (SVG/PNG) │ ├── new_design_icons/ # Updated design assets │ │ └── weather_icons/ # Weather SVG icons │ └── tones/ │ └── call_tone.mp3 # Patient call tone ├── qline_scripts/ # Deployment automation │ ├── deploy_qline.sh # Deploy APK to devices │ ├── restart_qline.sh # Restart app │ ├── extract_logs.sh # Pull logs │ ├── clear_logs.sh # Clear device logs │ ├── connect_devices.sh # Connect via ADB │ ├── kill_qline.sh # Force stop app │ ├── device_ips_*.txt # IP address lists per branch │ └── apks/ # APK storage (gitignored) ├── pubspec.yaml # Flutter dependencies ├── README.md # Basic project info ├── QUICK_SUMMARY.md # Quick reference ├── CHANGES_EXPLANATION.md # Detailed changelog ├── AC_POWERED_KIOSK_SUMMARY.md # Kiosk-specific docs ├── DEPLOYMENT_READY.md # (empty) └── PROJECT_CONTEXT.md # This file ``` --- ## 🎯 Core Features ### 1. Queue Management System #### Patient Call Types (Enums) ```dart enum CallTypeEnum { vitalSign, // 1 - Vital signs measurement room doctor, // 2 - Doctor consultation procedure, // 3 - Medical procedures vaccination, // 4 - Vaccination room nebulization, // 5 - Nebulization treatment none, // 6 - No specific type } ``` #### Queue Types ```dart enum QTypeEnum { appointment, // 1 - Scheduled appointments lab, // 2 - Laboratory services rad, // 3 - Radiology services general, // 4 - General queue } ``` #### Screen Types ```dart enum ScreenTypeEnum { waitingAreaScreen, // 1 - Main waiting area display roomLevelScreen, // 2 - Specific counter/room receptionScreen, // 3 - Reception desk dashboardScreen, // 4 - Admin dashboard kioskScreen, // 5 - Self-service kiosk } ``` ### 2. Real-Time Communication (SignalR) #### Hub Connection Features - **Base URL**: `{baseUrl}/PatientCallingHub?IPAddress={deviceIp}` - **Auto-reconnect**: Custom delays `[0, 2000, 5000, 10000, 30000]` ms - **Keep-alive**: 15-second heartbeat - **Timeout**: 2 minutes - **Manual retry**: 10 attempts with 5-second delay #### Hub Events ```dart // Receive patient call connection.on("SendQLinePatientCall", (message) => { // Adds ticket to queue // Plays tone + TTS announcement // Updates display }); // Receive configuration update connection.on("SendQLineConfig", (message) => { // Updates global configuration // Refreshes widgets (weather, prayer, RSS) }); ``` ### 3. Audio & Voice System #### Audio Service (Just Audio) - Plays call tone: `assets/tones/call_tone.mp3` - Mute support based on configuration - Completion callback triggers TTS #### Text-to-Speech Service - Multi-language: English + Arabic - Voice speed configuration - Dynamic message construction based on call type - Completion callback triggers next ticket in queue #### Call Flow ``` 1. Ticket received via SignalR 2. Add to queue (currentTickets or queueTickets) 3. Play audio tone (if enabled) 4. On tone complete → Speak TTS message (if enabled) 5. On TTS complete → Wait {concurrentCallDelaySec} seconds 6. Process next ticket in queue ``` ### 4. Kiosk Self-Service #### Kiosk States ```dart enum KioskScreenStateEnums { languageState, // Select language queueSelectionState, // Select service queue askPrescriptionState,// Ask for prescription (if applicable) ticketNumState, // Display generated ticket busyState, // Loading } ``` #### Kiosk Input Methods 1. **QR Code Scanner**: Scan patient ID barcode 2. **Manual Entry**: Type patient ID on virtual keyboard #### Ticket Generation Flow ``` 1. Select language (English/Arabic) 2. Select queue (from kioskQueueList) 3. Scan QR / Enter patient ID 4. API call: /api/Gen/GEN_PatientCallNo_Get 5. Display ticket with queue number 6. Auto-return to language selection after timeout ``` ### 5. Information Widgets #### Weather Widget - **API**: `/api/PatientCall/WeatherForecast_GetBy5Days` - **Data**: 5-day forecast with min/max temp - **Icons**: SVG weather icons (sunny, rainy, cloudy, etc.) - **Update**: Every midnight or manual refresh #### Prayer Times Widget - **API**: `/api/PatientCall/PrayerTime_Today` - **Data**: Fajr, Dhuhr, Asr, Maghrib, Isha - **Display**: Next upcoming prayer with countdown - **Source**: Based on project lat/long coordinates #### RSS Feed Widget - **API**: `/api/PatientCall/RssFeed_Get` - **Display**: Marquee scrolling text - **Update**: Every 2 hours - **Fallback**: Dummy news text if API fails ### 6. Health Monitoring System #### Health Check Timer (Every 5 Minutes) ```dart _startHealthCheckTimer() { Timer.periodic(Duration(minutes: 5), (timer) { // 1. Calculate actual uptime final uptime = DateTime.now().difference(appStartTime); // 2. Log health status loggerService.logToFile( "Health check - App uptime: ${uptime.inHours}h ${uptime.inMinutes % 60}m" ); // 3. Check SignalR connection syncHubConnectionState(); // 4. Track consecutive failures if (failure) { healthCheckFailures++; if (healthCheckFailures >= 3) { // Auto-restart SignalR connection } } // 5. Ensure wakelock still enabled WakelockPlus.enable(); // 6. Log connection status logToFile("Hub connected: $isHubConnected, Internet: $isInternetConnected"); // 7. Warn if running >60 hours if (uptime.inHours > 60) { logToFile("WARNING: App running for ${uptime.inHours} hours"); } }); } ``` --- ## 🔄 Data Flow ### Ticket Call Flow ```mermaid graph TD A[Backend sends ticket via SignalR] --> B[onHubTicketCall in QueuingViewModel] B --> C{Is calling in progress?} C -->|No| D[Add to currentTickets, show on screen] C -->|Yes| E[Add to queueTickets for later] D --> F[Acknowledge ticket via API] F --> G[Play audio tone] G --> H[Speak TTS message] H --> I{More tickets in queue?} I -->|Yes| J[Wait concurrentCallDelaySec] J --> K[Move next ticket from queueTickets to currentTickets] K --> F I -->|No| L[Set isCallingInProgress = false] ``` ### Configuration Update Flow ```mermaid graph TD A[App starts] --> B[Get device IP address] B --> C[Call /api/PatientCall/Common_Config_GetByIP] C --> D[Parse GlobalConfigurationsModel] D --> E[Set screen type, queue type, orientation] E --> F{Widgets required?} F -->|Weather| G[Fetch weather data] F -->|Prayers| H[Fetch prayer times] F -->|RSS| I[Fetch RSS feed] G --> J[Start periodic update timer] H --> J I --> J J --> K[Update every 10 minutes / midnight] ``` ### Lifecycle & Recovery Flow ```mermaid graph TD A[App initialized] --> B[Record appStartTime] B --> C[Start health check timer: 5 min] C --> D[Start midnight check timer: 10 min] D --> E[Listen to network changes] E --> F{Network disconnected?} F -->|Yes| G[Set isInternetConnected = false] G --> H[Set isHubConnected = false] F -->|No| I{Network reconnected?} I -->|Yes| J[Set isInternetConnected = true] J --> K[Restart SignalR connection] K --> L[Health check validates connection] L --> M{3 consecutive failures?} M -->|Yes| N[Force restart SignalR] M -->|No| O[Continue monitoring] ``` --- ## 🔑 Key Components ### 1. ScreenConfigViewModel **Responsibilities:** - Device IP detection - Global configuration management - Lifecycle management (onAppPaused, onAppResumed, onAppDetached) - Health monitoring (5-minute timer) - Widget data fetching (weather, prayers, RSS) - Kiosk queue selection - QR scanner management - Ticket generation for kiosk **Key Properties:** ```dart String currentScreenIP // Device IP address GlobalConfigurationsModel globalConfigurationsModel ScreenTypeEnum currentScreenTypeEnum QTypeEnum currentQTypeEnum bool isInternetConnected bool isHubConnected DateTime appStartTime // For uptime tracking int healthCheckFailures // Consecutive failure count ``` **Key Methods:** ```dart initializeScreenConfigVM() // Initialize on app start waitForIPAndInitializeConfigVM() // Wait for IP then init getGlobalConfigurationsByIP() // Fetch config from API listenNetworkConnectivity() // Monitor network changes _startHealthCheckTimer() // Health monitoring every 5 min syncHubConnectionState() // Check SignalR status onAppPaused() // Log when app backgrounded onAppResumed() // Re-enable wakelock onAppDetached() // Log when app killed ``` ### 2. QueuingViewModel **Responsibilities:** - SignalR hub connection management - Patient ticket queue management - Audio tone playback coordination - TTS voice announcement coordination - Ticket acknowledgment to backend **Key Properties:** ```dart List currentTickets // Currently displayed List queueTickets // Waiting queue bool isCallingInProgress // Calling state lock ``` **Key Methods:** ```dart initializeQueueingVM() // Initialize services startHubConnection() // Connect to SignalR hub stopHubConnection() // Disconnect from hub onHubTicketCall() // Handle incoming ticket onHubConfigCall() // Handle config update onHubReconnected() // Handle reconnection onHubDisconnected() // Handle disconnection addNewTicket() // Add ticket to queue callTicketOnScreen() // Display & announce ticket onToneCompleted() // Trigger TTS after tone onVoiceCompleted() // Process next ticket waitAndCallNextTicketIfAvailable() // Queue management ``` ### 3. SignalrRepo **Implementation:** ```dart class SignalrRepoImp implements SignalrRepo { HubConnection? connection; // Check if connected bool get isConnected => connection?.state == HubConnectionState.connected; // Start connection with auto-reconnect startHubConnection({ required String deviceIp, required Function(List?) onHubTicketCall, required Function(dynamic) onHubConfigCall, required Function(dynamic) onHubReconnected, required Function(dynamic) onHubDisconnected, }) { connection = HubConnectionBuilder() .withUrl("$baseUrl?IPAddress=$deviceIp") .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) .build(); connection!.serverTimeoutInMilliseconds = 120000; connection!.keepAliveIntervalInMilliseconds = 15000; // Register event handlers connection!.on("SendQLinePatientCall", onHubTicketCall); connection!.on("SendQLineConfig", onHubConfigCall); // Handle reconnection connection!.onreconnected(onHubReconnected); connection!.onclose((exception) { // Auto-retry up to 10 times }); await connection!.start(); } } ``` ### 4. LoggerService **Features:** - File-based logging to external storage - Three log types: Data, Error, Connectivity - Log rotation based on time (48h) or size (2MB) - IP-based log file naming **Log File Structure:** ``` /storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs/ ├── 12.4.5.1_data_logs.txt ├── 12.4.5.1_error_logs.txt └── 12.4.5.1_connectivity_logs.txt ``` **Log Format:** ``` [2026-07-30 02:30:15 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: Health check performed - App uptime: 14h 23m (started: 2026-07-30 00:07:00) ``` ### 5. CrashHandlerService **Global Error Handling:** ```dart setupGlobalErrorHandlers() { // Flutter framework errors FlutterError.onError = (FlutterErrorDetails details) { handleCrash(error: details.exception, stackTrace: details.stack); }; // Platform dispatcher errors PlatformDispatcher.instance.onError = (error, stack) { handleCrash(error: error, stackTrace: stack); return true; }; // Zone errors handled by runZonedGuarded in main.dart } ``` ### 6. GlobalConfigurationsModel **Key Configuration Fields:** ```dart class GlobalConfigurationsModel { int? id; int? projectID; // Branch identifier ScreenTypeEnum screenTypeEnum; // Screen type QTypeEnum qTypeEnum; // Queue type ScreenOrientationEnum orientationTypeEnum; // Display settings int screenMaxDisplayPatients = 16; int concurrentCallDelaySec = 1; // Audio/Voice bool isToneReq = false; bool isVoiceReq = false; LanguageEnum voiceLanguageEnum; LanguageEnum screenLanguageEnum; // Widget visibility bool isWeatherReq = false; bool isPrayerTimeReq = false; bool isRssFeedReq = false; // Location data double? projectLatitude; double? projectLongitude; int? cityKey; // Clinic prefix validation bool globalClinicPrefixReq = false; bool clinicPrefixReq = true; // Kiosk configuration List? kioskQueueList; List? kioskLanguageConfigList; // Legacy support flag bool isFromTakhasusiMain = false; // projectID == 16 // Clinic prefix validation method bool isClinicPrefixAdded(String ticketNo) { return RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo); // Returns true for: "ABC W-123" format } } ``` --- ## 🤖 Android Native Layer ### MainActivity.kt **Kiosk Display Mode Flags:** ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Keep screen on at all times (AC-powered kiosk) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) // Initialize daily restart alarm at 00:15 initializeRestartAlarm() } ``` **Method Channel Support:** ```kotlin configureFlutterEngine(flutterEngine: FlutterEngine) { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "reopenApp" -> bringAppToForeground() "restartApp" -> restartApplication() "restartDevice" -> restartDevice() // Requires root "clearAudioCache" -> clearAudioResources() "clearAllResources" -> clearAllNativeResources() "scheduleRestartAlarm" -> scheduleRestartAlarm(hour, minute) "cancelRestartAlarm" -> cancelRestartAlarm() "canScheduleExactAlarms" -> checkAlarmPermission() "requestExactAlarmPermission" -> requestAlarmPermission() } } } ``` ### AlarmScheduler.kt **Android Version Compatibility:** ```kotlin fun scheduleRestartAlarm(context: Context, hour: Int = 0, minute: Int = 15) { when { // Android 14+ (API 34+) Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> { if (alarmManager.canScheduleExactAlarms()) { alarmManager.setExactAndAllowWhileIdle(...) } else { alarmManager.setAndAllowWhileIdle(...) // Fallback } } // Android 12-13 (API 31-33) Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { if (alarmManager.canScheduleExactAlarms()) { alarmManager.setExactAndAllowWhileIdle(...) } } // Android 6-11 (API 23-30) Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { alarmManager.setExactAndAllowWhileIdle(...) } // Android < 6 else -> { alarmManager.setExact(...) } } } ``` ### BootBroadcastReceiver.kt **Auto-Start on Device Boot:** ```kotlin override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) { // 1. Schedule daily restart alarm AlarmScheduler.scheduleRestartAlarm(context, 0, 15) // 2. Start foreground service to launch app val serviceIntent = Intent(context, BootForegroundService::class.java) context.startForegroundService(serviceIntent) } } ``` ### AndroidManifest.xml **Critical Permissions:** ```xml ``` **Activity Configuration:** ```xml ``` --- ## 🌐 API Integration ### Base URLs ```dart static String baseUrlLive = 'https://qline.hmg.com'; // LIVE static String baseUrlUat = 'https://ms.hmg.com/nscapi'; // UAT static String baseUrlDev = 'https://ms.hmg.com/nscapi2'; // DEV static String baseUrl = baseUrlUat; // Current environment ``` ### API Endpoints #### Configuration APIs ```dart // Get device configuration by IP GET /api/PatientCall/Common_Config_GetByIP Parameters: { ipAddress: string, apiKey: string } Returns: GlobalConfigurationsModel // Get waiting area screen config GET /api/PatientCall/WaitingAreaScreen_Config_Get Parameters: { ipAddress: string, apiKey: string } Returns: Screen configuration // Get RSS feed GET /api/PatientCall/RssFeed_Get Parameters: { languageId: int } Returns: RssFeedModel // Get weather forecast GET /api/PatientCall/WeatherForecast_GetBy5Days Parameters: { cityKey: string } Returns: WeathersWidgetModel (5-day forecast) // Get prayer times GET /api/PatientCall/PrayerTime_Today Parameters: { latitude: double, longitude: double } Returns: PrayersWidgetModel ``` #### Ticket APIs ```dart // Create ticket for kiosk POST /api/Gen/GEN_PatientCallNo_Get Body: { projectID: int, queueID: int, patientID: int, apiKey: string } Returns: KioskPatientTicket // Acknowledge ticket viewed POST /api/Common/TicketQueueAck_Insert Body: { ticketQueueID: int, ipAddress: string, qType: int, apiKey: string } // Update call request (appointment only) POST /api/PatientCall/CallRequest_QueueUpdate Body: { ticketId: int, ipAddress: string, callType: int, apiKey: string } ``` #### SignalR Hub ```dart // Connect to hub WSS /PatientCallingHub?IPAddress={deviceIp} // Subscribe to events Hub.on("SendQLinePatientCall", callback) // Patient call notification Hub.on("SendQLineConfig", callback) // Configuration update ``` ### API Constants ```dart class ApiConstants { static String apiKey = 'EE17D21C7943485D9780223CCE55DCE5'; // Hub events static String sendQLinePatientCall = "SendQLinePatientCall"; static String sendQLineConfig = "SendQLineConfig"; } ``` --- ## 📝 Logging System ### Log Types ```dart enum LogTypeEnum { data, // Health checks, events, lifecycle error, // Errors and crashes connectivity, // Network status, SignalR events } ``` ### Log File Locations ```bash # External storage path /storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs/ # Files (named by device IP) {IP}_data_logs.txt # General data and health checks {IP}_error_logs.txt # Errors and crashes {IP}_connectivity_logs.txt # Network and SignalR events ``` ### Log Rotation Rules 1. **Time-based**: Clear logs after 48 hours 2. **Size-based**: Clear when file exceeds 2 MB 3. **Timestamp**: Last cleared time stored in SharedPreferences ### Log Format ``` [YYYY-MM-DD HH:MM:SS AM/PM] [SOURCE: method -> file.dart] TYPE: message ``` **Examples:** ``` [2026-07-30 02:30:15 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: Health check performed - App uptime: 14h 23m (started: 2026-07-30 00:07:00) [2026-07-30 02:30:16 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] CONNECTIVITY: Health check - Hub connected: true, Internet: true [2026-07-30 02:35:20 PM] [SOURCE: onHubDisconnected -> queueing_view_model.dart] CONNECTIVITY: Hub disconnected: Connection lost ==================== CRASH ==================== [2026-07-30 02:40:00 PM] CONTEXT: QueuingViewModel.triggerAsyncCrash ERROR: Exception: Test async crash STACKTRACE: #0 QueuingViewModel.triggerAsyncCrash #1 ... ================================================== ``` ### Logging Methods ```dart // Data logging loggerService.logToFile( message: "Health check performed", source: "method -> file.dart", type: LogTypeEnum.data, ); // Error logging loggerService.logError("Error message"); // Crash logging loggerService.logCrash( error: exception, stackTrace: stack, context: "method context", ); ``` --- ## 🚀 Deployment ### Build Process ```bash # Clean build cd /Users/faiz-hashmi/StudioProjects/HMG_QLine flutter clean flutter pub get flutter build apk --release # Output location build/app/outputs/flutter-apk/app-release.apk ``` ### Shell Scripts (qline_scripts/) #### 1. Connect Devices ```bash sh connect_devices.sh -ipfile device_ips_all.txt # Connects to all devices via ADB over TCP/IP # Uses format: adb connect {ip}:5555 ``` #### 2. Deploy APK ```bash sh deploy_qline.sh -ipfile device_ips_all.txt -version 9.4 # Steps: # 1. Connect to all devices # 2. Install APK: adb -s {ip}:5555 install -r apks/app-release-9.4.apk # 3. Launch app: adb -s {ip}:5555 shell am start -n {package}/{activity} # 4. Log installation in qline_installation_records.txt ``` #### 3. Restart App ```bash sh restart_qline.sh -ipfile device_ips_all.txt # Steps: # 1. Force stop: adb shell am force-stop {package} # 2. Launch: adb shell am start -n {package}/{activity} ``` #### 4. Extract Logs ```bash # Single device sh extract_logs.sh -ip 10.70.6.143 # All devices sh extract_all_logs.sh -ipfile device_ips_all.txt # Pulls files from: # /storage/emulated/0/Android/data/{package}/files/logs/ # To: logs/{ip}_{timestamp}/ ``` #### 5. Clear Logs ```bash # Single device sh clear_logs.sh -ip 10.70.6.143 # All devices sh clear_logs_all.sh -ipfile device_ips_all.txt # Removes all log files from device ``` #### 6. Kill App ```bash sh kill_qline.sh -ipfile device_ips_all.txt # Force stop on all devices # adb shell am force-stop {package} ``` ### Device IP Lists ``` device_ips_all.txt # All hospital devices device_ips_tak.txt # Takhasusi Main (projectID: 16) device_ips_tak_women.txt # Takhasusi Women (projectID: 140) device_ips_swd.txt # Suwaidi (projectID: 17) device_ips_khobar.txt # Khobar (projectID: 60) device_ips_cs_test.txt # Cloud Solutions test devices one_device_ip.txt # Single device for testing ``` **Format (one IP per line):** ``` 10.70.6.143 10.70.6.144 10.70.6.145 ``` --- ## 🔥 Recent Critical Fixes (March 2026) ### Problem: App Dying After 14 Hours **Symptoms:** - App ran successfully for ~14 hours - Complete log silence after 8:36 AM - No crash logs, no lifecycle events - Required manual restart by user **Root Causes:** 1. Android OS killed app due to "idle" detection (even on AC power) 2. Incorrect uptime tracking (calculated time since midnight, not app start) 3. No foreground service protection 4. No automatic failure recovery 5. Missing kiosk display mode flags ### Solutions Implemented #### 1. ✅ Fixed Uptime Tracking Bug **Before (WRONG):** ```dart // Calculated time since last midnight check DateTime.now().difference(lastChecked).inHours // At 6:31 AM, showed "6 hours" but app ran since midnight (6.5 hours) ``` **After (CORRECT):** ```dart DateTime appStartTime = DateTime.now(); // Track at initialization // Calculate actual uptime final uptimeHours = DateTime.now().difference(appStartTime).inHours; final uptimeMinutes = DateTime.now().difference(appStartTime).inMinutes % 60; logToFile("Health check - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: $appStartTime)"); ``` #### 2. ✅ Android Kiosk Display Mode **File:** `android/app/src/main/kotlin/.../MainActivity.kt` **Added:** ```kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Prevent Android from treating app as "idle" window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) Log.d(TAG, "MainActivity created - Kiosk display mode active") } ``` **File:** `android/app/src/main/AndroidManifest.xml` **Added:** ```xml ``` #### 3. ✅ Health Check System **File:** `lib/view_models/screen_config_view_model.dart` **Features:** ```dart Timer? _healthCheckTimer; int healthCheckFailures = 0; void _startHealthCheckTimer() { _healthCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) { try { // 1. Calculate actual uptime final uptime = DateTime.now().difference(appStartTime); // 2. Log health check logToFile("Health check - App uptime: ${uptime.inHours}h ${uptime.inMinutes % 60}m"); // 3. Check SignalR connection try { syncHubConnectionState(); healthCheckFailures = 0; // Reset on success } catch (e) { healthCheckFailures++; // Auto-recover after 3 failures if (healthCheckFailures >= 3) { logToFile("CRITICAL: 3 consecutive failures - attempting recovery"); await queuingViewModel.stopHubConnection(); await Future.delayed(Duration(seconds: 2)); await queuingViewModel.startHubConnection(); } } // 4. Ensure wakelock enabled await WakelockPlus.enable(); // 5. Log connection status logToFile("Hub connected: $isHubConnected, Internet: $isInternetConnected"); // 6. Warn if running >60 hours if (uptime.inHours > 60) { logToFile("WARNING: App running for ${uptime.inHours} hours"); } // 7. Keep UI thread active notifyListeners(); } catch (e) { healthCheckFailures++; logError("Health check failed: $e"); } }); } ``` #### 4. ✅ Enhanced Lifecycle Tracking **File:** `lib/view_models/screen_config_view_model.dart` **Added:** ```dart Future onAppPaused() async { final uptimeHours = DateTime.now().difference(appStartTime).inHours; logToFile("[onAppPaused] - App uptime: ${uptimeHours}h - WARNING: App going to background!"); } Future onAppResumed() async { final uptimeHours = DateTime.now().difference(appStartTime).inHours; logToFile("[onAppResumed] - App uptime: ${uptimeHours}h"); // Re-enable wakelock await WakelockPlus.enable(); // Verify connections syncHubConnectionState(); } Future onAppDetached() async { final uptimeHours = DateTime.now().difference(appStartTime).inHours; logToFile("[onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!"); } ``` **Impact:** - Now see EXACTLY when app goes to background or gets killed - If app stays alive correctly, should NEVER see these logs #### 5. ✅ SignalR Connection Improvements **File:** `lib/repositories/signalR_repo.dart` **Enhanced:** ```dart connection = HubConnectionBuilder() .withUrl("$hubBaseURL?IPAddress=$deviceIp") .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Custom retry delays .build(); connection!.serverTimeoutInMilliseconds = 120000; // 2 minutes connection!.keepAliveIntervalInMilliseconds = 15000; // 15 seconds // Manual reconnection on close connection!.onclose((exception) async { reconnectAttempts++; if (reconnectAttempts < maxReconnectAttempts) { logToFile("SignalR reconnect attempt #$reconnectAttempts"); await Future.delayed(Duration(seconds: 5)); try { await connection!.start(); logToFile("SignalR reconnected after disconnect"); reconnectAttempts = 0; } catch (e) { logError("Reconnect failed: $e"); } } }); ``` #### 6. ✅ Clinic Prefix Feature **File:** `lib/models/global_config_model.dart` **Added:** ```dart bool globalClinicPrefixReq = false; bool clinicPrefixReq = true; bool isClinicPrefixAdded(String ticketNo) { // Check for format: "XXX W-XX" (3 letters + space + W-) final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo); return hasClinicPrefix; } // Returns true for: "ABC W-123", "FMC W-T-4" // Returns false for: "W-123", "A W-123", "ABCD W-123" ``` ### Files Modified 1. `lib/view_models/screen_config_view_model.dart` - Uptime, health checks, lifecycle 2. `lib/models/global_config_model.dart` - Clinic prefix feature 3. `lib/repositories/signalR_repo.dart` - Connection improvements 4. `android/app/src/main/kotlin/.../MainActivity.kt` - Kiosk display mode 5. `android/app/src/main/AndroidManifest.xml` - Activity flags --- ## ⚙️ Configuration System ### IP-Based Configuration Each device is identified by its IP address and receives specific configuration: ```dart // Device IP detection (automatic or test) if (useTestIP) { currentScreenIP = AppConstants.testIP; // "12.4.5.1" } else { currentScreenIP = await connectivityService.getCurrentScreenIP(); } // Fetch configuration GlobalConfigurationsModel config = await screenDetailsRepo.getGlobalScreenConfigurations( ipAddress: currentScreenIP ); ``` ### Project ID Detection ```dart // Takhasusi Main uses legacy orientation detection value.isFromTakhasusiMain = value.projectID == AppConstants.projectIdTakhasusiMain; // 16 if (value.isFromTakhasusiMain) { log("✅ This device will use getTurnsByOrientationForOlderVersions()"); } else { log("✅ This device will use getTurnsByOrientationForNewVersions()"); } ``` ### Configuration Hierarchy ``` 1. Device IP → API call → GlobalConfigurationsModel 2. Screen Type (waiting/room/kiosk/dashboard) 3. Queue Type (appointment/lab/rad/general) 4. Orientation (portrait/landscape) 5. Display Settings (max patients, delay) 6. Audio/Voice Settings (tone, voice, language) 7. Widget Visibility (weather, prayers, RSS) 8. Kiosk Queues & Languages ``` ### Dynamic Configuration Updates Configuration can be updated via SignalR: ```dart // Backend sends new config Hub → SendQLineConfig → onHubConfigCall() // Updates applied globalConfigurationsModel = GlobalConfigurationsModel.fromJson(data); updateGlobalConfigurationsModel(value: globalConfigurationsModel, needNotify: true); getInfoWidgetsDetailsFromServer(); // Refresh widgets ``` --- ## ⚠️ Known Issues & Solutions ### Issue 1: App Dies After 14 Hours **Status:** ✅ FIXED (March 2026) **Solution:** - Added kiosk display mode flags - Implemented health check system - Fixed uptime tracking - Added lifecycle logging ### Issue 2: Android Doze Mode **Status:** ⚠️ REQUIRES DEVICE CONFIGURATION **Solution:** Some Android devices (Xiaomi, Oppo, Vivo) have aggressive task killers: **Manual Steps:** 1. Settings > Apps > QLine 2. Enable "Auto-start" 3. Set Battery to "No restrictions" 4. Disable "Battery optimization" **ADB Command:** ```bash adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline ``` ### Issue 3: Memory Leaks (48+ Hour Uptime) **Status:** ✅ MITIGATED **Solution:** - Daily restart scheduled at 00:15 AM - Resource cleanup on lifecycle events - System.gc() calls at strategic points ### Issue 4: TTS Stopping on Some Devices **Status:** ⚠️ DEVICE-SPECIFIC **Solution:** - Clear audio cache via `clearAudioCache()` method channel - Force stop TTS service: `am force-stop com.google.android.tts` - Restart app if TTS completely fails ### Issue 5: SignalR Connection Drops **Status:** ✅ MITIGATED **Solution:** - Auto-reconnect with exponential backoff - Health check monitors connection every 5 min - Auto-restart after 3 consecutive failures ### Issue 6: Exact Alarm Permission (Android 14+) **Status:** ⚠️ REQUIRES USER PERMISSION **Solution:** ```kotlin // Check permission if (!alarmManager.canScheduleExactAlarms()) { // Open settings Intent intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM) startActivity(intent) } ``` --- ## 📊 Testing & Monitoring ### Testing Checklist #### Deployment Testing - [ ] Build APK successfully - [ ] Deploy to test device via ADB - [ ] App starts automatically - [ ] Check log: "MainActivity created - Kiosk display mode active" - [ ] Verify health checks appear every 5 minutes #### Short-Term Testing (1 Hour) - [ ] Uptime increments correctly (0h 5m → 0h 10m → 0h 15m → 1h 0m) - [ ] SignalR connection established - [ ] Hub connected: true in logs - [ ] Internet connected: true in logs - [ ] No `[onAppPaused]` or `[onAppDetached]` logs #### Medium-Term Testing (24 Hours) - [ ] Health check logs continuous (no gaps) - [ ] App uptime reaches 24h - [ ] Hub connection stable - [ ] No lifecycle events (paused/detached) - [ ] Midnight refresh occurs (if widgets enabled) #### Long-Term Testing (48+ Hours) - [ ] App runs past 14-hour checkpoint (critical) - [ ] App reaches 48h uptime - [ ] Check for >60h warning log - [ ] Scheduled restart at 00:15 works correctly - [ ] Auto-start on boot works (if device rebooted) #### Network Resilience Testing - [ ] Disconnect WiFi for 10 minutes - [ ] Check log: "Hub disconnected" - [ ] Reconnect WiFi - [ ] Check log: "SignalR reconnect attempt #X" - [ ] Check log: "SignalR reconnected after disconnect" - [ ] Verify auto-recovery after 3 failures #### Audio/Voice Testing - [ ] Audio tone plays when ticket called - [ ] TTS voice announcement speaks (if enabled) - [ ] Language switching works (English/Arabic) - [ ] Mute settings respected - [ ] Sequential calls work with delay #### Kiosk Testing - [ ] Language selection screen appears - [ ] Queue selection shows all options - [ ] QR scanner works - [ ] Manual ID entry works - [ ] Ticket generation successful - [ ] Thank you screen displays - [ ] Auto-return to language selection after timeout ### Monitoring Commands #### Check if App Running ```bash adb shell ps | grep hmg_qline ``` #### View Logcat (MainActivity) ```bash adb logcat | grep MainActivity ``` #### View Logcat (SignalR) ```bash adb logcat | grep SignalR ``` #### Check Device Idle State ```bash adb shell dumpsys deviceidle ``` #### Check Battery Optimization Status ```bash adb shell dumpsys deviceidle whitelist | grep hmg_qline ``` #### View Last 100 App Logs ```bash adb logcat -t 100 | grep hmg_qline ``` #### Pull All Logs from Device ```bash cd qline_scripts sh extract_logs.sh -ip 10.70.6.143 ``` --- ## 🔧 Development Guidelines ### Code Style #### Naming Conventions - **Classes:** PascalCase (`ScreenConfigViewModel`) - **Methods:** camelCase (`startHubConnection()`) - **Variables:** camelCase (`currentScreenIP`) - **Constants:** camelCase (`static String baseUrl`) - **Enums:** PascalCase (`ScreenTypeEnum`) - **Files:** snake_case (`screen_config_view_model.dart`) #### File Organization ```dart // 1. Imports import 'dart:async'; import 'package:flutter/material.dart'; // 2. Class declaration class MyClass extends ChangeNotifier { // 3. Private constants static const String _CONSTANT = "value"; // 4. Dependencies final MyService myService; // 5. Constructor MyClass({required this.myService}); // 6. Public properties String publicProperty = ""; // 7. Private properties String _privateProperty = ""; // 8. Public methods void publicMethod() {} // 9. Private methods void _privateMethod() {} // 10. Overrides @override void dispose() { super.dispose(); } } ``` ### Logging Best Practices ```dart // Always include source file in logs loggerService.logToFile( message: "Meaningful message with data: $value", source: "methodName -> file_name.dart", type: LogTypeEnum.data, ); // Use appropriate log types LogTypeEnum.data // Health checks, state changes, events LogTypeEnum.error // Errors, exceptions LogTypeEnum.connectivity // Network, SignalR events // Include context in error logs try { // code } catch (e, stackTrace) { loggerService.logCrash( error: e, stackTrace: stackTrace, context: "methodName -> file_name.dart", ); } ``` ### Error Handling ```dart // Always use try-catch for async operations Future myAsyncMethod() async { try { final result = await somethingRisky(); // handle success } catch (e, stackTrace) { loggerService.logCrash( error: e, stackTrace: stackTrace, context: "myAsyncMethod -> my_file.dart", ); // handle error gracefully } } // Provide fallbacks final value = config.someValue ?? defaultValue; // Validate before use if (data != null && data.isNotEmpty) { processData(data); } ``` ### State Management ```dart // Always notifyListeners after state change void updateValue(String newValue) { myValue = newValue; notifyListeners(); // Trigger UI rebuild } // Use getIt for singleton access final viewModel = getIt.get(); // Consumer for reactive UI Consumer( builder: (context, viewModel, child) { return Text(viewModel.currentScreenIP); }, ) ``` ### API Calls ```dart // Always check response for null final response = await screenDetailsRepo.getConfig(ip: currentScreenIP); if (response == null) { loggerService.logError("Config API returned null"); updateViewState(ViewState.error); return; } // Use response updateConfig(response); ``` ### Testing Additions When adding new features: 1. **Add health check logs** for monitoring 2. **Add error handling** with proper logging 3. **Add null checks** for all external data 4. **Test with network disconnection** scenarios 5. **Test with long uptime** (24+ hours) 6. **Update this documentation** with new features --- ## 📚 Additional Documentation Files - **README.md** - Basic project information - **QUICK_SUMMARY.md** - Quick reference for March 2026 changes - **CHANGES_EXPLANATION.md** - Detailed explanation of fixes - **AC_POWERED_KIOSK_SUMMARY.md** - Kiosk-specific documentation - **PROJECT_CONTEXT.md** - This file (comprehensive context) --- ## 🔄 Version History | Version | Date | Changes | |---------|------|---------| | 9.4 | July 2026 | Current production version | | 9.2 | March 2026 | Critical fixes: uptime tracking, health checks, kiosk flags | | 9.0 | February 2026 | SignalR improvements, lifecycle tracking | | 8.x | 2025 | Previous stable versions | --- ## 📞 Emergency Contacts & Procedures ### If App Dies Again **Collect Information:** 1. Last 200 lines of logs before app stopped 2. Android version and device model 3. Screenshot of Apps > QLine > Battery settings 4. Output of: `adb shell dumpsys deviceidle whitelist` **Immediate Actions:** 1. Check device battery optimization settings 2. Verify app is whitelisted from doze mode 3. Check if exact alarm permission granted (Android 12+) 4. Review logs for `[onAppPaused]` or `[onAppDetached]` 5. Check for lifecycle events before silence **Advanced Troubleshooting:** ```bash # Force whitelist from doze adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline # Check app status adb shell ps | grep hmg_qline # Check if app is optimized adb shell dumpsys battery | grep hmg_qline # Force prevent background kill adb shell cmd appops set com.example.hmg_qline.hmg_qline RUN_IN_BACKGROUND allow ``` --- ## 🎯 Future Improvements ### Planned Enhancements 1. **Remote Monitoring**: Send health status to backend server 2. **Watchdog Service**: External process that restarts app if killed 3. **Scheduled Restart**: Auto-restart at 3 AM daily (already implemented at 00:15) 4. **Memory Profiling**: Track and log memory usage trends 5. **Firebase Crashlytics**: Cloud-based crash analytics 6. **Network Quality Metrics**: Log ping times and bandwidth ### Technical Debt 1. Clean up commented code in MainActivity.kt 2. Add unit tests for ViewModels 3. Add integration tests for SignalR connection 4. Implement proper i18n instead of hardcoded strings 5. Refactor large ViewModels into smaller components --- ## 📖 Glossary - **AC-Powered Kiosk**: Android device permanently plugged into power, not battery-powered - **App Standby Buckets**: Android 9+ feature that restricts background apps - **Doze Mode**: Android 6+ battery optimization that restricts background processing - **Health Check**: Periodic verification that app is functioning correctly - **Hub**: SignalR WebSocket server for real-time communication - **Kiosk Mode**: Display mode where app runs continuously without user intervention - **QType**: Queue type (appointment, lab, rad, general) - **SignalR**: Real-time communication library using WebSockets - **TTS**: Text-to-Speech for voice announcements - **Uptime**: Duration the app has been running since last start - **Wakelock**: Android API to prevent device from sleeping --- **END OF CONTEXT DOCUMENTATION** > This document should be updated whenever significant changes are made to the project. > Last updated by: AI Assistant > Last update date: July 30, 2026