diff --git a/docs/plans/2026-01-12-todo-bugs-design.md b/docs/plans/2026-01-12-todo-bugs-design.md new file mode 100644 index 0000000..182531a --- /dev/null +++ b/docs/plans/2026-01-12-todo-bugs-design.md @@ -0,0 +1,353 @@ +# TODO Bugs Design Document + +**Date:** 2026-01-12 +**Status:** Ready for Implementation + +## Overview + +This document outlines fixes for 9 bugs from TO-DOS.md affecting performance, UI polish, and the data scraper. + +--- + +## Category A: Performance Issues (3 bugs) + +### Bug 1: UI Unresponsive on Launch Until Featured Loads + +**Problem:** App freezes on launch while generating suggested trips. + +**Root Cause:** `SuggestedTripsGenerator` is `@MainActor`, so all `TripPlanningEngine` work (including TSP solving) blocks the main thread. + +**Files to Modify:** +- `SportsTime/Core/Services/SuggestedTripsGenerator.swift` +- `SportsTime/Features/Home/Views/HomeView.swift` + +**Fix:** +1. Move heavy computation to background using `Task.detached` +2. Show skeleton/placeholder UI immediately +3. Keep only UI state updates on `@MainActor` + +**Implementation:** +```swift +func generateTrips() async { + guard !isLoading else { return } + isLoading = true + loadingMessage = await loadingTextGenerator.generateMessage() + + // Move heavy work off main actor + let result = await Task.detached(priority: .userInitiated) { [dataProvider, planningEngine] in + // Ensure data is loaded + let teams = await dataProvider.teams + let stadiums = await dataProvider.stadiums + let games = try? await dataProvider.filterGames(...) + + // All planning engine work here + // Return generated trips + }.value + + // Back on MainActor for UI update + self.suggestedTrips = result + isLoading = false +} +``` + +--- + +### Bug 2: Lag on Big Data Sets ("By Game") + +**Problem:** Schedule list view with 1000+ games causes UI lag. + +**Root Cause:** SwiftUI List struggles with large datasets. Each `GameRowView` performs: +- Rich game lookups +- Date formatting on every render +- No virtualization + +**Files to Modify:** +- `SportsTime/Features/Schedule/Views/ScheduleListView.swift` +- `SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift` + +**Fix:** +1. Implement pagination with visible window +2. Cache formatted dates and lookups +3. Use `LazyVStack` with explicit `.id()` for efficient diffing + +**Implementation:** +```swift +struct ScheduleListView: View { + @State private var visibleRange: Range = 0..<50 + private let pageSize = 50 + + var body: some View { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(visibleGames) { game in + GameRowView(game: game, showDate: true) + .id(game.id) + .onAppear { + loadMoreIfNeeded(game) + } + } + } + } + } + + private func loadMoreIfNeeded(_ game: RichGame) { + // Load next page when approaching end + } +} +``` + +--- + +### Bug 4: Select Games View Laggy and Two Different Colors + +**Problem:** Game selection view has performance issues and visual inconsistency. + +**Root Cause:** Same as Bug 2 - large list without virtualization. Color issue likely from inconsistent background modifiers. + +**Files to Modify:** +- `SportsTime/Features/Trip/Views/TripCreationView.swift` (game selection section) + +**Fix:** +1. Apply same pagination fix as Bug 2 +2. Audit background colors - use consistent `.background()` modifier +3. Ensure selected/unselected states use same base colors + +--- + +## Category B: Missing Info & Animation Issues (3 bugs) + +### Bug 3: "By Games" Missing Location Info + +**Problem:** Games grouped by date don't show stadium/city. + +**Root Cause:** `GameRowView` designed for team-grouped context where location is implicit. + +**Files to Modify:** +- `SportsTime/Features/Schedule/Views/GameRowView.swift` +- `SportsTime/Features/Schedule/Views/ScheduleListView.swift` + +**Fix:** +1. Add `showLocation: Bool` parameter to `GameRowView` +2. Pass `true` when `groupBy == .byGame` + +**Implementation:** +```swift +struct GameRowView: View { + let game: RichGame + var showDate: Bool = false + var showLocation: Bool = false // New parameter + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + // Existing matchup content... + + if showLocation { + Text("\(game.stadium.name), \(game.stadium.city)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } +} + +// In ScheduleListView: +GameRowView(game: game, showDate: true, showLocation: groupBy == .byGame) +``` + +--- + +### Bug 5: Weird Animation When Selecting a Game + +**Problem:** Unexpected animation when tapping a game in selection view. + +**Root Cause:** SwiftUI animation conflict between List selection and state update. + +**Files to Modify:** +- `SportsTime/Features/Trip/Views/TripCreationView.swift` + +**Fix:** +1. Wrap selection in transaction to disable implicit animation +2. Or use `.animation(nil, value:)` to suppress + +**Implementation:** +```swift +// Option A: Disable animation on selection +Button { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + viewModel.toggleGameSelection(game) + } +} label: { + GameSelectionRow(game: game, isSelected: isSelected) +} +.buttonStyle(.plain) + +// Option B: On the selection indicator +Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") + .animation(nil, value: isSelected) +``` + +--- + +### Bug 6: Trip Game Times Should Be UTC to Local + +**Problem:** Game times display in device timezone, not stadium local time. + +**Root Cause:** `DateFormatter` without explicit timezone uses device default. + +**Files to Modify:** +- `SportsTime/Core/Models/Domain/Stadium.swift` +- `SportsTime/Core/Models/Domain/Game.swift` + +**Fix:** +1. Add `timeZoneIdentifier: String` to Stadium (or derive from coordinates) +2. Create `localGameTime` computed property on `RichGame` +3. Display with timezone abbreviation: "7:05 PM ET" + +**Implementation:** +```swift +// Stadium.swift - add timezone +struct Stadium: Identifiable, Codable, Hashable { + // ... existing properties + let timeZoneIdentifier: String? // e.g., "America/New_York" + + var timeZone: TimeZone? { + timeZoneIdentifier.flatMap { TimeZone(identifier: $0) } + } +} + +// RichGame extension +extension RichGame { + var localGameTime: String { + let formatter = DateFormatter() + formatter.dateFormat = "h:mm a z" + formatter.timeZone = stadium.timeZone ?? .current + return formatter.string(from: game.dateTime) + } +} +``` + +**Note:** Requires populating `timeZoneIdentifier` for all stadiums in: +- Bundled JSON bootstrap data +- CloudKit stadium records +- Scraper stadium resolver + +--- + +## Category C: UI Polish Issues (2 bugs) + +### Bug 7: Pace Capsule Animation Looks Off + +**Problem:** "packed"/"moderate"/"relaxed" label has glitchy animation. + +**Root Cause:** Implicit animation conflict or text morphing transition. + +**Files to Modify:** +- `SportsTime/Features/Home/Views/SavedTripsListView.swift` (or wherever pace capsule is) + +**Fix:** +1. Use `.contentTransition(.identity)` to prevent text morphing +2. Disable animation on the capsule when trip data changes + +**Implementation:** +```swift +Text(trip.paceLabel) + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(paceColor.opacity(0.2)) + .foregroundStyle(paceColor) + .clipShape(Capsule()) + .contentTransition(.identity) + .animation(nil, value: trip.id) +``` + +--- + +### Bug 8: Remove "My Trips" Title from Tab View + +**Problem:** Redundant title appearing in the My Trips tab. + +**Files to Modify:** +- `SportsTime/Features/Home/Views/HomeView.swift` (or main TabView) + +**Fix:** +1. Remove `.navigationTitle()` from SavedTripsListView root +2. Or hide navigation bar if title is inherited + +**Implementation:** +```swift +// In TabView: +NavigationStack { + SavedTripsListView(trips: savedTrips) + .toolbar(.hidden, for: .navigationBar) + // Or just remove any .navigationTitle("My Trips") +} +.tabItem { + Label("My Trips", systemImage: "suitcase") +} +``` + +--- + +## Category D: Scraper Issue (1 bug) + +### Bug 9: Frost Bank Center Team Mapping Missing + +**Problem:** NBA scraper can't resolve Frost Bank Center (Spurs home). + +**Root Cause:** Scraper likely returns old name "AT&T Center" which doesn't match. Stadium exists in resolver but alias is missing. + +**Files to Modify:** +- `Scripts/stadium_aliases.json` + +**Fix:** +1. Add alias for "AT&T Center" → "stadium_nba_frost_bank_center" +2. Check scraper logs for exact unresolved name + +**Implementation:** +```json +{ + "canonical_id": "stadium_nba_frost_bank_center", + "alias_name": "AT&T Center", + "sport": "nba", + "valid_from": "2002-10-18", + "valid_to": "2024-07-01", + "notes": "Renamed to Frost Bank Center in 2024" +} +``` + +**Investigation:** Run NBA scraper with verbose logging to confirm the exact stadium name being returned. + +--- + +## Implementation Priority + +1. **High Impact (Performance):** + - Bug 1: Launch responsiveness (blocking UX) + - Bug 2 & 4: List performance (core feature usability) + +2. **Medium Impact (UX Polish):** + - Bug 3: Location info (missing data) + - Bug 6: Timezone display (incorrect data) + - Bug 5: Selection animation (visual glitch) + +3. **Low Impact (Minor Polish):** + - Bug 7: Pace capsule animation + - Bug 8: Remove redundant title + - Bug 9: Scraper alias (data pipeline) + +--- + +## Testing Checklist + +- [ ] App launches with responsive UI, trips load in background +- [ ] "By Game" view scrolls smoothly with 1000+ games +- [ ] "By Game" shows stadium and city for each game +- [ ] Game selection has no weird animation on tap +- [ ] Trip game times show stadium local time with timezone (e.g., "7:05 PM ET") +- [ ] Pace capsule has no animation glitch +- [ ] "My Trips" tab has no redundant title +- [ ] NBA scraper resolves Spurs games to Frost Bank Center