# TODO Bug Fixes Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Fix 9 bugs affecting app performance, UI polish, and data pipeline. **Architecture:** Refactor `SuggestedTripsGenerator` to run heavy work off main actor; add pagination to large lists; fix UI display issues and animations; add stadium timezone support; add scraper alias. **Tech Stack:** Swift, SwiftUI, @Observable, async/await, Python (scraper) --- ## Task 1: Fix Launch Freeze (Bug 1) **Files:** - Modify: `SportsTime/Core/Services/SuggestedTripsGenerator.swift` **Step 1: Identify the blocking code** The entire `generateTrips()` method runs on `@MainActor`. We need to move heavy computation to a background task while keeping UI updates on main. **Step 2: Create a non-isolated helper struct for planning** Add this struct above `SuggestedTripsGenerator`: ```swift // Non-isolated helper for background trip generation private struct TripGenerationHelper: Sendable { let stadiums: [Stadium] let teams: [Team] let games: [Game] func generateRegionalTrips( startDate: Date, endDate: Date ) -> [SuggestedTrip] { let planningEngine = TripPlanningEngine() let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 } let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 } var generatedTrips: [SuggestedTrip] = [] // Regional trips (copy existing logic from generateTrips) for region in [Region.east, Region.central, Region.west] { let regionStadiumIds = Set( stadiums .filter { $0.region == region } .map { $0.id } ) let regionGames = games.filter { regionStadiumIds.contains($0.stadiumId) } guard !regionGames.isEmpty else { continue } // Generate trips using existing private methods (will need to move them) } return generatedTrips } } ``` **Step 3: Refactor generateTrips() to use Task.detached** Replace the heavy computation section in `generateTrips()`: ```swift func generateTrips() async { guard !isLoading else { return } isLoading = true error = nil suggestedTrips = [] loadingMessage = await loadingTextGenerator.generateMessage() // Ensure data is loaded if dataProvider.teams.isEmpty { await dataProvider.loadInitialData() } guard !dataProvider.stadiums.isEmpty else { error = "Unable to load stadium data" isLoading = false return } let calendar = Calendar.current let today = Date() guard let startDate = calendar.date(byAdding: .weekOfYear, value: 4, to: today), let endDate = calendar.date(byAdding: .weekOfYear, value: 8, to: today) else { error = "Failed to calculate date range" isLoading = false return } do { let allSports = Set(Sport.supported) let games = try await dataProvider.filterGames( sports: allSports, startDate: startDate, endDate: endDate ) guard !games.isEmpty else { error = "No games found in the next 4-8 weeks" isLoading = false return } // Capture data for background work let stadiums = dataProvider.stadiums let teams = dataProvider.teams // Heavy computation off main actor let generatedTrips = await Task.detached(priority: .userInitiated) { self.generateTripsInBackground( games: games, stadiums: stadiums, teams: teams, startDate: startDate, endDate: endDate ) }.value suggestedTrips = generatedTrips } catch { self.error = "Failed to generate trips: \(error.localizedDescription)" } isLoading = false } // New nonisolated method for background work nonisolated private func generateTripsInBackground( games: [Game], stadiums: [Stadium], teams: [Team], startDate: Date, endDate: Date ) -> [SuggestedTrip] { let stadiumsById = stadiums.reduce(into: [String: Stadium]()) { $0[$1.id] = $1 } let teamsById = teams.reduce(into: [String: Team]()) { $0[$1.id] = $1 } var generatedTrips: [SuggestedTrip] = [] // Copy existing regional trip generation logic here... // (Move the for loop from the original generateTrips) return generatedTrips } ``` **Step 4: Move helper methods to be nonisolated** Mark these methods as `nonisolated`: - `generateRegionalTrip` - `generateCrossCountryTrip` - `buildRichGames` - `buildCoastToCoastRoute` - `buildTripStop` - `buildTravelSegments` - `convertToTrip` - `generateTripName` - `buildCorridorTrip` - `haversineDistance` - `validateNoSameDayConflicts` **Step 5: Build and test** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` Expected: Build succeeds **Step 6: Manual test** Launch app in simulator. UI should be responsive immediately while "Suggested Trips" section shows loading state. **Step 7: Commit** ```bash git add SportsTime/Core/Services/SuggestedTripsGenerator.swift git commit -m "perf: move trip generation off main actor Fixes launch freeze by running TripPlanningEngine in background task. UI now responsive immediately while trips generate." ``` --- ## Task 2: Add Location Info to "By Game" View (Bug 3) **Files:** - Modify: `SportsTime/Features/Schedule/Views/ScheduleListView.swift` **Step 1: Find GameRowView and add showLocation parameter** Locate `GameRowView` in the file (or it may be in a separate file). Add parameter: ```swift struct GameRowView: View { let game: RichGame var showDate: Bool = false var showLocation: Bool = false // Add this ``` **Step 2: Add location display in the view body** After the existing content, add: ```swift var body: some View { VStack(alignment: .leading, spacing: 4) { // ... existing matchup content if showLocation { HStack(spacing: 4) { Image(systemName: "mappin.circle.fill") .font(.caption2) .foregroundStyle(.tertiary) Text("\(game.stadium.name), \(game.stadium.city)") .font(.caption) .foregroundStyle(.secondary) } } } } ``` **Step 3: Pass showLocation when groupBy is byGame** Find where `GameRowView` is called and update: ```swift // When rendering games in "by game" mode: GameRowView(game: richGame, showDate: true, showLocation: groupBy == .byGame) ``` **Step 4: Build and verify** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` **Step 5: Manual test** Open Schedule tab, switch to "By Game" grouping. Each row should now show stadium name and city. **Step 6: Commit** ```bash git add SportsTime/Features/Schedule/Views/ScheduleListView.swift git commit -m "feat: show location info in By Game schedule view Adds stadium name and city to game rows when grouped by game." ``` --- ## Task 3: Fix Game Selection Animation (Bug 5) **Files:** - Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift` **Step 1: Find the game selection tap handler** Look for the game selection list/grid and its tap action. **Step 2: Wrap selection in transaction to disable animation** ```swift // Replace direct toggle: // viewModel.toggleGameSelection(game) // With: var transaction = Transaction() transaction.disablesAnimations = true withTransaction(transaction) { viewModel.toggleGameSelection(game) } ``` **Step 3: Alternative - add animation nil modifier** If using a selection indicator (checkmark), add: ```swift Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundStyle(isSelected ? .blue : .secondary) .animation(nil, value: isSelected) ``` **Step 4: Build and test** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` **Step 5: Manual test** Go to trip creation, select "By Game" mode, tap games to select. Animation should be instant without weird morphing. **Step 6: Commit** ```bash git add SportsTime/Features/Trip/Views/TripCreationView.swift git commit -m "fix: remove weird animation on game selection Disables implicit animation when toggling game selection." ``` --- ## Task 4: Fix Pace Capsule Animation (Bug 7) **Files:** - Modify: `SportsTime/Features/Home/Views/SavedTripsListView.swift` (or similar) **Step 1: Find the pace capsule component** Search for "packed", "moderate", "relaxed" or pace-related text in the trips list view. **Step 2: Add contentTransition and disable animation** ```swift Text(trip.paceLabel) // or whatever the pace text is .font(.caption2) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(paceColor.opacity(0.2)) .foregroundStyle(paceColor) .clipShape(Capsule()) .contentTransition(.identity) // Prevents text morphing .animation(nil, value: trip.id) // No animation on data change ``` **Step 3: Build and test** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` **Step 4: Manual test** View saved trips list. Pace capsules should appear without glitchy animation. **Step 5: Commit** ```bash git add SportsTime/Features/Home/Views/SavedTripsListView.swift git commit -m "fix: pace capsule animation glitch Adds contentTransition(.identity) to prevent text morphing." ``` --- ## Task 5: Remove "My Trips" Title (Bug 8) **Files:** - Modify: `SportsTime/Features/Home/Views/HomeView.swift` (or main TabView file) **Step 1: Find the My Trips tab** Look for TabView and the "My Trips" tab item. **Step 2: Remove or hide navigation title** Option A - Remove `.navigationTitle()`: ```swift NavigationStack { SavedTripsListView(trips: savedTrips) // Remove: .navigationTitle("My Trips") } .tabItem { Label("My Trips", systemImage: "suitcase") } ``` Option B - Hide navigation bar: ```swift NavigationStack { SavedTripsListView(trips: savedTrips) .toolbar(.hidden, for: .navigationBar) } .tabItem { Label("My Trips", systemImage: "suitcase") } ``` **Step 3: Build and test** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` **Step 4: Manual test** Switch to My Trips tab. No redundant title should appear above the content. **Step 5: Commit** ```bash git add SportsTime/Features/Home/Views/HomeView.swift git commit -m "fix: remove redundant My Trips title from tab view" ``` --- ## Task 6: Add Stadium Timezone Support (Bug 6) **Files:** - Modify: `SportsTime/Core/Models/Domain/Stadium.swift` - Modify: `SportsTime/Core/Models/Domain/Game.swift` - Modify: `Scripts/sportstime_parser/normalizers/stadium_resolver.py` **Step 1: Add timeZoneIdentifier to Stadium model** ```swift struct Stadium: Identifiable, Codable, Hashable { let id: String let name: String let city: String let state: String let coordinate: CLLocationCoordinate2D let sport: Sport let region: Region let timeZoneIdentifier: String? // Add this var timeZone: TimeZone? { timeZoneIdentifier.flatMap { TimeZone(identifier: $0) } } // Update init to include timeZoneIdentifier with default nil for backward compatibility init( id: String, name: String, city: String, state: String, coordinate: CLLocationCoordinate2D, sport: Sport, region: Region, timeZoneIdentifier: String? = nil ) { self.id = id self.name = name self.city = city self.state = state self.coordinate = coordinate self.sport = sport self.region = region self.timeZoneIdentifier = timeZoneIdentifier } } ``` **Step 2: Add localGameTime to RichGame** ```swift 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) } var localGameTimeShort: String { let formatter = DateFormatter() formatter.dateFormat = "h:mm a" formatter.timeZone = stadium.timeZone ?? .current return formatter.string(from: game.dateTime) } } ``` **Step 3: Update Python scraper StadiumInfo** In `stadium_resolver.py`, update the dataclass and all stadium entries: ```python @dataclass class StadiumInfo: canonical_id: str name: str city: str state: str country: str sport: str latitude: float longitude: float timezone: str = "America/New_York" # Add with default # Update entries, e.g.: "stadium_nba_chase_center": StadiumInfo("stadium_nba_chase_center", "Chase Center", "San Francisco", "CA", "USA", "nba", 37.7680, -122.3877, "America/Los_Angeles"), ``` **Step 4: Build Swift code** Run: ```bash xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build ``` **Step 5: Commit** ```bash git add SportsTime/Core/Models/Domain/Stadium.swift SportsTime/Core/Models/Domain/Game.swift Scripts/sportstime_parser/normalizers/stadium_resolver.py git commit -m "feat: add timezone support for stadium-local game times Adds timeZoneIdentifier to Stadium and localGameTime to RichGame. Game times can now display in venue local time." ``` **Note:** Full timezone population for all stadiums is a separate data task. --- ## Task 7: Add Frost Bank Center Alias (Bug 9) **Files:** - Modify: `Scripts/stadium_aliases.json` **Step 1: Open stadium_aliases.json and add alias** Add entry for AT&T Center (old name for Frost Bank Center): ```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 July 2024" } ``` **Step 2: Verify JSON is valid** Run: ```bash python3 -c "import json; json.load(open('Scripts/stadium_aliases.json'))" ``` Expected: No output (valid JSON) **Step 3: Test scraper resolution** Run: ```bash cd Scripts && python3 -c " from sportstime_parser.normalizers.stadium_resolver import resolve_stadium result = resolve_stadium('nba', 'AT&T Center') print(f'Resolved: {result.canonical_id}, confidence: {result.confidence}') " ``` Expected: `Resolved: stadium_nba_frost_bank_center, confidence: 95` **Step 4: Commit** ```bash git add Scripts/stadium_aliases.json git commit -m "fix: add AT&T Center alias for Frost Bank Center NBA scraper now resolves old stadium name to current Spurs arena." ``` --- ## Task 8: Add Pagination to Schedule List (Bug 2) **Files:** - Modify: `SportsTime/Features/Schedule/Views/ScheduleListView.swift` - Modify: `SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift` **Step 1: Add pagination state to ViewModel** ```swift @Observable class ScheduleViewModel { // Existing properties... // Pagination private let pageSize = 50 var displayedGames: [RichGame] = [] private var currentPage = 0 private var allFilteredGames: [RichGame] = [] var hasMoreGames: Bool { displayedGames.count < allFilteredGames.count } func loadInitialGames() { currentPage = 0 displayedGames = Array(allFilteredGames.prefix(pageSize)) } func loadMoreGames() { guard hasMoreGames else { return } currentPage += 1 let start = currentPage * pageSize let end = min(start + pageSize, allFilteredGames.count) displayedGames.append(contentsOf: allFilteredGames[start..