Files
gps-speedo/.planning/phases/01-foundation/01-01-PLAN.md
T
Mikkeli Matlock 7f8075b76d docs(01): create phase plan
Phase 01: Foundation
- 2 plan(s) in 2 wave(s)
- 2 autonomous, 0 checkpoint-only
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:08:34 +09:00

23 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-foundation 01 execute 1
lib/services/logger.dart
lib/config/gps_constants.dart
lib/config/timing_constants.dart
lib/config/overlay_constants.dart
lib/config/speed_unit.dart
lib/config/color_themes.dart
lib/models/processed_gps_data.dart
lib/models/overlay_message.dart
lib/models/theme_model.dart
lib/services/gps_data_manager.dart
lib/main.dart
lib/gps_service.dart
true
truths artifacts key_links
App compiles with flutter analyze showing zero errors
All existing functionality preserved — speed display, heading, themes, units, overlay all work identically
Logger utility exists with 4 verbosity levels (error, warn, info, debug) guarded by kDebugMode
Behavioral constants centralized in config/ with SCREAMING_SNAKE naming in abstract static classes
ProcessedGpsData model lives in models/ as separate file with immutable fields and copyWith()
OverlayMessage typed class exists with toMap()/fromMap() for overlay IPC serialization
ThemeModel exists with internal palette storage, current palette exposure, and rotate() method
SpeedUnit enum lives in config/speed_unit.dart
ColorTheme/ColorThemes classes live in config/color_themes.dart
path provides contains
lib/services/logger.dart Logger utility with error/warn/info/debug levels class Logger
path provides contains
lib/config/gps_constants.dart GPS behavioral constants abstract class GpsConfig
path provides contains
lib/config/timing_constants.dart Timer and interval constants abstract class TimingConfig
path provides contains
lib/config/overlay_constants.dart Overlay sizing and display constants abstract class OverlayConfig
path provides contains
lib/config/speed_unit.dart Speed unit enum with conversion enum SpeedUnit
path provides contains
lib/config/color_themes.dart Color theme definitions class ColorTheme
path provides contains
lib/models/processed_gps_data.dart Immutable GPS data model class ProcessedGpsData
path provides contains
lib/models/overlay_message.dart Typed overlay IPC message class OverlayMessage
path provides contains
lib/models/theme_model.dart Theme model with palette rotation class ThemeModel
from to via pattern
lib/services/gps_data_manager.dart lib/models/processed_gps_data.dart import and usage of ProcessedGpsData import.*models/processed_gps_data
from to via pattern
lib/services/gps_data_manager.dart lib/config/gps_constants.dart import and usage of GpsConfig constants import.*config/gps_constants
from to via pattern
lib/main.dart lib/config/speed_unit.dart import SpeedUnit from new location import.*config/speed_unit
from to via pattern
lib/main.dart lib/config/color_themes.dart import ColorThemes from new location import.*config/color_themes
Create foundational infrastructure (directories, logger, constants) and extract all data models and config files to their proper locations in the new modular structure.

Purpose: Establishes the architectural skeleton that all subsequent Phase 1 and Phase 2 work depends on. Models, constants, and logger are the lowest-dependency pieces that everything else imports from.

Output: Logger utility, 3 constants files, 3 model files, 2 moved config files — all with updated imports across the codebase. App compiles and runs identically.

<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/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md

Source files to read before modifying: @lib/main.dart @lib/gps_service.dart @lib/speed_units.dart @lib/color_themes.dart @lib/services/gps_data_manager.dart

Task 1: Create directory structure, Logger utility, and constants files lib/screens/ (directory) lib/widgets/ (directory) lib/providers/ (directory) lib/models/ (directory) lib/config/ (directory) lib/services/logger.dart lib/config/gps_constants.dart lib/config/timing_constants.dart lib/config/overlay_constants.dart 1. Create all six directories under lib/: screens/, widgets/, providers/, models/, config/. The services/ directory already exists.
2. Create `lib/services/logger.dart` — a Logger utility class with 4 static methods:
   - `Logger.error(String message, [String? caller])`
   - `Logger.warn(String message, [String? caller])`
   - `Logger.info(String message, [String? caller])`
   - `Logger.debug(String message, [String? caller])`

   Implementation requirements:
   - All methods guarded by `if (kDebugMode)` from `package:flutter/foundation.dart`
   - Use `debugPrint()` (not `print()`) to avoid log truncation
   - Include ANSI color codes: red for error, yellow for warn, blue for info, gray for debug
   - Include ISO 8601 timestamp in output
   - Include caller tag in brackets when provided: `[ERROR] 2026-02-09T12:00:00 [GpsDataManager] message`
   - Reset ANSI color after each line with `\x1B[0m`

   This replaces the existing `customDebugPrint()` function in main.dart.

