Files
gps-speedo/.planning/phases/01-foundation/01-01-PLAN.md
T
Mikkeli Matlock 7e9ed22e40 fix(01): revise plans based on checker feedback
- Clarified ThemeModel delegation rationale (Issue 1)
- Documented why Task 3 is one atomic task (Issue 2)
- Made customDebugPrint deletion step unambiguous (Issue 3)

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

24 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,
       );
     }
   }
   ```

   **Design rationale — delegation IS "holding":** The user's mental model says ThemeModel "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." ThemeModel satisfies this by delegating palette storage to ColorThemes (which holds the static list of all palettes). From the consumer's perspective, ThemeModel IS the single object that holds palettes, exposes `current` (the active ColorTheme with named colors), `currentSetName` (the label), and `rotate()` (circular cycling). The delegation to ColorThemes is an implementation detail — callers never interact with ColorThemes directly. This avoids duplicating the palette data while giving the user exactly the API they described.

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.
**Why this is one task (not split):** All changes here are interdependent — you cannot update imports without also replacing the constants they point to, and you cannot delete old files until all imports have been rewired. Splitting into "rewire imports" + "replace constants" + "delete files" would create intermediate broken states where imports point to non-existent symbols. The changes span 3 files but are mechanically repetitive (find/replace patterns), not intellectually complex. A single atomic pass is safer.

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';
     ```
   - **Delete the `customDebugPrint()` function definition from main.dart** (lines 16-20, the `void customDebugPrint(String message)` function body). This function is fully replaced by Logger. Remove the entire function, not just its body.
   - 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`