fix: 22 audit fixes — concurrency, memory, performance, accessibility

- Move 7 Data(contentsOf:) calls off MainActor via Task.detached (BootstrapService)
- Batch-fetch N+1 queries in sync merge loops (CanonicalSyncService)
- Predicate-based gamesForTeam fetch instead of fetching all games (DataProvider)
- Proper Sendable on RouteInfo with nonisolated(unsafe) polyline (LocationService)
- [weak self] in BGTaskScheduler register closures (BackgroundSyncManager)
- Cache tripDays, routeWaypoints as @State with recompute (TripDetailView)
- Remove unused AnyCancellable, add Task lifecycle management (TripDetailView)
- Cache sportStadiums, recentVisits as stored properties (ProgressViewModel)
- Dynamic Type fonts replacing hardcoded sizes (OnboardingPaywallView)
- Accessibility labels/hints on stadium picker, date picker, map, stats,
  settings toggle, and day cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 09:23:29 -06:00
parent dad3270be7
commit e7420061a5
12 changed files with 180 additions and 101 deletions

View File

@@ -429,11 +429,16 @@ final class CanonicalSyncService {
var skippedIncompatible = 0
var skippedOlder = 0
// Batch-fetch all existing stadiums to avoid N+1 FetchDescriptor lookups
let allExistingStadiums = try context.fetch(FetchDescriptor<CanonicalStadium>())
let existingStadiumsByCanonicalId = Dictionary(grouping: allExistingStadiums, by: \.canonicalId).compactMapValues(\.first)
for syncStadium in syncStadiums {
// Use canonical ID directly from CloudKit - no UUID-based generation!
let result = try mergeStadium(
syncStadium.stadium,
canonicalId: syncStadium.canonicalId,
existingRecord: existingStadiumsByCanonicalId[syncStadium.canonicalId],
context: context
)
@@ -465,6 +470,10 @@ final class CanonicalSyncService {
var skippedIncompatible = 0
var skippedOlder = 0
// Batch-fetch all existing teams to avoid N+1 FetchDescriptor lookups
let allExistingTeams = try context.fetch(FetchDescriptor<CanonicalTeam>())
let existingTeamsByCanonicalId = Dictionary(grouping: allExistingTeams, by: \.canonicalId).compactMapValues(\.first)
for syncTeam in allSyncTeams {
// Use canonical IDs directly from CloudKit - no UUID lookups!
let result = try mergeTeam(
@@ -472,6 +481,7 @@ final class CanonicalSyncService {
canonicalId: syncTeam.canonicalId,
stadiumCanonicalId: syncTeam.stadiumCanonicalId,
validStadiumIds: &validStadiumIds,
existingRecord: existingTeamsByCanonicalId[syncTeam.canonicalId],
context: context
)
@@ -514,6 +524,10 @@ final class CanonicalSyncService {
var skippedIncompatible = 0
var skippedOlder = 0
// Batch-fetch all existing games to avoid N+1 FetchDescriptor lookups
let allExistingGames = try context.fetch(FetchDescriptor<CanonicalGame>())
let existingGamesByCanonicalId = Dictionary(grouping: allExistingGames, by: \.canonicalId).compactMapValues(\.first)
for syncGame in syncGames {
// Use canonical IDs directly from CloudKit - no UUID lookups!
let result = try mergeGame(
@@ -525,6 +539,7 @@ final class CanonicalSyncService {
validTeamIds: validTeamIds,
teamStadiumByTeamId: teamStadiumByTeamId,
validStadiumIds: &validStadiumIds,
existingRecord: existingGamesByCanonicalId[syncGame.canonicalId],
context: context
)
@@ -649,13 +664,19 @@ final class CanonicalSyncService {
private func mergeStadium(
_ remote: Stadium,
canonicalId: String,
existingRecord: CanonicalStadium? = nil,
context: ModelContext
) throws -> MergeResult {
// Look up existing
let descriptor = FetchDescriptor<CanonicalStadium>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
let existing = try context.fetch(descriptor).first
// Use pre-fetched record if available, otherwise fall back to individual fetch
let existing: CanonicalStadium?
if let existingRecord {
existing = existingRecord
} else {
let descriptor = FetchDescriptor<CanonicalStadium>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
existing = try context.fetch(descriptor).first
}
if let existing = existing {
// Preserve user fields
@@ -715,12 +736,19 @@ final class CanonicalSyncService {
canonicalId: String,
stadiumCanonicalId: String?,
validStadiumIds: inout Set<String>,
existingRecord: CanonicalTeam? = nil,
context: ModelContext
) throws -> MergeResult {
let descriptor = FetchDescriptor<CanonicalTeam>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
let existing = try context.fetch(descriptor).first
// Use pre-fetched record if available, otherwise fall back to individual fetch
let existing: CanonicalTeam?
if let existingRecord {
existing = existingRecord
} else {
let descriptor = FetchDescriptor<CanonicalTeam>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
existing = try context.fetch(descriptor).first
}
let remoteStadiumId = stadiumCanonicalId?.trimmingCharacters(in: .whitespacesAndNewlines)
let existingStadiumId = existing?.stadiumCanonicalId.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -809,12 +837,19 @@ final class CanonicalSyncService {
validTeamIds: Set<String>,
teamStadiumByTeamId: [String: String],
validStadiumIds: inout Set<String>,
existingRecord: CanonicalGame? = nil,
context: ModelContext
) throws -> MergeResult {
let descriptor = FetchDescriptor<CanonicalGame>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
let existing = try context.fetch(descriptor).first
// Use pre-fetched record if available, otherwise fall back to individual fetch
let existing: CanonicalGame?
if let existingRecord {
existing = existingRecord
} else {
let descriptor = FetchDescriptor<CanonicalGame>(
predicate: #Predicate { $0.canonicalId == canonicalId }
)
existing = try context.fetch(descriptor).first
}
func resolveTeamId(remote: String, existing: String?) -> String? {
if validTeamIds.contains(remote) {