diff options
Diffstat (limited to 'DomainDataPortabilityService.swift')
| -rw-r--r-- | DomainDataPortabilityService.swift | 87 |
1 files changed, 86 insertions, 1 deletions
diff --git a/DomainDataPortabilityService.swift b/DomainDataPortabilityService.swift index ab0bc0b..b6f9eee 100644 --- a/DomainDataPortabilityService.swift +++ b/DomainDataPortabilityService.swift @@ -9,6 +9,7 @@ struct DomainDigBackup: Codable { let appVersion: String let trackedDomains: [TrackedDomain] let historyEntries: [HistoryEntry] + let auditSessions: [AuditSession]? let workflows: [DomainWorkflow] let monitoringSettings: MonitoringSettings? let monitoringLogs: [MonitoringLog] @@ -88,6 +89,7 @@ enum DataPortabilityImportKind: String { struct DataLifecycleSummary: Equatable { let trackedDomains: Int let historySnapshots: Int + let auditSessions: Int let workflows: Int let cachedItems: Int let monitoringLogs: Int @@ -352,6 +354,9 @@ enum DataMigrationService { let historyEntries = DomainDataPortabilityService.loadHistoryEntries(defaults: defaults) DomainDataPortabilityService.saveHistoryEntries(historyEntries, defaults: defaults) + let auditSessions = DomainDataPortabilityService.loadAuditSessions(defaults: defaults) + DomainDataPortabilityService.saveAuditSessions(auditSessions, defaults: defaults) + let workflows = DomainDataPortabilityService.loadWorkflows(defaults: defaults) DomainDataPortabilityService.saveWorkflows(workflows, defaults: defaults) @@ -378,6 +383,7 @@ enum DataValidationService { messages.append(contentsOf: validateTrackedDomains(backup.trackedDomains)) messages.append(contentsOf: validateHistoryEntries(backup.historyEntries)) + messages.append(contentsOf: validateAuditSessions(backup.auditSessions ?? [])) messages.append(contentsOf: validateWorkflows(backup.workflows)) if backup.appSettings.resolverURLString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -430,6 +436,23 @@ enum DataValidationService { return messages } + private static func validateAuditSessions(_ auditSessions: [AuditSession]) -> [DataValidationMessage] { + var messages: [DataValidationMessage] = [] + var seen = Set<UUID>() + + for session in auditSessions { + if normalizeDomain(session.domain).isEmpty { + messages.append(.init(text: "An audit session is missing its domain name.", isError: true)) + } + + if !seen.insert(session.id).inserted { + messages.append(.init(text: "Duplicate audit session found for \(session.domain). Merge rules will consolidate it.", isError: false)) + } + } + + return messages + } + private static func validateWorkflows(_ workflows: [DomainWorkflow]) -> [DataValidationMessage] { var messages: [DataValidationMessage] = [] for workflow in workflows { @@ -479,6 +502,7 @@ enum DomainDataPortabilityService { static let trackedDomains = "trackedDomains" static let legacyWatchedDomains = "watchedDomains" static let history = "lookupHistory" + static let audits = "domainAudits" static let workflows = "domainWorkflows" static let monitoringSettings = "monitoring.settings" static let monitoringLogs = "monitoring.logs" @@ -565,6 +589,21 @@ enum DomainDataPortabilityService { } } + static func loadAuditSessions(defaults: UserDefaults = .standard) -> [AuditSession] { + guard let data = defaults.data(forKey: StorageKey.audits), + let sessions = try? JSONDecoder().decode([AuditSession].self, from: data) else { + return [] + } + return sessions.sorted { $0.createdAt > $1.createdAt } + } + + static func saveAuditSessions(_ auditSessions: [AuditSession], defaults: UserDefaults = .standard) { + let sessions = auditSessions.sorted { $0.createdAt > $1.createdAt } + if let data = try? JSONEncoder().encode(sessions) { + defaults.set(data, forKey: StorageKey.audits) + } + } + static func loadWorkflows(defaults: UserDefaults = .standard) -> [DomainWorkflow] { guard let data = defaults.data(forKey: StorageKey.workflows), let workflows = try? JSONDecoder().decode([DomainWorkflow].self, from: data) else { @@ -662,6 +701,7 @@ enum DomainDataPortabilityService { appVersion: AppVersion.current, trackedDomains: loadTrackedDomains(defaults: defaults), historyEntries: loadHistoryEntries(defaults: defaults), + auditSessions: loadAuditSessions(defaults: defaults), workflows: loadWorkflows(defaults: defaults), monitoringSettings: loadMonitoringSettings(defaults: defaults), monitoringLogs: loadMonitoringLogs(defaults: defaults), @@ -750,7 +790,7 @@ enum DomainDataPortabilityService { return DataImportResult( kind: .backup, mode: mode, - summary: "Imported backup with \(backup.trackedDomains.count) tracked domains, \(backup.historyEntries.count) history snapshots, and \(backup.workflows.count) workflows.", + summary: "Imported backup with \(backup.trackedDomains.count) tracked domains, \(backup.historyEntries.count) history snapshots, \(backup.auditSessions?.count ?? 0) audit sessions, and \(backup.workflows.count) workflows.", warnings: report.warnings ) case .trackedDomains(let trackedDomains, let report): @@ -794,6 +834,7 @@ enum DomainDataPortabilityService { return DataLifecycleSummary( trackedDomains: loadTrackedDomains(defaults: defaults).count, historySnapshots: loadHistoryEntries(defaults: defaults).count, + auditSessions: loadAuditSessions(defaults: defaults).count, workflows: loadWorkflows(defaults: defaults).count, cachedItems: cachedItems, monitoringLogs: loadMonitoringLogs(defaults: defaults).count @@ -897,6 +938,7 @@ enum DomainDataPortabilityService { appVersion: AppVersion.current, trackedDomains: legacy.trackedDomains ?? [], historyEntries: legacy.historyEntries ?? [], + auditSessions: nil, workflows: legacy.workflows ?? [], monitoringSettings: legacy.monitoringSettings, monitoringLogs: legacy.monitoringLogs ?? [], @@ -918,6 +960,7 @@ enum DomainDataPortabilityService { if mode == .replace { saveTrackedDomains(deduplicatedTrackedDomains(backup.trackedDomains), defaults: defaults) saveHistoryEntries(deduplicatedHistoryEntries(backup.historyEntries), defaults: defaults) + saveAuditSessions(backup.auditSessions ?? [], defaults: defaults) saveWorkflows(deduplicatedWorkflows(backup.workflows), defaults: defaults) saveMonitoringSettings( MonitoringStorage.sanitizeSettings( @@ -943,6 +986,10 @@ enum DomainDataPortabilityService { defaults: defaults ) + let existingAuditSessions = loadAuditSessions(defaults: defaults) + let mergedAuditSessions = mergeAuditSessions(existing: existingAuditSessions, incoming: backup.auditSessions ?? []) + saveAuditSessions(mergedAuditSessions, defaults: defaults) + let existingWorkflows = loadWorkflows(defaults: defaults) saveWorkflows(mergeWorkflows(existing: existingWorkflows, incoming: backup.workflows), defaults: defaults) @@ -1012,6 +1059,7 @@ enum DomainDataPortabilityService { return DataLifecycleSummary( trackedDomains: backup.trackedDomains.count, historySnapshots: backup.historyEntries.count, + auditSessions: backup.auditSessions?.count ?? 0, workflows: backup.workflows.count, cachedItems: backup.appSettings.recentSearches.count + backup.appSettings.savedDomains.count + ((backup.featureMetadata?.cachedEntitlement == nil ? 0 : 1) + (backup.featureMetadata?.usageCredits == nil ? 0 : 1)), monitoringLogs: backup.monitoringLogs.count @@ -1021,6 +1069,7 @@ enum DomainDataPortabilityService { return DataLifecycleSummary( trackedDomains: mergeTrackedDomains(existing: loadTrackedDomains(defaults: defaults), incoming: backup.trackedDomains).domains.count, historySnapshots: mergeHistoryEntries(existing: loadHistoryEntries(defaults: defaults), incoming: backup.historyEntries).count, + auditSessions: mergeAuditSessions(existing: loadAuditSessions(defaults: defaults), incoming: backup.auditSessions ?? []).count, workflows: mergeWorkflows(existing: loadWorkflows(defaults: defaults), incoming: backup.workflows).count, cachedItems: max(current.cachedItems, backup.appSettings.recentSearches.count + backup.appSettings.savedDomains.count), monitoringLogs: mergeMonitoringLogs(existing: loadMonitoringLogs(defaults: defaults), incoming: backup.monitoringLogs).count @@ -1032,6 +1081,7 @@ enum DomainDataPortabilityService { return DataLifecycleSummary( trackedDomains: total, historySnapshots: current.historySnapshots, + auditSessions: current.auditSessions, workflows: current.workflows, cachedItems: current.cachedItems, monitoringLogs: current.monitoringLogs @@ -1043,6 +1093,7 @@ enum DomainDataPortabilityService { return DataLifecycleSummary( trackedDomains: current.trackedDomains, historySnapshots: current.historySnapshots, + auditSessions: current.auditSessions, workflows: total, cachedItems: current.cachedItems, monitoringLogs: current.monitoringLogs @@ -1132,6 +1183,39 @@ enum DomainDataPortabilityService { } } + private static func mergeAuditSessions(existing: [AuditSession], incoming: [AuditSession]) -> [AuditSession] { + var merged = Dictionary(uniqueKeysWithValues: existing.map { ($0.id, $0) }) + + for session in incoming { + if let existingSession = merged[session.id] { + let winner = existingSession.findings.count >= session.findings.count ? existingSession : session + let candidateNotes = [existingSession.notes, session.notes] + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + let notes = candidateNotes.reduce(into: [String]()) { result, note in + if !result.contains(note) { + result.append(note) + } + }.joined(separator: "\n\n") + merged[session.id] = AuditSession( + id: existingSession.id, + domain: existingSession.domain, + createdAt: min(existingSession.createdAt, session.createdAt), + reviewer: winner.reviewer, + status: winner.status, + evidence: winner.evidence, + findings: winner.findings, + notes: notes, + checklist: winner.checklist + ) + } else { + merged[session.id] = session + } + } + + return merged.values.sorted { $0.createdAt > $1.createdAt } + } + private static func mergeWorkflows(existing: [DomainWorkflow], incoming: [DomainWorkflow]) -> [DomainWorkflow] { var merged = Dictionary(uniqueKeysWithValues: existing.map { ($0.id, normalizedWorkflow($0)) }) @@ -1394,6 +1478,7 @@ private enum ImportPayload { "Full backup import", "\(backup.trackedDomains.count) tracked domains", "\(backup.historyEntries.count) history snapshots", + "\(backup.auditSessions?.count ?? 0) audit sessions", "\(backup.workflows.count) workflows", "\(backup.monitoringLogs.count) monitoring logs", mode == .replace ? "Replace mode will overwrite local backupable data." : "Merge mode will keep local data and consolidate duplicates." |
