Files
Sportstime/docs/plans/2026-01-12-todo-bugs-implementation.md
Trey t 3d40145ffb docs: update planning documents and todos
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 13:16:52 -06:00

21 KiB

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:

// 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():

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:

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

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:

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:

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:

// When rendering games in "by game" mode:
GameRowView(game: richGame, showDate: true, showLocation: groupBy == .byGame)

Step 4: Build and verify

Run:

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

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

// 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:

Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
    .foregroundStyle(isSelected ? .blue : .secondary)
    .animation(nil, value: isSelected)

Step 4: Build and test

Run:

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

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

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:

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

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():

NavigationStack {
    SavedTripsListView(trips: savedTrips)
    // Remove: .navigationTitle("My Trips")
}
.tabItem {
    Label("My Trips", systemImage: "suitcase")
}

Option B - Hide navigation bar:

NavigationStack {
    SavedTripsListView(trips: savedTrips)
        .toolbar(.hidden, for: .navigationBar)
}
.tabItem {
    Label("My Trips", systemImage: "suitcase")
}

Step 3: Build and test

Run:

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

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

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

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:

@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:

xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build

Step 5: Commit

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):

{
  "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:

python3 -c "import json; json.load(open('Scripts/stadium_aliases.json'))"

Expected: No output (valid JSON)

Step 3: Test scraper resolution

Run:

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

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

@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..<end])
    }
}

Step 2: Update filtering to populate allFilteredGames

In the method that filters/fetches games:

func applyFilters() async {
    // ... existing filter logic that produces games

    allFilteredGames = filteredGames  // Store full list
    loadInitialGames()  // Display first page
}

Step 3: Update ScheduleListView to use displayedGames

struct ScheduleListView: View {
    @Bindable var viewModel: ScheduleViewModel

    var body: some View {
        List {
            ForEach(viewModel.displayedGames) { game in
                GameRowView(game: game, showDate: true, showLocation: viewModel.groupBy == .byGame)
                    .onAppear {
                        if game.id == viewModel.displayedGames.last?.id {
                            viewModel.loadMoreGames()
                        }
                    }
            }

            if viewModel.hasMoreGames {
                ProgressView()
                    .frame(maxWidth: .infinity)
                    .listRowBackground(Color.clear)
            }
        }
    }
}

Step 4: Build and test

Run:

xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build

Step 5: Manual test

Open Schedule tab with "By Game" grouping. List should load quickly with 50 games, then load more as you scroll.

Step 6: Commit

git add SportsTime/Features/Schedule/Views/ScheduleListView.swift SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift
git commit -m "perf: add pagination to schedule list

Loads 50 games at a time to fix lag with large datasets."

Task 9: Fix Game Selection View Performance (Bug 4)

Files:

  • Modify: SportsTime/Features/Trip/Views/TripCreationView.swift
  • Modify: SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift

Step 1: Add pagination to TripCreationViewModel

Similar to Task 8, add pagination for the game selection:

// In TripCreationViewModel
private let gamePageSize = 50
var displayedAvailableGames: [RichGame] = []
private var currentGamePage = 0

var hasMoreAvailableGames: Bool {
    displayedAvailableGames.count < availableGames.count
}

func loadInitialAvailableGames() {
    currentGamePage = 0
    displayedAvailableGames = Array(availableGames.prefix(gamePageSize))
}

func loadMoreAvailableGames() {
    guard hasMoreAvailableGames else { return }
    currentGamePage += 1
    let start = currentGamePage * gamePageSize
    let end = min(start + gamePageSize, availableGames.count)
    displayedAvailableGames.append(contentsOf: availableGames[start..<end])
}

Step 2: Update game selection view to use displayedAvailableGames

// In game selection section of TripCreationView
LazyVStack(spacing: 8) {
    ForEach(viewModel.displayedAvailableGames) { game in
        GameSelectionRow(game: game, isSelected: viewModel.selectedGameIds.contains(game.id))
            .onTapGesture {
                var transaction = Transaction()
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    viewModel.toggleGameSelection(game)
                }
            }
            .onAppear {
                if game.id == viewModel.displayedAvailableGames.last?.id {
                    viewModel.loadMoreAvailableGames()
                }
            }
    }
}

Step 3: Fix color inconsistency

Ensure all game rows use consistent background:

struct GameSelectionRow: View {
    let game: RichGame
    let isSelected: Bool

    var body: some View {
        HStack {
            // Game info...

            Spacer()

            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
                .foregroundStyle(isSelected ? .blue : .secondary)
        }
        .padding()
        .background(Color(.systemBackground))  // Consistent background
        .clipShape(RoundedRectangle(cornerRadius: 8))
    }
}

Step 4: Build and test

Run:

xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build

Step 5: Manual test

Create a trip with "By Game" mode. Game selection should be smooth and colors consistent.

Step 6: Commit

git add SportsTime/Features/Trip/Views/TripCreationView.swift SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift
git commit -m "perf: add pagination to game selection view

Fixes lag and color inconsistency in trip creation game selection."

Final Task: Run Full Test Suite

Step 1: Run all tests

xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test

Step 2: Verify all pass

Expected: All tests pass (existing 63 tests)

Step 3: Manual smoke test

  • Launch app - UI responsive immediately
  • Home tab - suggested trips load in background
  • Schedule tab - "By Game" shows location, scrolls smoothly
  • Trip creation - game selection smooth, no weird animation
  • My Trips - no redundant title, pace capsules look good