Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
af11159
feat(remote-access): add RemoteAccessState with serialization
PeterJhongLinksys Jan 21, 2026
d93d26c
feat(remote-access): add RemoteAccessProvider
PeterJhongLinksys Jan 21, 2026
e27c83b
feat(remote-access): add RemoteReadOnlyBanner widget
PeterJhongLinksys Jan 21, 2026
a118b72
feat(remote-access): integrate banner into RootContainer
PeterJhongLinksys Jan 21, 2026
2f42de7
feat(remote-access): add defensive check in RouterRepository
PeterJhongLinksys Jan 21, 2026
e5c03c1
fix(remote-access): prevent bypass of read-only mode with allowlist a…
PeterJhongLinksys Jan 21, 2026
e822f2b
docs(remote-access): add usage guide for remote read-only mode
PeterJhongLinksys Jan 21, 2026
c8dbca6
fix(remote-access): prevent banner from overlaying UI content
PeterJhongLinksys Jan 22, 2026
71f5623
style(remote-access): use theme colors instead of hardcoded orange
PeterJhongLinksys Jan 22, 2026
bf2714c
docs(remote-access): add UI controls protection design
PeterJhongLinksys Jan 26, 2026
2f76409
docs(remote-access): add detailed implementation plan for UI controls
PeterJhongLinksys Jan 26, 2026
0c0afa2
refactor(geolocation): use isMaster property instead of nodeType check
PeterJhongLinksys Jan 26, 2026
906def5
feat(remote-access): add RemoteAwareSwitch component with local mode …
PeterJhongLinksys Jan 26, 2026
2e6d99e
test(remote-access): add RemoteAwareSwitch disabled state test
PeterJhongLinksys Jan 26, 2026
2b96359
test(remote-access): verify RemoteAwareSwitch preserves value when di…
PeterJhongLinksys Jan 26, 2026
4a6c271
test(remote-access): verify RemoteAwareSwitch reacts to loginType cha…
PeterJhongLinksys Jan 26, 2026
cf27880
feat(remote-access): add checkRemoteReadOnly param to UiKitBottomBarC…
PeterJhongLinksys Jan 26, 2026
f9cf94a
feat(remote-access): auto-disable Save button in remote mode
PeterJhongLinksys Jan 26, 2026
ec766da
docs(remote-access): catalog all AppSwitch usages for replacement
PeterJhongLinksys Jan 26, 2026
ec81747
feat(remote-access): replace AppSwitch with RemoteAwareSwitch (batch 1)
PeterJhongLinksys Jan 26, 2026
bb16d1d
feat(remote-access): replace AppSwitch with RemoteAwareSwitch (batch 2)
PeterJhongLinksys Jan 26, 2026
0495911
docs(remote-access): update usage guide with RemoteAwareSwitch and Ui…
PeterJhongLinksys Jan 26, 2026
b8ec51c
docs(remote-access): add implementation verification report
PeterJhongLinksys Jan 26, 2026
cd60b3b
feat(remote-access): enhance remote read-only mode with diagnostic to…
PeterJhongLinksys Jan 28, 2026
63a1f6b
Merge remote-tracking branch 'origin/dev-2.0.0' into peter/remote_ass…
PeterJhongLinksys Jan 28, 2026
26b0364
fix(demo): fix theme studio export dialog overflow
PeterJhongLinksys Jan 28, 2026
80b4d74
fix(ui): update remote read-only banner icon
PeterJhongLinksys Jan 28, 2026
b133403
Revert "Merge remote-tracking branch 'origin/dev-2.0.0' into peter/re…
PeterJhongLinksys Jan 28, 2026
cc214b3
fix(test): update remote read-only banner test
PeterJhongLinksys Jan 28, 2026
aecaaef
Re-apply merge of dev-2.0.0
PeterJhongLinksys Jan 29, 2026
5f39d11
fix(session): remove fetchRemote param from getDeviceInfo call
PeterJhongLinksys Jan 29, 2026
f68e1f6
Update build.sh to enable the theme json config
PeterJhongLinksys Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build_web.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ function buildWebApp() {


if [ "$cloud" == "qa" ]; then
flutter build web --target=lib/main.dart --base-href="/${href}" --build-number="${buildNumber}" --dart-define=force="${force}" --dart-define=cloud_env="${cloud}" --dart-define=enable_env_picker="${picker}" --dart-define=ca="${ca}" $enableHTMLRenderer
flutter build web --target=lib/main.dart --base-href="/${href}" --build-number="${buildNumber}" --dart-define=force="${force}" --dart-define=cloud_env="${cloud}" --dart-define=enable_env_picker="${picker}" --dart-define=ca="${ca}" --dart-define=THEME_JSON="${theme}" $enableHTMLRenderer
else
flutter build web --target=lib/main.dart --base-href="/${href}" --build-number="${buildNumber}" --dart-define=force="${force}" --dart-define=cloud_env="${cloud}" --dart-define=enable_env_picker="${picker}" --dart-define=ca="${ca}" $enableHTMLRenderer
flutter build web --target=lib/main.dart --base-href="/${href}" --build-number="${buildNumber}" --dart-define=force="${force}" --dart-define=cloud_env="${cloud}" --dart-define=enable_env_picker="${picker}" --dart-define=ca="${ca}" --dart-define=THEME_JSON="${theme}" $enableHTMLRenderer
fi
# rm -rf ./build/web/canvasKit
}
Expand All @@ -17,6 +17,7 @@ href=$3
cloud=$4
picker=$5
ca=$6
theme=$7

