// // DiagnosticsReport.swift // Reference code — copy into your app, adjust the type/file names to taste. // // Adapted from the IndieDiag package (`IndieDiag.swift` + the // `DiagnosticReport` model, commit bc6f475), trimmed to the three // iCloud-documents-relevant pieces (container, document sync, network) — // `cloudKitStatus` and `storageQuota` are gone along with CloudKit. This // file shows how `ContainerDiagnostic`, `DocumentSyncInspector`, and // `NetworkReadiness` compose into the single snapshot a settings // "Diagnostics" screen wants — plus the optional `SchemaSkipScanner` // (a post-package addition, see that file's header) for counting // documents skipped by the schema-version forward gate. // import Foundation /// Coarse overall health, derived from the three individual statuses. public enum DiagnosticHealth: String, Sendable { case healthy = "Healthy" case syncing = "Syncing" case warning = "Warning" case critical = "Critical" } /// Combined snapshot of container, document sync, and network status. public struct DiagnosticsReport: Sendable { public let containerStatus: ICloudContainerStatus public let documentSync: DocumentSyncSnapshot /// Present only when the coordinator was given a `SchemaSkipScanner`. public let schemaSkips: SchemaSkipSnapshot? public let networkStatus: NetworkStatus public let generatedAt: Date /// Note: schema-skipped documents deliberately do NOT degrade health. /// A document written by a newer app version is an expected state, not /// a fault — the data is intact and an app update recovers it. public var overallHealth: DiagnosticHealth { guard containerStatus.isAvailable, networkStatus.isConnected else { return .critical } if documentSync.errorCount > 0 || documentSync.conflictCount > 0 { return .warning } if documentSync.isSyncing { return .syncing } return .healthy } /// Human-readable summary, e.g. for a debug console or support email /// attachment — not for display as primary UI copy. public var summary: String { var lines = ["Diagnostics — \(overallHealth.rawValue)"] lines.append("iCloud: \(containerStatus.isAvailable ? "available" : "unavailable") (account signed in: \(containerStatus.hasSignedInAccount))") lines.append("Network: \(networkStatus.isConnected ? "connected via \(networkStatus.connectionType.rawValue)" : "disconnected")") lines.append("Documents: \(documentSync.currentCount)/\(documentSync.totalCount) current") // Skipped and evicted are paired deliberately: evicted documents // are never probed, so the skipped count is a floor — showing both // numbers side by side lets the reader draw the conclusion. if let schemaSkips, schemaSkips.skippedCount > 0 || documentSync.evictedCount > 0 { lines.append("Skipped (newer schema): \(schemaSkips.skippedCount); evicted (unprobed): \(documentSync.evictedCount)") } else if documentSync.evictedCount > 0 { lines.append("Evicted: \(documentSync.evictedCount)") } if documentSync.errorCount > 0 { lines.append("Errors: \(documentSync.errorCount)") } if documentSync.conflictCount > 0 { lines.append("Conflicts: \(documentSync.conflictCount)") } return lines.joined(separator: "\n") } } /// Coordinates the three diagnostics into a single on-demand snapshot. /// Construct one (it's cheap — no I/O happens until `generate()` runs), /// call `generate()` when a diagnostics screen appears and again on a /// manual "Refresh" action. Don't keep polling it on a timer and don't /// call it from a hot path — see SKILL.md's anti-patterns. @MainActor public final class DiagnosticsCoordinator { private let container: ContainerDiagnostic private let documentInspector = DocumentSyncInspector() private let network = NetworkReadiness() private let schemaScanner: SchemaSkipScanner? /// Pass a `SchemaSkipScanner` to also count documents this build skips /// because their schema version is newer than it understands: /// /// DiagnosticsCoordinator( /// schemaScanner: SchemaSkipScanner( /// currentSchemaVersion: RecordDocument.currentSchema)) public init(containerIdentifier: String? = nil, schemaScanner: SchemaSkipScanner? = nil) { self.container = ContainerDiagnostic(containerIdentifier: containerIdentifier) self.schemaScanner = schemaScanner } /// Runs all checks concurrently and combines them into one report. The /// schema scan (when configured) runs after the document snapshot it /// depends on; container and network checks overlap with both. public func generate() async -> DiagnosticsReport { async let containerStatus = container.checkStatus() async let networkStatus = network.currentStatus() let syncSnapshot = await documentInspector.snapshot() var schemaSkips: SchemaSkipSnapshot? if let schemaScanner { // Probe only documents that certainly have a full local copy — // reading an evicted (or still-downloading) document would // force a download just to inspect one integer. Evicted // documents therefore aren't in the skipped count; the report // already surfaces them separately as `evictedCount`. let locallyPresent: Set = [.current, .uploading, .notUploaded, .conflict] let urls = syncSnapshot.items .filter { locallyPresent.contains($0.syncState) } .map(\.url) schemaSkips = await schemaScanner.scan(urls: urls) } return await DiagnosticsReport( containerStatus: containerStatus, documentSync: syncSnapshot, schemaSkips: schemaSkips, networkStatus: networkStatus, generatedAt: Date() ) } }