fix(schedule): use start of day for date range queries

- 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>
This commit is contained in:
Trey t
2026-01-20 22:25:44 -06:00
parent 8ea3e6112a
commit 87079b434d
6 changed files with 355 additions and 19 deletions

View File

@@ -317,39 +317,66 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all games. If provided, fetches only games modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchGamesForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncGame] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
log.log("☁️ [CK] Fetching games modified since \(lastSync.formatted())")
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL games (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.game, predicate: predicate)
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) game records from CloudKit")
return records.compactMap { record -> SyncGame? in
var validGames: [SyncGame] = []
var skippedMissingIds = 0
var skippedInvalidGame = 0
for record in records {
let ckGame = CKGame(record: record)
guard let canonicalId = ckGame.canonicalId,
let homeTeamCanonicalId = ckGame.homeTeamCanonicalId,
let awayTeamCanonicalId = ckGame.awayTeamCanonicalId,
let stadiumCanonicalId = ckGame.stadiumCanonicalId
else { return nil }
else {
skippedMissingIds += 1
continue
}
guard let game = ckGame.game(
homeTeamId: homeTeamCanonicalId,
awayTeamId: awayTeamCanonicalId,
stadiumId: stadiumCanonicalId
) else { return nil }
) else {
skippedInvalidGame += 1
continue
}
return SyncGame(
validGames.append(SyncGame(
game: game,
canonicalId: canonicalId,
homeTeamCanonicalId: homeTeamCanonicalId,
awayTeamCanonicalId: awayTeamCanonicalId,
stadiumCanonicalId: stadiumCanonicalId
)
}.sorted { $0.game.dateTime < $1.game.dateTime }
))
}
log.log("☁️ [CK] Parsed \(validGames.count) valid games (skipped: \(skippedMissingIds) missing IDs, \(skippedInvalidGame) invalid)")
// Log sport breakdown
var bySport: [String: Int] = [:]
for g in validGames {
bySport[g.game.sport.rawValue, default: 0] += 1
}
for (sport, count) in bySport.sorted(by: { $0.key < $1.key }) {
log.log("☁️ [CK] \(sport): \(count) games")
}
return validGames.sorted { $0.game.dateTime < $1.game.dateTime }
}
// MARK: - League Structure & Team Aliases