3. Create `lib/config/gps_constants.dart` — extract GPS behavioral constants from gps_data_manager.dart:
   ```dart
   abstract class GpsConfig {
     static const Duration STALE_DATA_THRESHOLD = Duration(seconds: 4);
     static const Duration INITIAL_FIX_TIMEOUT = Duration(seconds: 20);
     static const Duration UPDATE_TIMEOUT = Duration(seconds: 2);
     static const double VALID_HEADING_MIN = 0.0;
     static const double VALID_HEADING_MAX = 360.0;
     static const double LOW_SPEED_THRESHOLD = 1.0;
   }
   ```
   Use SCREAMING_SNAKE naming per CONTEXT.md decisions. Include a comment at the top: `// GPS behavioral constants — SCREAMING_SNAKE per project convention`

4. Create `lib/config/timing_constants.dart` — extract timer intervals from main.dart:
   ```dart
   abstract class TimingConfig {
     static const Duration HEARTBEAT_INTERVAL = Duration(seconds: 5);
     static const Duration OVERLAY_STATUS_CHECK_INTERVAL = Duration(milliseconds: 1000);
     static const Duration TAP_CLOSE_DELAY = Duration(milliseconds: 500);
     static const Duration LONG_PRESS_SIGNAL_DELAY = Duration(milliseconds: 50);
   }
   ```

5. Create `lib/config/overlay_constants.dart` — extract overlay sizing constants from main.dart:
   ```dart
   abstract class OverlayConfig {
     static const double WIDTH_PERCENTAGE = 0.45;
     static const double ASPECT_RATIO = 0.6;
     static const double BACKGROUND_OPACITY = 0.85;
     static const double BORDER_OPACITY = 0.3;
     static const double FONT_SIZE_RATIO = 0.2;
     static const double UNIT_FONT_RATIO = 0.6;
     static const double ICON_SIZE_RATIO = 0.72;
     static const double HEADING_FONT_RATIO = 0.4;
     static const double DEFAULT_SCREEN_WIDTH = 400.0;
     static const double DEFAULT_OVERLAY_WIDTH = 280.0;
     static const double DEFAULT_OVERLAY_HEIGHT = 140.0;
   }
   ```

Do NOT update any imports in main.dart or gps_data_manager.dart yet — that happens in Task 2 and Task 3.
- Run `flutter analyze` in the project root — should show zero new errors (new files are standalone, not imported yet) - Verify all 6 directories exist under lib/ - Verify logger.dart, gps_constants.dart, timing_constants.dart, overlay_constants.dart all exist Directory structure created. Logger utility exists with 4 levels guarded by kDebugMode. Three constants files exist with all behavioral magic numbers extracted using SCREAMING_SNAKE in abstract static classes. Task 2: Move config files and extract data models lib/config/speed_unit.dart lib/config/color_themes.dart lib/models/processed_gps_data.dart lib/models/overlay_message.dart lib/models/theme_model.dart 1. Create `lib/config/speed_unit.dart` — copy the SpeedUnit enum from `lib/speed_units.dart` exactly as-is. Same content, new location. Do NOT delete the old file yet.
2. Create `lib/config/color_themes.dart` — copy both ColorTheme and ColorThemes classes from `lib/color_themes.dart` exactly as-is. Same content, new location. Do NOT delete the old file yet.