enableHTMLRenderer=""
if [ "$FlutterVersion" == "3.27.1" ]; then
Expand Down
303 changes: 303 additions & 0 deletions docs/plans/2026-01-20-remote-read-only-mode-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# Remote Read-Only Mode - Usage Guide

**Date:** 2026-01-21
**Status:** Implementation Complete

## Overview

When users connect remotely via Linksys Cloud, all router configuration changes (JNAP SET commands) are automatically disabled for security. This guide shows how to integrate remote read-only checks into your UI components.

## Quick Start

### 1. Using RemoteAwareSwitch (Recommended for Immediate Actions)

For switches that directly trigger JNAP operations (immediate effect):

```dart
import 'package:privacy_gui/page/components/views/remote_aware_switch.dart';

RemoteAwareSwitch(
value: isFeatureEnabled,
onChanged: (value) {
ref.read(myProvider.notifier).toggleFeature(value);
},
)
```

**When to use RemoteAwareSwitch:**
- ✅ Switch onChanged directly calls a provider method
- ✅ That provider method triggers JNAP SET operations
- ✅ Changes apply immediately (no Save button)

**Do NOT use RemoteAwareSwitch when:**
- ❌ Page has a Save button (use regular AppSwitch - auto-protected)
- ❌ Switch only modifies local UI state
- ❌ Switch controls pure frontend behavior

### 2. Form Pages with Save Buttons (Automatic Protection)

No changes needed! All pages using `UiKitBottomBarConfig` automatically have their Save buttons disabled in remote mode.

```dart
// This is automatically protected - no changes needed
bottomBar: UiKitBottomBarConfig(
isPositiveEnabled: state.isDirty, // Auto-disabled in remote mode
onPositiveTap: _saveSettings,
),
```

**Opt-out (rare cases):**

```dart
bottomBar: UiKitBottomBarConfig(
checkRemoteReadOnly: false, // Opt out of auto-disable
isPositiveEnabled: state.isDirty,
onPositiveTap: _saveUIPreferences, // Non-JNAP operation
),
```

### 3. Manual Remote Read-Only Check (Advanced)

For custom controls that don't fit the above patterns:

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/providers/remote_access/remote_access_provider.dart';

class MySettingsWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the remote read-only status
final isReadOnly = ref.watch(
remoteAccessProvider.select((state) => state.isRemoteReadOnly),
);

// Use it to disable controls
return ElevatedButton(
onPressed: isReadOnly ? null : _onSave,
child: const Text('Save Settings'),
);
}
}
```

### 4. Disable Different Control Types

#### Buttons
```dart
ElevatedButton(
onPressed: isReadOnly ? null : _onSave,
child: const Text('Save'),
)
```

#### Switches
```dart
Switch(
value: state.isEnabled,
onChanged: isReadOnly ? null : (value) => _updateSetting(value),
)
```

#### Text Fields
```dart
TextField(
enabled: !isReadOnly,
controller: _controller,
)
```

#### IconButtons
```dart
IconButton(
icon: const Icon(Icons.edit),
onPressed: isReadOnly ? null : _onEdit,
)
```

## Architecture

### Component Hierarchy

```
remoteAccessProvider (Provider<RemoteAccessState>)
↓ watches
authProvider (AsyncNotifierProvider<AuthState>)
↓ provides
LoginType (local / remote / none)
```

### State Flow

```
User logs in remotely
authProvider updates with LoginType.remote
remoteAccessProvider recomputes (isRemoteReadOnly = true)
UI widgets rebuild with disabled controls
Banner appears at top of screen
```

## Banner

A global banner automatically appears at the top of the screen when in remote mode. No action needed - already integrated into [lib/page/components/layouts/root_container.dart](../../lib/page/components/layouts/root_container.dart).

**Banner message**: "Remote View Mode - Setting changes are disabled"

## Defensive Layer

Even if UI controls are enabled by mistake, RouterRepository will **block all write operations** at the service layer:

```dart
// In RouterRepository.send()
if (!_isReadOnlyOperation(action) && _isRemoteReadOnly()) {
throw const UnexpectedError(
message: 'Write operations are not allowed in remote read-only mode',
);
}
```

**Allowed operations** (allowlist):
- `get*` - Get operations (getDeviceInfo, getWANSettings, etc.)
- `is*` - Status checks (isAdminPasswordDefault, etc.)
- `check*` - Validation operations (checkAdminPassword, etc.)

**Blocked operations**: Everything else, including:
- `set*` - All SET operations
- `reboot` - Device reboot
- `factoryReset` - Factory reset
- `deleteDevice` - Device deletion
- etc.

## Best Practices

### DO ✅

```dart
// Use select() to optimize rebuilds
final isReadOnly = ref.watch(
remoteAccessProvider.select((state) => state.isRemoteReadOnly),
);

