fix: comprehensive codebase hardening — crashes, silent failures, performance, and security

Fixes ~95 issues from deep audit across 12 categories in 82 files:

- Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in
  DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test
  bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files
- Silent failure elimination: all 34 try? sites replaced with do/try/catch +
  logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService,
  CanonicalModels, CKModels, SportsTimeApp, and more)
- Performance: cached DateFormatters (7 files), O(1) team lookups via
  AppDataProvider, achievement definition dictionary, AnimatedBackground
  consolidated from 19 Tasks to 1, task cancellation in SharePreviewView
- Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard,
  @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix
- Planning engine: game end time in travel feasibility, state-aware city
  normalization, exact city matching, DrivingConstraints parameter propagation
- IAP: unknown subscription states → expired, unverified transaction logging,
  entitlements updated before paywall dismiss, restore visible to all users
- Security: API key to Info.plist lookup, filename sanitization in PDF export,
  honest User-Agent, removed stale "Feels" analytics super properties
- Navigation: consolidated competing navigationDestination, boolean → value-based
- Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat
- Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote
  fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel
  MKDirections, Sendable-safe POI struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-27 17:03:09 -06:00
parent e046cb6b34
commit c94e373e33
82 changed files with 1163 additions and 599 deletions

View File

@@ -631,11 +631,22 @@ enum AchievementRegistry {
)
]
// MARK: - Lookup Dictionary
private static let definitionsById: [String: AchievementDefinition] = {
Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) })
}()
/// O(1) lookup by typeId
static func definition(for typeId: String) -> AchievementDefinition? {
definitionsById[typeId]
}
// MARK: - Lookup Methods
/// Get achievement by ID
static func achievement(byId id: String) -> AchievementDefinition? {
all.first { $0.id == id }
definitionsById[id]
}
/// Get achievements by category

View File

@@ -81,7 +81,7 @@ struct Game: Identifiable, Codable, Hashable {
}
var gameDate: Date {
Calendar.current.startOfDay(for: dateTime)
dateTime.startOfDay(in: TimeZone(identifier: "UTC"))
}
/// Alias for TripPlanningEngine compatibility

View File

@@ -137,6 +137,18 @@ struct VisitSummary: Identifiable {
let photoCount: Int
let notes: String?
private static let mediumDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
private static let shortDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM d, yyyy"
return f
}()
/// Combined matchup for backwards compatibility
var matchup: String? {
guard let home = homeTeamName, let away = awayTeamName else { return nil }
@@ -144,15 +156,11 @@ struct VisitSummary: Identifiable {
}
var dateDescription: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: visitDate)
Self.mediumDateFormatter.string(from: visitDate)
}
var shortDateDescription: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
return formatter.string(from: visitDate)
Self.shortDateFormatter.string(from: visitDate)
}
}

View File

@@ -95,4 +95,8 @@ extension Stadium: Equatable {
static func == (lhs: Stadium, rhs: Stadium) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@@ -64,4 +64,8 @@ extension Team: Equatable {
static func == (lhs: Team, rhs: Team) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@@ -96,10 +96,14 @@ struct Trip: Identifiable, Codable, Hashable {
var startDate: Date { stops.first?.arrivalDate ?? preferences.startDate }
var endDate: Date { stops.last?.departureDate ?? preferences.endDate }
private static let dateRangeFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMM d, yyyy"
return f
}()
var formattedDateRange: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))"
"\(Self.dateRangeFormatter.string(from: startDate)) - \(Self.dateRangeFormatter.string(from: endDate))"
}
var formattedTotalDistance: String { String(format: "%.0f miles", totalDistanceMiles) }
@@ -191,10 +195,14 @@ struct ItineraryDay: Identifiable, Hashable {
let stops: [TripStop]
let travelSegments: [TravelSegment]
private static let dayFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "EEEE, MMM d"
return f
}()
var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMM d"
return formatter.string(from: date)
Self.dayFormatter.string(from: date)
}
var isRestDay: Bool { stops.first?.isRestDay ?? false }

View File

@@ -63,12 +63,17 @@ struct TripPoll: Identifiable, Codable, Hashable {
// MARK: - Trip Hash
static func computeTripHash(_ trip: Trip) -> String {
var hasher = Hasher()
hasher.combine(trip.stops.map { $0.city })
hasher.combine(trip.stops.flatMap { $0.games })
hasher.combine(trip.preferences.startDate)
hasher.combine(trip.preferences.endDate)
return String(hasher.finalize())
let cities = trip.stops.map { $0.city }.joined(separator: "|")
let games = trip.stops.flatMap { $0.games }.joined(separator: "|")
let start = String(trip.preferences.startDate.timeIntervalSince1970)
let end = String(trip.preferences.endDate.timeIntervalSince1970)
let input = "\(cities);\(games);\(start);\(end)"
// Simple deterministic hash: DJB2
var hash: UInt64 = 5381
for byte in input.utf8 {
hash = hash &* 33 &+ UInt64(byte)
}
return String(hash, radix: 16)
}
// MARK: - Deep Link URL
@@ -87,22 +92,53 @@ struct TripPoll: Identifiable, Codable, Hashable {
struct PollVote: Identifiable, Codable, Hashable {
let id: UUID
let pollId: UUID
let odg: String // voter's userRecordID
let voterId: String // voter's userRecordID
var rankings: [Int] // trip indices in preference order
let votedAt: Date
var modifiedAt: Date
/// Backward-compatible decoding: accepts both "voterId" and legacy "odg" keys
enum CodingKeys: String, CodingKey {
case id, pollId, voterId, rankings, votedAt, modifiedAt
case odg // legacy key
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
pollId = try container.decode(UUID.self, forKey: .pollId)
// Decode from "voterId" first, falling back to legacy "odg"
if let vid = try container.decodeIfPresent(String.self, forKey: .voterId) {
voterId = vid
} else {
voterId = try container.decode(String.self, forKey: .odg)
}
rankings = try container.decode([Int].self, forKey: .rankings)
votedAt = try container.decode(Date.self, forKey: .votedAt)
modifiedAt = try container.decode(Date.self, forKey: .modifiedAt)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(pollId, forKey: .pollId)
try container.encode(voterId, forKey: .voterId)
try container.encode(rankings, forKey: .rankings)
try container.encode(votedAt, forKey: .votedAt)
try container.encode(modifiedAt, forKey: .modifiedAt)
}
init(
id: UUID = UUID(),
pollId: UUID,
odg: String,
voterId: String,
rankings: [Int],
votedAt: Date = Date(),
modifiedAt: Date = Date()
) {
self.id = id
self.pollId = pollId
self.odg = odg
self.voterId = voterId
self.rankings = rankings
self.votedAt = votedAt
self.modifiedAt = modifiedAt