3. Create `lib/models/processed_gps_data.dart` — extract the ProcessedGpsData class from `lib/services/gps_data_manager.dart` (lines 6-40) into its own file. The class is already immutable with copyWith(). No changes to the class itself, just move it to its own file. Add the appropriate import for `package:flutter/foundation.dart` if needed (it isn't — this is a pure Dart class, no Flutter imports needed).

4. Create `lib/models/overlay_message.dart` — a NEW typed class replacing the unvalidated Map data currently used for overlay IPC. Analyze the current overlay message maps in main.dart to determine fields:

   From `_pushDataToOverlay()` (line 403-411): action, speedText, unitText, headingText, heading, unitIndex, themeIndex
   From `_showFloatingWindow()` (line 441-450): adds overlayWidth, overlayHeight
   From overlay long press (line 1063-1064): action 'longPressClose'

   Create the class:
   ```dart
   import 'dart:convert';

   class OverlayMessage {
     final String action;
     final String? speedText;
     final String? unitText;
     final String? headingText;
     final double? heading;
     final int? unitIndex;
     final int? themeIndex;
     final double? overlayWidth;
     final double? overlayHeight;

     const OverlayMessage({
       required this.action,
       this.speedText,
       this.unitText,
       this.headingText,
       this.heading,
       this.unitIndex,
       this.themeIndex,
       this.overlayWidth,
       this.overlayHeight,
     });

     // Factory constructors for common message types
     factory OverlayMessage.updateDisplay({
       required String speedText,
       required String unitText,
       required String headingText,
       required double heading,
       required int unitIndex,
       required int themeIndex,
       double? overlayWidth,
       double? overlayHeight,
     }) {
       return OverlayMessage(
         action: 'updateDisplay',
         speedText: speedText,
         unitText: unitText,
         headingText: headingText,
         heading: heading,
         unitIndex: unitIndex,
         themeIndex: themeIndex,
         overlayWidth: overlayWidth,
         overlayHeight: overlayHeight,
       );
     }

     factory OverlayMessage.longPressClose() {
       return const OverlayMessage(action: 'longPressClose');
     }

     factory OverlayMessage.overlayClosed() {
       return const OverlayMessage(action: 'overlayClosed');
     }

     Map<String, dynamic> toMap() {
       final map = <String, dynamic>{'action': action};
       if (speedText != null) map['speedText'] = speedText;
       if (unitText != null) map['unitText'] = unitText;
       if (headingText != null) map['headingText'] = headingText;
       if (heading != null) map['heading'] = heading;
       if (unitIndex != null) map['unitIndex'] = unitIndex;
       if (themeIndex != null) map['themeIndex'] = themeIndex;
       if (overlayWidth != null) map['overlayWidth'] = overlayWidth;
       if (overlayHeight != null) map['overlayHeight'] = overlayHeight;
       return map;
     }

     factory OverlayMessage.fromMap(Map<dynamic, dynamic> map) {
       return OverlayMessage(
         action: map['action'] as String? ?? 'unknown',
         speedText: map['speedText'] as String?,
         unitText: map['unitText'] as String?,
         headingText: map['headingText'] as String?,
         heading: (map['heading'] as num?)?.toDouble(),
         unitIndex: map['unitIndex'] as int?,
         themeIndex: map['themeIndex'] as int?,
         overlayWidth: (map['overlayWidth'] as num?)?.toDouble(),
         overlayHeight: (map['overlayHeight'] as num?)?.toDouble(),
       );
     }

     OverlayMessage copyWith({...all fields...}) { ... }
   }
   ```

   Include `toMap()` and `fromMap()` because this model crosses the overlay IPC boundary via `FlutterOverlayWindow.shareData()` which requires Map data.

5. Create `lib/models/theme_model.dart` — a new ThemeModel that wraps the existing ColorThemes:
   ```dart
   import '../config/color_themes.dart';

   class ThemeModel {
     final int _currentIndex;

     const ThemeModel({int currentIndex = 0}) : _currentIndex = currentIndex;

     // Current palette access
     ColorTheme get current => ColorThemes.getTheme(_currentIndex);
     String get currentSetName => current.name;
     int get currentIndex => _currentIndex;

     // Rotate to next theme (circular)
     ThemeModel rotate() {
       return ThemeModel(
         currentIndex: ColorThemes.getNextThemeIndex(_currentIndex),
       );
     }

     ThemeModel copyWith({int? currentIndex}) {
       return ThemeModel(
         currentIndex: currentIndex ?? _currentIndex,
       );
     }
   }
   ```

   ThemeModel delegates palette storage to ColorThemes (which holds the static list). ThemeModel only tracks current selection and provides rotation. This matches the CONTEXT.md mental model: "holds all possible colour palettes, exposing only the current set of colours (with their nametags), informative 'current set name', and a method for rotating themes." The palettes are held via delegation to ColorThemes, not duplicated.

Do NOT update imports in main.dart or gps_data_manager.dart yet — that is Task 3.
- Run `flutter analyze` — zero new errors (new files not imported yet by existing code) - Verify all 5 model/config files exist in their new locations - Verify ProcessedGpsData in models/ matches the original in gps_data_manager.dart SpeedUnit and ColorThemes copied to config/. ProcessedGpsData extracted to models/. OverlayMessage typed class created with toMap()/fromMap(). ThemeModel created with palette rotation. Task 3: Rewire all imports, replace print() with Logger, delete old files lib/services/gps_data_manager.dart lib/gps_service.dart lib/main.dart lib/speed_units.dart (delete) lib/color_themes.dart (delete) This is the critical wiring task. Every import must be updated in a single pass. The app must compile after this task completes.
1. **Update `lib/services/gps_data_manager.dart`:**
   - Remove the ProcessedGpsData class definition (lines 6-40) — it now lives in models/
   - Add imports:
     ```dart
     import '../models/processed_gps_data.dart';
     import '../config/speed_unit.dart';
     import '../config/gps_constants.dart';
     import '../services/logger.dart';
     ```
   - Change import `'../speed_units.dart'` to `'../config/speed_unit.dart'`
   - Replace `_staleDataThreshold` constant with `GpsConfig.STALE_DATA_THRESHOLD`
   - Replace the Duration(seconds: 20) timeout with `GpsConfig.INITIAL_FIX_TIMEOUT`
   - Replace heading validation magic numbers (0, 360) with `GpsConfig.VALID_HEADING_MIN`, `GpsConfig.VALID_HEADING_MAX`
   - Replace ALL `print()` calls (there are ~27) with appropriate Logger calls:
     - `print('[GpsDataManager] 🚀 ...')` → `Logger.info('Starting GPS manager initialization...', 'GpsDataManager')`
     - `print('[GpsDataManager] ❌ ...')` → `Logger.error('GPS service disabled', 'GpsDataManager')`
     - `print('[GpsDataManager] ⚠️ ...')` → `Logger.warn('Already initialized, skipping...', 'GpsDataManager')`
     - `print('[GpsDataManager] 📡 ...')` → `Logger.debug('Raw GPS update: ...', 'GpsDataManager')`
     - `print('[GpsDataManager] ✅ ...')` → `Logger.info('GPS manager initialized successfully', 'GpsDataManager')`
     - Use error for failures/exceptions, warn for recoverable issues, info for lifecycle events, debug for data flow
   - Remove emoji prefixes from log messages — the Logger's color coding replaces them

2. **Update `lib/gps_service.dart`:**
   - No import changes needed (it only imports dart:async, geolocator, permission_handler)
   - Replace heading validation magic numbers (0, 360) with GpsConfig constants:
     ```dart
     import 'config/gps_constants.dart';
     ```
     Change `heading >= 0 && heading <= 360` to `heading >= GpsConfig.VALID_HEADING_MIN && heading <= GpsConfig.VALID_HEADING_MAX`

3. **Update `lib/main.dart`:**
   - Change `import 'speed_units.dart'` to `import 'config/speed_unit.dart'`
   - Change `import 'color_themes.dart'` to `import 'config/color_themes.dart'`
   - Add imports:
     ```dart
     import 'config/timing_constants.dart';
     import 'config/overlay_constants.dart';
     import 'models/overlay_message.dart';
     import 'services/logger.dart';
     ```
   - Remove the `customDebugPrint()` function definition (lines 16-20) — replaced by Logger
   - Replace ALL `customDebugPrint()` calls with appropriate `Logger.*()` calls:
     - Pattern: `customDebugPrint('[Main] emoji message')` → `Logger.debug('message', 'Main')` (or info/warn/error as appropriate)
     - Use Logger.error for catch blocks
     - Use Logger.warn for unexpected states
     - Use Logger.info for lifecycle transitions
     - Use Logger.debug for data flow, state changes, gesture events
   - Replace ALL remaining `print()` calls (lines 337-373 in _initializeGpsManager and _onGpsDataUpdate) with Logger calls
   - Replace magic numbers in timer durations:
     - `Duration(seconds: 5)` in heartbeat → `TimingConfig.HEARTBEAT_INTERVAL`
     - `Duration(milliseconds: 1000)` in overlay check → `TimingConfig.OVERLAY_STATUS_CHECK_INTERVAL`
     - `Duration(milliseconds: 500)` in tap close → `TimingConfig.TAP_CLOSE_DELAY`
     - `Duration(milliseconds: 50)` in long press delay → `TimingConfig.LONG_PRESS_SIGNAL_DELAY`
   - Replace magic numbers in overlay sizing:
     - `0.45` width percentage → `OverlayConfig.WIDTH_PERCENTAGE`
     - `0.6` aspect ratio → `OverlayConfig.ASPECT_RATIO`
     - `0.85` background opacity → `OverlayConfig.BACKGROUND_OPACITY`
     - `0.3` border opacity → `OverlayConfig.BORDER_OPACITY`
   - Replace overlay IPC Maps with OverlayMessage typed class:
     - In `_pushDataToOverlay()`: replace `FlutterOverlayWindow.shareData({...map...})` with `FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay(...).toMap())`
     - In `_showFloatingWindow()`: replace the shareData map with `OverlayMessage.updateDisplay(...).toMap()`
     - In overlay `_listenToMainAppMessages()`: parse incoming data with `OverlayMessage.fromMap(data)`
     - In overlay long press: replace `FlutterOverlayWindow.shareData({'action': 'longPressClose'})` with `FlutterOverlayWindow.shareData(OverlayMessage.longPressClose().toMap())`
   - In `_startListeningToOverlayMessages()`: parse incoming data using OverlayMessage.fromMap() instead of raw Map access
   - In overlay constants: replace `_systemScreenWidth = 400` default with `OverlayConfig.DEFAULT_SCREEN_WIDTH`, etc.

4. **Delete old files:**
   - Delete `lib/speed_units.dart` (moved to config/speed_unit.dart)
   - Delete `lib/color_themes.dart` (moved to config/color_themes.dart)

**IMPORTANT:** Do not change any business logic, UI layout, gesture handling, or widget structure. Only change imports, constant references, Map→OverlayMessage conversions, and print→Logger replacements. The app must behave identically.
- Run `flutter analyze` — zero errors - Verify `lib/speed_units.dart` and `lib/color_themes.dart` no longer exist - Grep for bare `print(` in lib/ — should find zero occurrences (all replaced with Logger) - Grep for `customDebugPrint(` in lib/ — should find zero occurrences (all replaced with Logger) - Grep for `import 'speed_units.dart'` — should find zero occurrences - Grep for `import 'color_themes.dart'` — should find zero occurrences (only config/color_themes.dart) - Verify main.dart no longer contains the customDebugPrint function definition All imports rewired to new file locations. All print()/customDebugPrint() replaced with Logger calls. All behavioral magic numbers replaced with named constants. OverlayMessage typed class used for all overlay IPC. Old speed_units.dart and color_themes.dart deleted. App compiles with zero analyzer errors. After all 3 tasks complete:
  1. flutter analyze returns zero errors
  2. No bare print() calls remain in lib/ (grep confirms)
  3. No customDebugPrint() calls remain in lib/ (grep confirms)
  4. Old files deleted: lib/speed_units.dart, lib/color_themes.dart
  5. Directory structure exists: lib/screens/, lib/widgets/, lib/providers/, lib/models/, lib/config/, lib/services/
  6. Files exist in new locations:
    • lib/services/logger.dart
    • lib/config/gps_constants.dart, timing_constants.dart, overlay_constants.dart
    • lib/config/speed_unit.dart, color_themes.dart
    • lib/models/processed_gps_data.dart, overlay_message.dart, theme_model.dart
  7. gps_data_manager.dart imports ProcessedGpsData from models/ and uses GpsConfig constants
  8. main.dart imports SpeedUnit from config/ and uses TimingConfig/OverlayConfig constants
  9. Overlay IPC uses OverlayMessage.toMap()/fromMap() instead of raw Maps

Requirements coverage: ORG-01 (partial — dirs created), ORG-02, ORG-03, ORG-04, ORG-05, ORG-06

<success_criteria>

  • App compiles cleanly with flutter analyze showing zero errors
  • All 8 requirements (ORG-01 through ORG-06) addressed by this plan are satisfied
  • Zero bare print() or customDebugPrint() calls remain in codebase
  • All behavioral constants use named references from config/ files
  • OverlayMessage typed class handles all overlay IPC serialization </success_criteria>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`