// Disable controls that trigger SET operations
ElevatedButton(
onPressed: isReadOnly ? null : _onSave,
child: const Text('Save'),
)
```

### DON'T ❌

```dart
// Don't watch entire auth state if you only need remote status
final auth = ref.watch(authProvider); // Rebuilds on every auth change
final isReadOnly = auth.loginType == LoginType.remote; // ❌

// Don't hide controls completely - disable them instead
if (!isReadOnly) { // ❌
return ElevatedButton(
onPressed: _onSave,
child: const Text('Save'),
);
}
// User won't see the button, won't understand why
```

### When to Use

Apply remote read-only checks to:
- **Any button that saves settings** (Save, Apply, Update, etc.)
- **Any switch/checkbox that changes router state** (Enable WiFi, etc.)
- **Any input field for configuration** (SSID, Password, IP address, etc.)
- **Any action that modifies data** (Add, Delete, Edit, etc.)

Do NOT apply to:
- **View/Read operations** (Refresh, View Details, etc.)
- **Navigation** (Back, Cancel, etc.)
- **Local UI state** (Expand/Collapse, Filter, Sort, etc.)

## Testing

### Unit Tests

See [test/providers/remote_access/](../../test/providers/remote_access/) for examples:

```dart
test('UI is disabled in remote mode', () {
final container = ProviderContainer(
overrides: [
authProvider.overrideWith(() => TestAuthNotifier(
const AsyncValue.data(AuthState(loginType: LoginType.remote)),
)),
],
);

final isReadOnly = container.read(remoteAccessProvider).isRemoteReadOnly;
expect(isReadOnly, true);
});
```

### Widget Tests

```dart
testWidgets('Save button is disabled in remote mode', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
authProvider.overrideWith(() => TestAuthNotifier(
const AsyncValue.data(AuthState(loginType: LoginType.remote)),
)),
],
child: MySettingsWidget(),
),
);

final saveButton = find.text('Save');
expect(tester.widget<ElevatedButton>(saveButton).onPressed, isNull);
});
```

## Compile-Time Testing

For testing remote mode behavior without actual remote login:

```dart
// In BuildConfig
BuildConfig.forceCommandType = ForceCommand.remote;
```

This will force the app into remote read-only mode regardless of actual `loginType`.

## Related Files

### Core Implementation
- [lib/providers/remote_access/remote_access_provider.dart](../../lib/providers/remote_access/remote_access_provider.dart) - Provider logic
- [lib/providers/remote_access/remote_access_state.dart](../../lib/providers/remote_access/remote_access_state.dart) - State class
- [lib/page/components/views/remote_read_only_banner.dart](../../lib/page/components/views/remote_read_only_banner.dart) - Banner widget
- [lib/page/components/views/remote_aware_switch.dart](../../lib/page/components/views/remote_aware_switch.dart) - **NEW: RemoteAwareSwitch component**
- [lib/page/components/ui_kit_page_view.dart](../../lib/page/components/ui_kit_page_view.dart) - **UPDATED: UiKitBottomBarConfig auto-protection**
- [lib/core/jnap/router_repository.dart](../../lib/core/jnap/router_repository.dart) - Defensive checks

### Tests
- [test/providers/remote_access/remote_access_provider_test.dart](../../test/providers/remote_access/remote_access_provider_test.dart)
- [test/providers/remote_access/remote_access_state_test.dart](../../test/providers/remote_access/remote_access_state_test.dart)
- [test/page/components/views/remote_read_only_banner_test.dart](../../test/page/components/views/remote_read_only_banner_test.dart)
- [test/page/components/views/remote_aware_switch_test.dart](../../test/page/components/views/remote_aware_switch_test.dart) - **NEW: RemoteAwareSwitch tests**
- [test/core/jnap/router_repository_test.dart](../../test/core/jnap/router_repository_test.dart) - Defensive checks tests

### Documentation
- [docs/plans/2026-01-26-switch-replacement-catalog.md](./2026-01-26-switch-replacement-catalog.md) - **NEW: AppSwitch usage catalog and replacement guide**

## Future Enhancements

1. **Exception List**: Allowlist specific SET operations in remote mode
2. **Granular Permissions**: Different permission levels (read-only, limited-write, full-access)
3. **Custom Messages**: Per-feature explanations of why controls are disabled
4. **Temporary Override**: Allow privileged operations with additional confirmation

## Questions?

See the design document: [docs/plans/2026-01-20-remote-read-only-mode-design.md](./2026-01-20-remote-read-only-mode-design.md)
Loading