crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 78c8e221f7c5c086398af6b5772e880a8c008528
parent 64a417dffdc59be84f3cb50e00ce7adc326b88e6
Author: Michael Camilleri <[email protected]>
Date:   Mon, 13 Apr 2026 02:29:15 +0900

Add even more iCloud debugging support

Diffstat:
MCrossmate/Sync/SyncEngine.swift | 38++++++++++++++++++++++++++++++++++++++
MCrossmate/Sync/SyncMonitor.swift | 5++++-
MCrossmate/Views/SyncDiagnosticsView.swift | 14++++++++++++++
3 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift @@ -531,6 +531,44 @@ actor SyncEngine { let hasZoneToken: Bool } + /// Runs a sequence of minimal CloudKit operations against the container + /// and returns a list of (probe name, result) pairs for diagnostic display. + /// Each probe runs independently — a failure in one does not stop others. + func probeContainer() async -> [(name: String, result: String)] { + var results: [(String, String)] = [] + + results.append(("containerIdentifier", container.containerIdentifier ?? "nil")) + + do { + let status = try await container.accountStatus() + results.append(("accountStatus", String(describing: status))) + } catch { + results.append(("accountStatus", describe(error))) + } + + do { + let recordID = try await container.userRecordID() + results.append(("userRecordID", recordID.recordName)) + } catch { + results.append(("userRecordID", describe(error))) + } + + do { + let zones = try await privateDatabase.allRecordZones() + let zoneNames = zones.map(\.zoneID.zoneName).joined(separator: ", ") + results.append(("allRecordZones", "\(zones.count) zone(s): [\(zoneNames)]")) + } catch { + results.append(("allRecordZones", describe(error))) + } + + return results + } + + private func describe(_ error: Error) -> String { + let nsError = error as NSError + return "ERROR domain=\(nsError.domain) code=\(nsError.code) \(nsError.localizedDescription)" + } + /// Clears the local sync bookkeeping (zone/subscription created flags and /// change tokens) so the next bootstrap actually hits the network. Used by /// the diagnostics screen to recover from stale flags. Does not touch the diff --git a/Crossmate/Sync/SyncMonitor.swift b/Crossmate/Sync/SyncMonitor.swift @@ -37,7 +37,10 @@ final class SyncMonitor { lastErrorDomain = nsError.domain lastErrorCode = nsError.code lastErrorDescription = nsError.localizedDescription - let message = "\(phase) failed: domain=\(nsError.domain) code=\(nsError.code) \(nsError.localizedDescription)" + let userInfoSummary = nsError.userInfo + .map { "\($0.key)=\($0.value)" } + .joined(separator: " | ") + let message = "\(phase) failed: domain=\(nsError.domain) code=\(nsError.code) \(nsError.localizedDescription) | userInfo: \(userInfoSummary)" append(level: "error", message) } diff --git a/Crossmate/Views/SyncDiagnosticsView.swift b/Crossmate/Views/SyncDiagnosticsView.swift @@ -50,6 +50,11 @@ struct SyncDiagnosticsView: View { } .disabled(isSyncing) + Button("Probe Container") { + Task { await probeContainer() } + } + .disabled(isSyncing) + Button("Reset Sync State", role: .destructive) { Task { await resetSyncState() } } @@ -102,6 +107,15 @@ struct SyncDiagnosticsView: View { syncMonitor.updateSnapshot(snapshot) } + private func probeContainer() async { + syncMonitor.note("Starting container probe") + let results = await syncEngine.probeContainer() + for (name, result) in results { + syncMonitor.note("probe[\(name)]: \(result)") + } + syncMonitor.note("Container probe complete") + } + private func runFullSync() async { guard !isSyncing else { return } isSyncing = true