- Add a11y label to ProgressMapView reset button and progress bar values
- Fix CADisplayLink retain cycle in ItineraryTableViewController via deinit
- Add [weak self] to PhotoGalleryViewModel Task closure
- Add @MainActor to TripWizardViewModel, remove manual MainActor.run hop
- Fix O(n²) rank lookup in PollDetailView/DebugPollPreviewView with enumerated()
- Cache itinerarySections via ItinerarySectionBuilder static extraction + @State
- Convert CanonicalSyncService/BootstrapService from actor to @MainActor final class
- Add .accessibilityHidden(true) to RegionMapSelector Map to prevent duplicate VoiceOver
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include segment index in travel anchor IDs ("travel:INDEX:from->to")
so Follow Team trips visiting the same city pair multiple times get
unique, independently addressable travel segments. Prevents override
dictionary collisions and incorrect validDayRange lookups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add VoiceOver labels, hints, and element grouping across all 60+ views
- Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations
- Replace fixed font sizes with semantic Dynamic Type styles
- Hide decorative elements from VoiceOver with .accessibilityHidden(true)
- Add .minimumHitTarget() modifier ensuring 44pt touch targets
- Add AccessibilityAnnouncer utility for VoiceOver announcements
- Improve color contrast values in Theme.swift for WCAG AA compliance
- Extract CloudKitContainerConfig for explicit container identity
- Remove PostHog debug console log from AnalyticsManager
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run East/Central/West regional trips and cross-country routes
concurrently instead of sequentially, reducing wall-clock time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update CloudKit container ID to iCloud.com.88oakapps.SportsTime across all services
- Update IAP product IDs to match new bundle ID (com.88oakapps.SportsTime)
- Add app landing page with light, welcoming design matching app aesthetic
- Update entitlements and project configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ET timezone (America/New_York) to all Sports-Reference scrapers:
- NBA: Basketball-Reference times parsed as ET
- NFL: Pro-Football-Reference times parsed as ET
- NHL: Hockey-Reference times parsed as ET
- MLB: Baseball-Reference times parsed as ET
- Document source timezones in scraper docstrings
- Add 11 new stadiums to STADIUM_MAPPINGS:
- NFL: 5 international venues (Corinthians Arena, Croke Park,
Olympic Stadium Berlin, Santiago Bernabéu, Tom Benson Hall of Fame)
- MLS: 4 alternate venues (Miami Freedom Park, Citi Field,
LA Memorial Coliseum, M&T Bank Stadium)
- NWSL: 2 alternate venues (Northwestern Medicine Field, ONE Spokane)
- Add 15 stadium aliases for MLS/NWSL team-based lookups
- Fix CanonicalSyncService to sync timezone identifier to SwiftData
- Update debug logging to use stadium timezone for display
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix startDate to use Calendar.startOfDay instead of Date() to include
games earlier in the current day
- Add SyncLogger for file-based sync logging viewable in Settings
- Add "View Sync Logs" button in Settings debug section
- Add diagnostics and NBA game logging to ScheduleViewModel
- Add dropped game logging to DataProvider.filterRichGames
- Use SyncLogger in SportsTimeApp and CloudKitService for sync operations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add debug-only sync status monitoring to help diagnose CloudKit sync issues.
Shows last sync time, success/failure, and record counts for each entity type.
Includes manual sync trigger and re-enable button when sync is paused.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create shared TipsSection component for displaying planning tips
- Add TipsSection to all 22 home content variants
- Fix displayedTips population with onAppear in HomeView
- Add map buttons to GameRowCompact (opens stadium in Apple Maps)
- Add map buttons to TravelRowView (opens driving directions)
- Add map buttons to CustomItemRowView (opens location when GPS available)
- Add AppleMapsLauncher.openLocation() and openDirections() methods
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add floating action button (bottom-right) on trip map
- Create AppleMapsLauncher service to handle route opening
- Collect all routable stops (trip stops + custom items with coords)
- Handle >16 waypoints with alert offering to open in parts
- Silently skip stops without coordinates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Travel constraint validation was not working because ItineraryConstraints
had no game items to validate against - games came from RichGame objects
but were never converted to ItineraryItem for constraint checking.
Changes:
- Add city parameter to ItemKind.game enum case
- Create game ItineraryItems from RichGame data in buildItineraryData()
- Update isValidTravelPosition to compare against actual game sortOrders
- Fix tests to use appropriate game sortOrder conventions
Now travel is properly constrained to appear before arrival city games
and after departure city games.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add cancelPendingSync() method for explicit cleanup
- Use [weak self] capture to prevent potential retain issues
- Check Task.isCancelled before and after sleep
- Catch CancellationError from Task.sleep for immediate cancellation response
- Extract debounceInterval as constant
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update CKModels.swift to remove deleted type references
- Migrate LocalCustomItem to LocalItineraryItem in SavedTrip.swift
- Update AppDelegate to handle subscription removal
- Refactor AddItemSheet to create ItineraryItem with CustomInfo
- Update ItineraryTableViewController and Wrapper for new model
- Refactor TripDetailView state, methods and callbacks
- Fix TripMapView to display custom items with new model structure
This completes the migration from the legacy CustomItineraryItem/TravelDayOverride
model to the unified ItineraryItem model with ItemKind enum.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed:
- CustomItineraryItem
- TravelDayOverride
- CustomItemService
- CustomItemSubscriptionService
- TravelOverrideService
- CustomItemRow
These are replaced by unified ItineraryItem model and service.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Debounced updates (1.5s), local-first with silent retry.
Supports game, travel, and custom item kinds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fixed map header not updating in ItineraryTableViewWrapper using Coordinator pattern
- Added routeVersion UUID to force Map re-render when routes change
- Fixed determineAnchor to scan backwards for correct anchor context
- Added location support to CustomItineraryItem (lat/lng/address)
- Added MapKit place search to AddItemSheet
- Added extensive debug logging for route waypoint calculation
Known issues:
- Custom items still not routing correctly after drag/drop
- Anchor type determination may still have bugs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TravelDayOverride model for storing user-customized travel day positions
- Add TravelOverrideService for CloudKit CRUD operations on travel overrides
- Add CKTravelDayOverride CloudKit model wrapper
- Refactor itinerarySections to validate travel day bounds (must be after last game in departure city)
- Travel segments can now be dragged to different days within valid range
- Persist travel day overrides to CloudKit for cross-device sync
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CustomItineraryItem domain model with sortOrder for ordering
- Add CKCustomItineraryItem CloudKit wrapper for persistence
- Create CustomItemService for CRUD operations
- Create CustomItemSubscriptionService for real-time sync
- Add AppDelegate for push notification handling
- Add AddItemSheet for creating/editing items
- Add CustomItemRow with drag handle
- Update TripDetailView with continuous vertical timeline
- Enable drag-to-reorder using .draggable/.dropDestination
- Add inline "Add" buttons after games and travel segments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When updating a vote, CloudKit requires the server's changeTag to modify
existing records. Creating a new CKRecord caused "record to insert already
exists" errors. Now fetches the existing record first before saving.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add `displayName` computed property to Trip model that always
generates city list with " → " separator for consistent display
- Replace all `trip.name` usages with `trip.displayName` in UI files
- Update SuggestedTripsGenerator to use " → " separator
- Update PDFGenerator to use displayName for PDF titles
This ensures all trip names display consistently regardless of when
the trip was created or how the name was originally stored.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix suggested trips showing wrong sports for cross-country trips
- Remove quick start sections from home variants (Classic, Spotify)
- Remove dead quickActions code from HomeView
- Fix pace capsule animation in TripCreationView
- Add text wrapping to achievement descriptions
- Improve poll parsing with better error handling
- Various sharing system improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract deep link handling into dedicated DeepLinkHandler service
- Add MockData+Polls.swift with reusable test mocks for Trip, TripStop,
TripPoll, PollVote, and PollResults
- Update SportsTimeApp to use DeepLinkHandler.shared
- Add error alert for deep link failures
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add complete group trip polling feature allowing users to share trips
with friends for voting using Borda count scoring.
New components:
- TripPoll and PollVote domain models with share codes and rankings
- LocalTripPoll and LocalPollVote SwiftData models for persistence
- CKTripPoll and CKPollVote CloudKit record wrappers
- PollService actor for CloudKit CRUD operations and subscriptions
- PollCreation/Detail/Voting views and view models
- Deep link handling for sportstime://poll/{code} URLs
- Debug Pro status override toggle in Settings
Integration:
- HomeView shows polls section in My Trips
- SportsTimeApp registers SwiftData models and handles deep links
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CloudKit pagination: fetchAllRecords() handles >400 record batches
with cursor-based pagination (400 records per page)
- Cancellation support: SyncCancellationToken protocol enables graceful
sync termination when background tasks expire
- Per-entity progress: SyncState now tracks timestamps per entity type
so interrupted syncs resume where they left off
- NetworkMonitor: NWPathMonitor integration triggers sync on network
restoration with 2.5s debounce to handle WiFi↔cellular flapping
- wasCancelled flag in SyncResult distinguishes partial from full syncs
This addresses critical data sync issues:
- CloudKit queries were limited to ~400 records but bundled data has ~5000 games
- Background tasks could be killed mid-sync without saving progress
- App had no awareness of network state changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CKSport model to parse CloudKit Sport records
- Add fetchSportsForSync() to CloudKitService for delta fetching
- Add syncSports() and mergeSport() to CanonicalSyncService
- Update DataProvider with dynamicSports support and allSports computed property
- Update MockAppDataProvider with matching dynamic sports support
- Add comprehensive documentation for adding new sports
The app can now sync sport definitions from CloudKit, enabling new sports
to be added without app updates. Sports are fetched, merged into SwiftData,
and exposed via AppDataProvider.allSports alongside built-in Sport enum cases.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- PaywallView: remove unnecessary nil coalescing for currencyCode
- GameDAGRouter: change var to let for immutable compositeKeys
- GamesHistoryRow/View: add missing wnba and nwsl switch cases
- VisitDetailView: fix unused variable in preview
- AchievementEngine: use convenience init to avoid default parameter warning
- ProgressCardGenerator: use method overload instead of default parameter
- StadiumProximityMatcher: extract constants to ProximityConstants enum
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add LoadingSpinner component with small/medium/large sizes using system gray color
- Add LoadingPlaceholder for skeleton loading states
- Add LoadingSheet for full-screen blocking overlays
- Replace ThemedSpinner/ThemedSpinnerCompact across all views
- Remove deprecated loading components from AnimatedComponents.swift
- Delete LoadingTextGenerator.swift
- Fix PhotoImportView layout to fill full width
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hardcoded stadiumTeamMap with AppDataProvider lookup so
stadium renames (e.g., AT&T Center → Frost Bank Center) work
automatically via CloudKit sync without code changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace upfront loading of all games with lazy Sport → Team → Game
hierarchy. Games are now fetched per-team when expanded and cached
to prevent re-fetching. Also removes pagination workaround and
pre-computes groupings in ScheduleViewModel to avoid per-render work.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes launch freeze by running TripPlanningEngine in background task.
UI now responsive immediately while trips generate.
- Move heavy computation to Task.detached with .userInitiated priority
- Change all helper methods to nonisolated static functions
- Create local TripPlanningEngine in background task instead of instance property
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add allRichGames method to DataProvider
- Update TripCreationViewModel.loadGamesForBrowsing to use allGames (removes 90-day limit)
- Update MockCloudKitService sync methods to use new delta sync signatures
- Update MockAppDataProvider with renamed methods and new allGames/allRichGames
- Fix all callers: ScheduleViewModel, TripCreationViewModel, SuggestedTripsGenerator, GameMatcher
- Update CLAUDE.md documentation with new method names
This completes the delta sync implementation:
- CloudKit sync now uses modificationDate for proper delta sync
- First sync fetches ALL data, subsequent syncs only fetch modified records
- "By Games" mode now shows all available games (not just 90 days)
- All data types (stadiums, teams, games) use consistent delta sync pattern
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- fetchGames → filterGames (clarifies local SwiftData query)
- fetchRichGames → filterRichGames
- Add allGames(for:) method for unfiltered game access
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- syncStadiums now passes lastSync to CloudKit fetch
- syncTeams simplified to single CloudKit call (not per-sport loop)
- syncGames removes 6-month date range, uses modificationDate delta
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- fetchStadiumsForSync now accepts since: Date? parameter
- fetchTeamsForSync changed from per-sport to all teams with delta sync
- fetchGamesForSync uses modificationDate instead of game dateTime
When lastSync is nil, fetches all records (first sync).
When lastSync has value, fetches only modified records (delta sync).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This refactor fixes the achievement system by using stable canonical string
IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures
stadium mappings for achievements are consistent across app launches and
CloudKit sync operations.
Changes:
- Stadium, Team, Game: id property changed from UUID to String
- Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums
- CKModels: removed UUID parsing, use canonical IDs directly
- AchievementEngine: now matches against canonical stadium IDs
- All test files updated to use String IDs instead of UUID()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Green Monster (Fenway) and Ivy League (Wrigley) achievements
weren't working because:
1. Symbolic IDs use lowercase sport (stadium_mlb_bos)
2. Sport enum uses uppercase raw values (MLB)
3. Visits store stadium UUIDs, not symbolic IDs
Added resolveSymbolicStadiumId() helper that:
- Uppercases the sport string before Sport(rawValue:)
- Looks up team by abbreviation and sport
- Returns the team's stadiumId as UUID string
Also fixed:
- getStadiumIdsForLeague returns UUID strings (not symbolic IDs)
- AchievementProgress.isEarned computed from progress OR stored record
- getStadiumIdsForDivision queries CanonicalTeam properly
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Lock all maps to North America (no pan/zoom) in ProgressMapView and TripDetailView
- Sort saved trips by most cities (stops count)
- Filter cross-country trips to top 2 by stops on home screen
- Use LocationSearchSheet for Follow Team home location (consistent with must-stop)
- Initialize DateRangePicker to show selected dates on appear
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add RegionMapSelector UI for geographic trip filtering (East/Central/West)
- Add RouteFilters module for allowRepeatCities preference
- Improve GameDAGRouter to preserve route length diversity
- Routes now grouped by city count before scoring
- Ensures 2-city trips appear alongside longer trips
- Increased beam width and max options for better coverage
- Add TripOptionsView filters (max cities slider, pace filter)
- Remove TravelStyle section from trip creation (replaced by region selector)
- Clean up debug logging from DataProvider and ScenarioAPlanner
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix same-day different-city validation in C2C routes (no more impossible
games like Detroit 7:30pm AND Milwaukee 8pm on the same day)
- Cap C2C trips at 14 days max with 3 middle stops, prefer shortest routes
- Add sport icon and name to game rows in trip itinerary
- Add horizontal scroll to route dots in suggested trip cards
- Allow swipe-to-dismiss on home sheet (trip planner still blocks)
- Generate travel segments for suggested trips
- Increase DAG route lookahead to 5 days for multi-day drives
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add local canonicalization pipeline (stadiums, teams, games) that generates
deterministic canonical IDs before CloudKit upload
- Fix CanonicalSyncService to use deterministic UUIDs from canonical IDs
instead of random UUIDs from CloudKit records
- Add SyncStadium/SyncTeam/SyncGame types to CloudKitService that preserve
canonical ID relationships during sync
- Add canonical ID field keys to CKModels for reading from CloudKit records
- Bundle canonical JSON files (stadiums_canonical, teams_canonical,
games_canonical, stadium_aliases) for consistent bootstrap data
- Update BootstrapService to prefer canonical format files over legacy format
This ensures all entities use consistent deterministic UUIDs derived from
their canonical IDs, preventing duplicate records when syncing CloudKit
data with bootstrapped local data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CKStadiumAlias model for CloudKit record mapping
- Add fetchStadiumAliases/fetchStadiumAliasChanges to CloudKitService
- Add syncStadiumAliases to CanonicalSyncService for delta sync
- Add subscribeToStadiumAliasUpdates for push notifications
- Update cloudkit_import.py with --stadium-aliases-only option
Data Architecture Updates:
- Remove obsolete provider files (CanonicalDataProvider, CloudKitDataProvider, StubDataProvider)
- AppDataProvider now reads exclusively from SwiftData
- Add background CloudKit sync on app startup (non-blocking)
- Document data architecture in CLAUDE.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>