diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 29fabbf..afb7468 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1094,6 +1094,68 @@ Files backup/restore - mirror mode or zip --> ## Priority 10: Adaptive Tablet / Desktop Navigation - [x] Implement adaptive Navigation Rail for tablet viewports and maintain persistent sidebar on desktop. --> + +# Settings screen +- academic & graduation section: save cutoff date only show when the date of cutoff is changed, in edit schedule dates start and end date no x button +- remove github repository button +- for report an issue make it "emailto:[EMAIL_ADDRESS]" use my email lead developer, add modal to report issues with title, description, category (bug, feature, question), urgency (high, medium, low), screenshots or attachments, and send button, then opens email client ready to send +- it has bug on system theme if I toggle system theme I cannot click light or dark mode +- security screen if more than 3-5 devices add load more button at the bottom to load more devices and load less at the bottom of all listed devices, make the only show the devices that have enabled "remember me" on that device, so if the device is not enabled "remember me" it will not show on the list + +# DASHBOARD +- after using admin account and login as teacher, it occurs that dashboard of admin shows on milli seconds inside of teacher account fix it +- remove space at top of the dashboard "Dashboard Overview" in windows app it has space remove it +- remove the top students by documents and needs attention at the bottom +- fix the "4 missing" even i click no students listed +- dynamic missing document requirement chips (Approach A): dynamically count and display all missing enabled mandatory requirements (SF9, SF10, PSA, etc.) sorted by highest missing count instead of hardcoding SF9/SF10 only + +# STUDENT SCREEN +- enrolled tab: + - when no students it has clear filter button, remove it, then in the student list table, add add button at the bottom to add student and nagivation for all students tab to enroll a students] + - when I update the student status to inactive, dropped, transferred, graduated stays on this tab, add in filter for inactive, dropped, transferred, graducation + - rename the tab as the active academic year +- student profile: + - prevent or remove the buttons for not enrolled students upload docs + - history add load more and load less, it only show last 10 entries, and resets to default when not active tab + - remove the verified badge for files on records + - add button to navigate to document screen inside of their folder +- pagination on windows app + - use white font for dark theme and black for white theme for previous, next, and pagination numbers and add background like the numbers + +# ARCHIVE SCREEN +- remove the recycle bin and delete, restore, upload new version buttons + + +# IMPLEMENTATION ROADMAP (EXECUTION ORDER) + +## Phase 1: Critical Bugs & Session/Data State (Implement First) +- [ ] Fix admin dashboard flashing on teacher login (invalidate & reset role cache on auth change) +- [ ] Fix "4 missing" count showing non-enrolled students (scope requirements query strictly to active academic year) +- [ ] Fix Enrolled tab keeping inactive, dropped, transferred, or graduated students after status update + +## Phase 2: Student Screen & Profile Core Workflows +- [ ] Enrolled tab: Rename tab to active academic year (e.g. S.Y. 2024–2025) +- [ ] Enrolled tab: Remove clear filter button when empty; add bottom "Add Student" button and navigation CTA to All Students tab +- [ ] Enrolled tab: Add status filters for inactive, dropped, transferred, and graduated students +- [ ] Student profile: Prevent/remove upload docs buttons for non-enrolled students +- [ ] Student profile: Activity history display last 10 entries with "Load More" / "Load Less" (resets when tab is not active) +- [ ] Student profile: Remove verified badge for files on records +- [ ] Student profile: Add button to navigate directly to document screen inside student folder + +## Phase 3: Settings Screen Fixes & Issue Reporting +- [ ] Fix system theme toggle bug (ensure manual light/dark mode selection remains clickable) +- [ ] Academic & graduation: Save cutoff date button only shows when cutoff date is changed; remove 'x' buttons from schedule start/end date pickers +- [ ] Security screen: Filter device list to only show devices with "Remember Me" enabled; add "Load More" / "Load Less" if > 3–5 devices +- [ ] Report an issue modal: Add modal with Title, Description, Category, Urgency, Attachments, and Send button that opens email client (`mailto:`) to lead developer + +## Phase 4: UI Polish, Styling & Cleanups (Implement Last) +- [ ] Dashboard: Remove top whitespace above "Dashboard Overview" on Windows desktop app +- [ ] Dashboard: Remove top students by documents and bottom needs attention (or implement dynamic Approach A missing chips) +- [ ] Settings: Remove GitHub repository button +- [ ] Windows app pagination: Apply white text for dark theme, black text for light theme, with button background styling +- [ ] Archive screen: Remove recycle bin, delete, restore, and upload new version buttons + + --- diff --git a/frontend/lib/core/network/server_discovery.dart b/frontend/lib/core/network/server_discovery.dart index 3473fe7..6d8e9ae 100644 --- a/frontend/lib/core/network/server_discovery.dart +++ b/frontend/lib/core/network/server_discovery.dart @@ -1,18 +1,23 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'api_constants.dart'; /// Discovers the TIS RMS server on the local network (LAN) and manages seamless fallback to Cloud Tunnel. -/// 1. Verifies physical network interfaces, filtering out virtual/bridge/loopback adapters. -/// 2. Probes local subnet hosts with bounded concurrency (pools of 25) using lightweight raw TCP checks first. -/// 3. Confirms identity via HTTP X-TIS-RMS response header. -/// 4. Prioritizes LAN for speed and offline resilience, seamlessly falling back to Cloud Tunnel (https://tis-rms.cc.cd/api). +/// 1. Instant UDP discovery beacon (<50ms) to detect local server on Wi-Fi/LAN automatically. +/// 2. Verifies physical network interfaces, filtering out virtual/bridge/loopback adapters. +/// 3. Probes local subnet hosts with bounded concurrency (pools of 25) using lightweight raw TCP checks first. +/// 4. Confirms identity via HTTP X-TIS-RMS response header. +/// 5. Prioritizes LAN for speed and offline resilience, seamlessly falling back to Cloud Tunnel (https://tis-rms.cc.cd/api). class ServerDiscoveryService { static const int _port = 18484; + static const int _discoveryPort = 18485; static const String _prefsKey = 'server_url'; static const Duration _socketTimeout = Duration(milliseconds: 300); static const Duration _pingTimeout = Duration(seconds: 2); + static const Duration _udpTimeout = Duration(milliseconds: 350); // ─── Saved URL ──────────────────────────────────────────────────────────── @@ -204,18 +209,142 @@ class ServerDiscoveryService { return null; } + // ─── UDP Discovery Beacon (Instant <50ms auto-detection) ──────────────────── + + /// Broadcasts a lightweight UDP probe ('TIS_RMS_DISCOVER') on port 18485. + /// If the TIS RMS backend / Server Manager is running on the LAN, it replies in <50ms. + /// Returns the discovered server URL (e.g. "http://192.168.1.50:18484") or null. + static Future quickUdpBeacon({ + Duration timeout = _udpTimeout, + }) async { + RawDatagramSocket? socket; + Timer? timer; + final completer = Completer(); + + try { + socket = await RawDatagramSocket.bind( + InternetAddress.anyIPv4, + 0, + reuseAddress: true, + ); + socket.broadcastEnabled = true; + + final message = utf8.encode('TIS_RMS_DISCOVER'); + + // 1. Broadcast to standard global local subnet broadcast address + try { + socket.send(message, InternetAddress('255.255.255.255'), _discoveryPort); + } catch (_) {} + + // 2. Also broadcast explicitly to all active physical subnet broadcast IPs (e.g. 192.168.1.255) + try { + final prefixes = await getSubnetPrefixes(); + for (final prefix in prefixes) { + final broadcastIp = '${prefix}255'; + try { + socket.send(message, InternetAddress(broadcastIp), _discoveryPort); + } catch (_) {} + } + } catch (_) {} + + // Listen for UDP response from server + socket.listen((RawSocketEvent event) { + if (event == RawSocketEvent.read) { + final datagram = socket?.receive(); + if (datagram != null && !completer.isCompleted) { + try { + final text = utf8.decode(datagram.data).trim(); + if (text.contains('TIS_RMS_SERVER')) { + int port = _port; + try { + final Map data = jsonDecode(text); + if (data['port'] != null) { + port = int.tryParse(data['port'].toString()) ?? _port; + } + } catch (_) {} + + final senderIp = datagram.address.address; + final candidateUrl = 'http://$senderIp:$port'; + + timer?.cancel(); + completer.complete(candidateUrl); + } + } catch (_) {} + } + } + }); + + timer = Timer(timeout, () { + if (!completer.isCompleted) { + completer.complete(null); + } + }); + + final result = await completer.future; + if (result != null) { + // Fast HTTP verify check (500ms) to ensure API server is fully ready + final verified = await ping(result, timeout: const Duration(milliseconds: 500)); + if (verified) { + return result; + } + } + return null; + } catch (_) { + return null; + } finally { + timer?.cancel(); + socket?.close(); + } + } + + // ─── Manual LAN-Only Scanner (Never falls back to Cloud Tunnel) ─────────── + + /// Scans specifically for local network endpoints. Bypasses SharedPreferences + /// and NEVER returns Cloud Tunnel. Used when user explicitly clicks "Scan Local Network". + static Future scanLanOnly({ + void Function(String message)? onProgress, + }) async { + // 1. Fast UDP Discovery Beacon (<400ms) + onProgress?.call('Broadcasting LAN discovery beacon…'); + final udpFound = await quickUdpBeacon(timeout: const Duration(milliseconds: 400)); + if (udpFound != null) { + await save(udpFound); + ApiConstants.setBaseUrl(udpFound); + return udpFound; + } + + // 2. Physical Subnet Probing (Fallback if Wi-Fi router blocks UDP broadcast) + onProgress?.call('Scanning local network IP range…'); + final prefixes = await getSubnetPrefixes(); + if (prefixes.isNotEmpty) { + final subnetFound = await discover( + onProgress: (subnet, scanned, total) { + onProgress?.call('Scanning LAN ${subnet}x … ($scanned/$total)'); + }, + ); + if (subnetFound != null) { + await save(subnetFound); + ApiConstants.setBaseUrl(subnetFound); + return subnetFound; + } + } + + return null; + } + // ─── Master Resolution: LAN First, Then Seamless Tunnel Fallback ────────── /// Resolves the optimal server endpoint: /// 1. Fast Localhost check (for single-machine setups). - /// 2. Fast check of previously working saved LAN URL. - /// 3. Fast physical LAN discovery (bounded ~2.5s scan). - /// 4. Previously saved Tunnel URL or Cloud Tunnel fallback (https://tis-rms.cc.cd/api). - /// 5. VPS & Localhost fallbacks. + /// 2. Instant UDP LAN discovery beacon (instant <50ms auto-connect on Wi-Fi). + /// 3. Fast check of previously working saved LAN server. + /// 4. Subnet port scan fallback. + /// 5. Cloud Tunnel fallback (https://tis-rms.cc.cd/api). + /// 6. VPS fallback. static Future resolveServerWithFallback({ void Function(String message)? onProgress, }) async { - // 1. Fast Localhost Check (< 400ms) + // 1. Fast Localhost Check (< 350ms) onProgress?.call('Checking local host (127.0.0.1)…'); final localhostAlive = await ping( ApiConstants.localhostUrl, @@ -227,19 +356,29 @@ class ServerDiscoveryService { return ApiConstants.localhostUrl; } - // 2. Fast check of previously working saved server (instant reconnect) + // 2. Instant UDP LAN Discovery Beacon (< 350ms) + // Instantly finds the local Server Manager backend on school Wi-Fi without manual scanning! + onProgress?.call('Probing local network (LAN)…'); + final udpFound = await quickUdpBeacon(timeout: const Duration(milliseconds: 350)); + if (udpFound != null) { + await save(udpFound); + ApiConstants.setBaseUrl(udpFound); + return udpFound; + } + + // 3. Fast check of previously working saved server (if it's a LAN address) final saved = await getSaved(); - if (saved != null && saved.isNotEmpty) { - onProgress?.call('Checking saved server connection…'); - final alive = await ping(saved, timeout: const Duration(seconds: 1)); + if (saved != null && saved.isNotEmpty && _isLanUrl(saved)) { + onProgress?.call('Checking saved LAN server…'); + final alive = await ping(saved, timeout: const Duration(milliseconds: 700)); if (alive) { ApiConstants.setBaseUrl(saved); return saved; } } - // 3. Scan physical Local Network (LAN) - onProgress?.call('Scanning local network (LAN)…'); + // 4. Physical Subnet Scan (if UDP was blocked by router) + onProgress?.call('Scanning local network subnets…'); final prefixes = await getSubnetPrefixes(); if (prefixes.isNotEmpty) { final found = await discover( @@ -254,7 +393,7 @@ class ServerDiscoveryService { } } - // 4. Cloudflare Tunnel Domain Fallback (https://tis-rms.cc.cd/api) + // 5. Cloudflare Tunnel Domain Fallback (https://tis-rms.cc.cd/api) onProgress?.call('Connecting to Cloud Tunnel…'); final tunnelAlive = await ping( ApiConstants.tunnelUrl, diff --git a/frontend/lib/ui/screens/login/login_screen.dart b/frontend/lib/ui/screens/login/login_screen.dart index ea71159..daf5b2e 100644 --- a/frontend/lib/ui/screens/login/login_screen.dart +++ b/frontend/lib/ui/screens/login/login_screen.dart @@ -1137,7 +1137,7 @@ class _NetworkScanDialogState extends State<_NetworkScanDialog> { } Future _scan() async { - final found = await ServerDiscoveryService.resolveServerWithFallback( + final found = await ServerDiscoveryService.scanLanOnly( onProgress: (msg) { if (mounted) setState(() => _status = msg); }, @@ -1145,21 +1145,19 @@ class _NetworkScanDialogState extends State<_NetworkScanDialog> { if (!mounted) return; Navigator.pop(context); if (found != null) { - final isLan = found.contains('192.168.') || - found.contains('10.') || - found.contains('172.'); showSuccessDialog( context, - title: isLan ? 'LAN Server Connected' : 'Tunnel Connected', - message: isLan - ? 'Connected to local server: $found' - : 'Local server not found. Connected to Cloud Tunnel: $found', + title: 'LAN Server Connected', + message: 'Successfully discovered and connected to local server: $found', ); } else { showErrorDialog( context, - 'Server Not Found', - 'No TIS RMS server found on your Local Network (LAN) and the Cloud Tunnel domain (${ApiConstants.tunnelUrl}) is currently unreachable.', + 'LAN Server Not Found', + 'No TIS RMS server was found on your local network (LAN).\n\n' + '• Ensure the TIS RMS Server Manager is running on the host computer.\n' + '• Ensure this device is connected to the same Wi-Fi / network.\n\n' + 'Your connection remains active on Cloud Tunnel (${ApiConstants.tunnelUrl}).', ); } } diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 7e0f0b7..a477c5a 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.1+1 +version: 1.2.51+51 environment: sdk: ^3.11.5