Phase 3: Overlay Refactor - 2 plan(s) in 2 wave(s) - Sequential execution (Plan 02 depends on Plan 01) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-overlay-refactor | 02 | execute | 2 |
|
|
true |
|
Purpose: This is the integration plan that makes the overlay communication reliable. OverlayProvider delegates to OverlayService (retry + verification), overlay screen detects stale data visually (dim at 3s, dashes at 10s), and SpeedometerScreen shows errors to the user instead of swallowing them silently.
Output: Fully wired overlay refactor satisfying all OVRL-01 through OVRL-06 requirements with user-decided thresholds and behaviors.
<execution_context> @C:\Users\SKSup.claude/get-shit-done/workflows/execute-plan.md @C:\Users\SKSup.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-overlay-refactor/03-CONTEXT.md @.planning/phases/03-overlay-refactor/03-RESEARCH.md @.planning/phases/03-overlay-refactor/03-01-SUMMARY.md @lib/providers/overlay_provider.dart @lib/screens/overlay_screen.dart @lib/screens/speedometer_screen.dart @lib/main.dart @lib/services/overlay_service.dart @lib/models/overlay_message.dart @lib/config/overlay_constants.dart @lib/config/timing_constants.dart Task 1: Refactor OverlayProvider to use OverlayService with error callback and GPS grace period lib/providers/overlay_provider.dart lib/main.dart **OverlayProvider refactor (lib/providers/overlay_provider.dart):**-
Add OverlayService dependency:
- Add
final OverlayService _overlayService;field. - Update constructor:
OverlayProvider(this._overlayService); - Import overlay_service.dart.
- Add
-
Add error callback mechanism:
- Define enum at top of file (or in separate model if preferred):
enum OverlayError { permission, creationFailed, communicationDegraded } - Add field:
void Function(OverlayError)? onError; - This is set by SpeedometerScreen after provider is available (not constructor param — allows late binding since screen needs BuildContext for snackbar).
- Define enum at top of file (or in separate model if preferred):
-
Refactor showOverlay():
- BEFORE calling showOverlay, check permission via
_overlayService.checkPermission(). If denied, callonError?.call(OverlayError.permission)and return early. - Replace direct FlutterOverlayWindow.showOverlay call with
_overlayService.showOverlay(width: ..., height: ...). - Check the return value (bool). If false, call
onError?.call(OverlayError.creationFailed)and return early. - ONLY set
_isOverlayActive = trueAFTER showOverlay returns true (verified by OverlayService.isActive check inside). - Keep the initial data push (shareData via _overlayService.shareData instead of direct FlutterOverlayWindow.shareData).
- Keep _startListeningToOverlayMessages() and _startOverlayStatusCheck() calls.
- BEFORE calling showOverlay, check permission via
-
Refactor closeOverlay():
- Replace direct FlutterOverlayWindow.closeOverlay with
_overlayService.closeOverlay(). - Keep existing _handleOverlayClose() call.
- Replace direct FlutterOverlayWindow.closeOverlay with
-
Refactor _pushDataToOverlay():
- Replace direct FlutterOverlayWindow.shareData with
_overlayService.shareData(message.toMap()). - The OverlayMessage.updateDisplay factory already auto-stamps DateTime.now() (from Plan 01 Task 2).
- Replace direct FlutterOverlayWindow.shareData with
-
Refactor _startListeningToOverlayMessages():
- Replace
FlutterOverlayWindow.overlayListenerwith_overlayService.overlayListener.
- Replace
-
Add GPS grace period to _handleOverlayClose():
- Add
Timer? _gracePeriodTimer;field. - In _handleOverlayClose: After setting _isOverlayActive = false, start a 30-second grace period timer using
TimingConfig.GPS_GRACE_PERIOD. - The grace period timer just logs when it expires:
Logger.info('GPS grace period expired', 'OverlayProvider'). - Cancel grace period in dispose().
- Note: Actual GPS subscription lifecycle management is Phase 4 scope. The timer is created here as infrastructure but does not yet control GPS subscription start/stop. This avoids scope creep.
- Add
-
Cancel grace period when overlay reopens:
- In showOverlay(), cancel
_gracePeriodTimerif running (user reopened overlay within grace period).
- In showOverlay(), cancel
-
Refactor _startOverlayStatusCheck():
- Replace
FlutterOverlayWindow.isActive()with_overlayService.isActive().
- Replace
main.dart update:
- Update MultiProvider in main.dart:
- Add
Provider<OverlayService>(create: (_) => OverlayService()),BEFORE the OverlayProvider. - Update OverlayProvider creation to pass OverlayService:
ChangeNotifierProxyProvider2<GpsDataManager, SettingsProvider, OverlayProvider>( create: (context) => OverlayProvider(context.read<OverlayService>()), update: (_, gpsManager, settings, overlay) => overlay! ..updateDependencies( gpsManager: gpsManager, currentUnit: settings.currentUnit, currentThemeIndex: settings.currentThemeIndex, ), ), - Import overlay_service.dart.
Run
flutter analyzeto confirm no errors. Verify OverlayProvider no longer directly imports or calls FlutterOverlayWindow (all calls go through _overlayService). Verify showOverlay checks permission, verifies creation success, and only sets _isOverlayActive after verification. Verify grace period timer is created and cancelled properly. OverlayProvider delegates all platform calls to OverlayService. Error callback mechanism exists for permission and creation failures. Overlay creation is verified before setting active flag. GPS grace period timer (30s) starts on overlay close. OverlayService is registered in MultiProvider and injected into OverlayProvider. No direct FlutterOverlayWindow usage remains in OverlayProvider.
- Add
-
Add staleness monitoring:
- Import overlay_constants.dart (for STALENESS_DIM_THRESHOLD, STALENESS_DASH_THRESHOLD, STALENESS_DIM_OPACITY, STALENESS_CHECK_INTERVAL).
- Add field:
DateTime _lastUpdateTime = DateTime.now(); - Add field:
Timer? _stalenessCheckTimer;
-
Start staleness check timer in initState():
- After existing _listenToMainAppMessages() call, add _startStalenessMonitoring().
- _startStalenessMonitoring() creates a Timer.periodic with OverlayConfig.STALENESS_CHECK_INTERVAL (1 second).
- Timer callback checks
if (!mounted) return;, then:- Calculates age:
DateTime.now().difference(_lastUpdateTime) - If age > STALENESS_DASH_THRESHOLD (10s) AND _speedText is not already '--':
setState(() => _speedText = '--'); - The dimming (3s threshold) is handled in build() via opacity, no setState needed for it.
- Calculates age:
-
Update _listenToMainAppMessages() to track timestamp:
- When processing 'updateDisplay' messages, extract timestamp from message:
_lastUpdateTime = message.timestamp; - This uses the timestamp from OverlayMessage (added in Plan 01 Task 2).
- On receiving fresh data, _speedText gets the actual speed value — this is the "immediate snap-back" (no animation, just setState with new value).
- When processing 'updateDisplay' messages, extract timestamp from message:
-
Update build() for staleness dimming:
- At the start of build(), calculate:
final age = DateTime.now().difference(_lastUpdateTime); final isStale = age >= OverlayConfig.STALENESS_DIM_THRESHOLD; final staleOpacity = isStale ? OverlayConfig.STALENESS_DIM_OPACITY : 1.0; - Apply staleOpacity to the speed text color: change
color: currentTheme.speedTexttocolor: currentTheme.speedText.withValues(alpha: staleOpacity)for the speed Text widget. - Apply staleOpacity to the unit text color similarly.
- Apply staleOpacity to the heading icon and heading text colors.
- Do NOT use AnimatedOpacity — user decision is "immediate snap-back, no fade-in animation". Direct color alpha is correct.
- At the start of build(), calculate:
-
Cancel staleness timer in dispose():
- Add
_stalenessCheckTimer?.cancel();before super.dispose().
- Add
SpeedometerScreen error surfacing (lib/screens/speedometer_screen.dart):
-
Register error callback on OverlayProvider:
- Import permission_handler (for openAppSettings).
- In initState() addPostFrameCallback, after _initializeGpsManager(), set up the error callback:
context.read<OverlayProvider>().onError = _handleOverlayError;
-
Implement _handleOverlayError(OverlayError error):
- Check
if (!mounted) return; - Determine message and action based on error type:
OverlayError.permission: message = 'Overlay permission required', add SnackBarAction with label 'Settings' that callsopenAppSettings()(from permission_handler).OverlayError.creationFailed: message = 'Failed to create overlay window', no action button.OverlayError.communicationDegraded: message = 'Overlay communication degraded', no action button.
- Show snackbar:
ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), action: action, duration: const Duration(seconds: 4), ), );
- Check
-
Clean up error callback in dispose():
- Before super.dispose(), null out the callback:
context.read<OverlayProvider>().onError = null; - Use a try-catch since the provider might already be disposed.
Run
flutter analyzeto confirm no errors. Verify overlay_screen.dart has staleness timer that dims at 3s and shows dashes at 10s. Verify speedometer_screen.dart registers error callback and shows snackbar with Settings action for permission errors. Verify no AnimatedOpacity is used (immediate snap-back per user decision). Overlay screen has staleness detection: dims all display elements after 3s of no GPS data (0.5 opacity), shows "--" after 10s, snaps back immediately when fresh data arrives. SpeedometerScreen surfaces overlay errors via snackbar with "Settings" action button for permission issues. All staleness thresholds use constants from OverlayConfig. Error callback mechanism connects OverlayProvider (no BuildContext) to SpeedometerScreen (has BuildContext).
- Before super.dispose(), null out the callback:
<success_criteria>
- OVRL-01: Overlay receives continuous debounced data updates via GPS stream forwarding through OverlayService.shareData
- OVRL-02: Overlay errors surfaced to user via snackbar (permission errors include "Settings" action)
- OVRL-03: Overlay creation verified via isActive() before setting isOverlayActive flag
- OVRL-04: Messages include timestamp for staleness detection (millisecondsSinceEpoch)
- OVRL-05: Overlay displays dim at 3s, "--" at 10s when data is stale (user decision: 3s not 2s)
- OVRL-06: OverlayService encapsulates all FlutterOverlayWindow platform calls with retry logic </success_criteria>