Skip to content

Commit fa17d59

Browse files
committed
New Features
Regional admins can now enforce hybrid mode per-zone via the API; when enabled, hybrid mode is automatically activated and the toggle is locked on the Settings screen with a "Forced Enabled by Regional Admin" label Regional admins can set a minimum auto-ping interval per-zone; interval options below the minimum are greyed out in Settings with a "Disabled by Regional Admin" label If a user's saved interval falls below the admin-configured minimum, it is automatically bumped up on connect
1 parent 9fe6acf commit fa17d59

File tree

3 files changed

+84
-12
lines changed

3 files changed

+84
-12
lines changed

lib/providers/app_state_provider.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
390390
bool get rxAllowed => _apiService.rxAllowed;
391391
bool get hasApiSession => _apiService.hasSession;
392392
bool get isApiRxOnlyMode => hasApiSession && !txAllowed && rxAllowed;
393+
bool get enforceHybrid => _apiService.enforceHybrid;
394+
int get minModeInterval => _apiService.minModeInterval;
393395

394396
// Offline mode
395397
bool get offlineMode => _preferences.offlineMode;
@@ -1099,6 +1101,18 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
10991101
debugLog('[CONN] No regional scope — using unscoped flood');
11001102
}
11011103

1104+
// Enforce hybrid mode if required by regional admin
1105+
if (_apiService.enforceHybrid && !_preferences.hybridModeEnabled) {
1106+
_preferences = _preferences.copyWith(hybridModeEnabled: true);
1107+
debugLog('[CONN] Hybrid mode force-enabled by regional admin');
1108+
}
1109+
1110+
// Enforce minimum auto-ping interval if required by regional admin
1111+
if (_preferences.autoPingInterval < _apiService.minModeInterval) {
1112+
_preferences = _preferences.copyWith(autoPingInterval: _apiService.minModeInterval);
1113+
debugLog('[CONN] Auto-ping interval bumped to ${_apiService.minModeInterval}s by regional admin');
1114+
}
1115+
11021116
// Create ping service with wakelock (create new instance per connection)
11031117
_pingService = PingService(
11041118
gpsService: _gpsService,

lib/screens/settings_screen.dart

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
242242
),
243243
],
244244
),
245-
subtitle: const Text('Combines Active and Passive modes'),
246-
value: prefs.hybridModeEnabled,
247-
onChanged: isAutoMode ? null : (value) {
245+
subtitle: appState.enforceHybrid
246+
? const Text(
247+
'Forced Enabled by Regional Admin',
248+
style: TextStyle(color: Colors.amber),
249+
)
250+
: const Text('Combines Active and Passive modes'),
251+
value: appState.enforceHybrid ? true : prefs.hybridModeEnabled,
252+
onChanged: (isAutoMode || appState.enforceHybrid) ? null : (value) {
248253
appState.updatePreferences(prefs.copyWith(hybridModeEnabled: value));
249254
},
250255
),
@@ -1083,7 +1088,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
10831088
}
10841089

10851090
void _showIntervalSelector(BuildContext context, AppStateProvider appState) {
1086-
final currentInterval = appState.preferences.autoPingInterval;
1091+
final minInterval = appState.minModeInterval;
1092+
var currentInterval = appState.preferences.autoPingInterval;
1093+
1094+
// Auto-bump if current interval is below the admin minimum
1095+
if (currentInterval < minInterval) {
1096+
currentInterval = minInterval;
1097+
appState.updatePreferences(
1098+
appState.preferences.copyWith(autoPingInterval: minInterval),
1099+
);
1100+
}
10871101

10881102
showDialog(
10891103
context: context,
@@ -1102,18 +1116,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
11021116
child: Column(
11031117
mainAxisSize: MainAxisSize.min,
11041118
children: AutoPingInterval.values.map((interval) {
1105-
final isSelected = interval == currentInterval;
1119+
final isDisabled = interval < minInterval;
1120+
1121+
String description;
1122+
if (interval == 15) {
1123+
description = 'Fast (More coverage, causes more mesh load)';
1124+
} else if (interval == 30) {
1125+
description = 'Normal (Balanced coverage and mesh load)';
1126+
} else {
1127+
description = 'Slow (Less coverage, little mesh load)';
1128+
}
11061129

1107-
return RadioListTile<int>(
1130+
final tile = RadioListTile<int>(
11081131
title: Text('$interval seconds'),
1109-
subtitle: Text(interval == 15
1110-
? 'Fast (More coverage, causes more mesh load)'
1111-
: interval == 30
1112-
? 'Normal (Balanced coverage and mesh load)'
1113-
: 'Slow (Less coverage, little mesh load)'),
1132+
subtitle: isDisabled
1133+
? const Text(
1134+
'Disabled by Regional Admin',
1135+
style: TextStyle(color: Colors.amber),
1136+
)
1137+
: Text(description),
11141138
value: interval,
1115-
selected: isSelected,
11161139
);
1140+
1141+
if (isDisabled) {
1142+
return IgnorePointer(
1143+
child: Opacity(
1144+
opacity: 0.5,
1145+
child: tile,
1146+
),
1147+
);
1148+
}
1149+
return tile;
11171150
}).toList(),
11181151
),
11191152
),

lib/services/api_service.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class ApiService {
4141
Function? _onSessionExpiring;
4242
List<String> _channels = [];
4343
List<String> _scopes = [];
44+
bool _enforceHybrid = false;
45+
int _minModeInterval = 15;
4446

4547
/// Callback to get current GPS coordinates for heartbeat
4648
/// Returns (lat, lon) or null if GPS is not available
@@ -52,6 +54,12 @@ class ApiService {
5254
/// Regional scopes from auth response (e.g., ['ottawa'])
5355
List<String> get scopes => List.unmodifiable(_scopes);
5456

57+
/// Whether hybrid mode is enforced by regional admin
58+
bool get enforceHybrid => _enforceHybrid;
59+
60+
/// Minimum auto-ping interval enforced by regional admin (seconds)
61+
int get minModeInterval => _minModeInterval;
62+
5563
ApiService({http.Client? client}) : _client = client ?? http.Client();
5664

5765
/// Sanitize payload by removing sensitive fields for logging
@@ -315,6 +323,21 @@ class ApiService {
315323
_scopes = [];
316324
}
317325

326+
// Parse enforce_hybrid flag from auth response
327+
_enforceHybrid = data['enforce_hybrid'] == true;
328+
if (_enforceHybrid) {
329+
debugLog('[API] Regional admin enforces hybrid mode');
330+
}
331+
332+
// Parse min_mode_interval from auth response
333+
final minInterval = data['min_mode_interval'];
334+
if (minInterval is int && minInterval > 0) {
335+
_minModeInterval = minInterval;
336+
debugLog('[API] Regional admin min interval: ${_minModeInterval}s');
337+
} else {
338+
_minModeInterval = 15;
339+
}
340+
318341
// Note: Heartbeat is enabled by AppStateProvider when auto mode starts
319342
// (not on initial auth, since heartbeat is only for auto mode)
320343
} else if (reason == 'disconnect') {
@@ -614,6 +637,8 @@ class ApiService {
614637
_sessionExpiresAt = null;
615638
_channels = [];
616639
_scopes = [];
640+
_enforceHybrid = false;
641+
_minModeInterval = 15;
617642
_heartbeatTimer?.cancel();
618643
_heartbeatTimer = null;
619644
debugLog('[API] Session cleared');

0 commit comments

Comments
 (0)