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:
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