1→// 2→// DataManager.swift 3→// KortexOS 4→// 5→// Central data management with UserDefaults persistence and 3-layer backup 6→// 7→ 8→import Foundation 9→import SwiftUI 10→import CloudKit 11→import os 12→ 13→@MainActor 14→class DataManager: ObservableObject { 15→ static let shared = DataManager() 16→ 17→ // MARK: - Published Properties 18→ 19→ @Published var rituals: [DailyRitual] = [] 20→ @Published var goals: [LifeGoal] = [] 21→ @Published var journalEntries: [JournalEntry] = [] 22→ @Published var userProgress: UserProgress = UserProgress() 23→ @Published var todaySnapshot: DailySnapshot = DailySnapshot.createForToday() 24→ 25→ // MARK: - Storage Keys 26→ 27→ private struct Keys { 28→ static let rituals = "KortexOS_Rituals" 29→ static let ritualsBackup1 = "KortexOS_Rituals_backup1" 30→ static let ritualsBackup2 = "KortexOS_Rituals_backup2" 31→ 32→ static let goals = "KortexOS_Goals" 33→ static let goalsBackup1 = "KortexOS_Goals_backup1" 34→ static let goalsBackup2 = "KortexOS_Goals_backup2" 35→ 36→ static let journal = "KortexOS_Journal" 37→ static let journalBackup1 = "KortexOS_Journal_backup1" 38→ static let journalBackup2 = "KortexOS_Journal_backup2" 39→ 40→ static let userProgress = "KortexOS_UserProgress" 41→ static let hasLaunchedBefore = "KortexOS_HasLaunchedBefore" 42→ } 43→ 44→ // MARK: - Private Properties 45→ 46→ private let defaults = UserDefaults.standard 47→ private let logger = Logger(subsystem: "com.kortexos", category: "DataManager") 48→ private var saveDebounceTask: Task? 49→ private let saveDebounceDelay: UInt64 = 500_000_000 // 500ms 50→ private let cloudKitManager = CloudKitManager.shared 51→ private var syncDebounceTask: Task? 52→ private let syncDebounceDelay: UInt64 = 1_000_000_000 // 1 second 53→ 54→ // MARK: - Initialization 55→ 56→ private init() { 57→ loadAllData() 58→ setupDailyReset() 59→ setupCloudKitCallbacks() 60→ } 61→ 62→ // MARK: - CloudKit Integration 63→ 64→ private func setupCloudKitCallbacks() { 65→ cloudKitManager.onRecordsReceived = { [weak self] records in 66→ Task { @MainActor [weak self] in 67→ self?.handleReceivedRecords(records) 68→ } 69→ } 70→ 71→ cloudKitManager.onRecordsDeleted = { [weak self] recordIDs in 72→ Task { @MainActor [weak self] in 73→ self?.handleDeletedRecords(recordIDs) 74→ } 75→ } 76→ } 77→ 78→ private func handleReceivedRecords(_ records: [CKRecord]) { 79→ for record in records { 80→ switch record.recordType { 81→ case DailyRitual.recordType: 82→ if let serverRitual = DailyRitual.from(record: record) { 83→ mergeRitual(serverRitual) 84→ } 85→ case LifeGoal.recordType: 86→ if let serverGoal = LifeGoal.from(record: record) { 87→ mergeGoal(serverGoal) 88→ } 89→ case JournalEntry.recordType: 90→ if let serverEntry = JournalEntry.from(record: record) { 91→ mergeJournalEntry(serverEntry) 92→ } 93→ case UserProgress.recordType: 94→ if let serverProgress = UserProgress.from(record: record) { 95→ mergeUserProgress(serverProgress) 96→ } 97→ default: 98→ logger.warning("Unknown record type: \(record.recordType)") 99→ } 100→ } 101→ 102→ // Save locally after merging 103→ saveRituals() 104→ saveGoals() 105→ saveJournalEntries() 106→ saveUserProgress() 107→ updateTodaySnapshot() 108→ 109→ logger.info("Merged \(records.count) records from CloudKit") 110→ } 111→ 112→ private func handleDeletedRecords(_ recordIDs: [CKRecord.ID]) { 113→ for recordID in recordIDs { 114→ let recordName = recordID.recordName 115→ 116→ // Extract UUID from record name (format: "RecordType_UUID") 117→ if let uuidString = recordName.split(separator: "_").last, 118→ let uuid = UUID(uuidString: String(uuidString)) { 119→ 120→ if recordName.hasPrefix(DailyRitual.recordType) { 121→ rituals.removeAll { $0.id == uuid } 122→ } else if recordName.hasPrefix(LifeGoal.recordType) { 123→ goals.removeAll { $0.id == uuid } 124→ } else if recordName.hasPrefix(JournalEntry.recordType) { 125→ journalEntries.removeAll { $0.id == uuid } 126→ } 127→ } 128→ } 129→ 130→ saveRituals() 131→ saveGoals() 132→ saveJournalEntries() 133→ updateTodaySnapshot() 134→ 135→ logger.info("Deleted \(recordIDs.count) records from CloudKit") 136→ } 137→ 138→ // MARK: - Merge Functions 139→ 140→ private func mergeRitual(_ server: DailyRitual) { 141→ if let index = rituals.firstIndex(where: { $0.id == server.id }) { 142→ rituals[index] = rituals[index].merge(with: server) 143→ } else { 144→ rituals.append(server) 145→ } 146→ } 147→ 148→ private func mergeGoal(_ server: LifeGoal) { 149→ if let index = goals.firstIndex(where: { $0.id == server.id }) { 150→ goals[index] = goals[index].merge(with: server) 151→ } else { 152→ goals.append(server) 153→ } 154→ } 155→ 156→ private func mergeJournalEntry(_ server: JournalEntry) { 157→ if let index = journalEntries.firstIndex(where: { $0.id == server.id }) { 158→ journalEntries[index] = journalEntries[index].merge(with: server) 159→ } else { 160→ journalEntries.insert(server, at: 0) 161→ } 162→ } 163→ 164→ private func mergeUserProgress(_ server: UserProgress) { 165→ userProgress = userProgress.merge(with: server) 166→ } 167→ 168→ // MARK: - Sync to CloudKit 169→ 170→ private func syncToCloudKit(_ items: [T]) { 171→ guard cloudKitManager.isSyncEnabled else { return } 172→ 173→ syncDebounceTask?.cancel() 174→ syncDebounceTask = Task { 175→ do { 176→ try await Task.sleep(nanoseconds: syncDebounceDelay) 177→ 178→ let zoneID = CloudKitConfiguration.zoneID 179→ let records = items.map { $0.toRecord(in: zoneID) } 180→ 181→ try await cloudKitManager.pushRecords(records) 182→ logger.debug("Synced \(records.count) \(T.recordType) records to CloudKit") 183→ } catch { 184→ logger.error("Failed to sync to CloudKit: \(error.localizedDescription)") 185→ } 186→ } 187→ } 188→ 189→ private func syncSingleToCloudKit(_ item: T) { 190→ guard cloudKitManager.isSyncEnabled else { return } 191→ 192→ Task { 193→ let record = item.toRecord(in: CloudKitConfiguration.zoneID) 194→ do { 195→ try await cloudKitManager.pushRecords([record]) 196→ logger.debug("Synced single \(T.recordType) to CloudKit") 197→ } catch { 198→ logger.error("Failed to sync single record: \(error.localizedDescription)") 199→ } 200→ } 201→ } 202→ 203→ private func deleteFromCloudKit(_ item: T) { 204→ guard cloudKitManager.isSyncEnabled else { return } 205→ 206→ Task { 207→ let recordID = item.recordID(in: CloudKitConfiguration.zoneID) 208→ do { 209→ try await cloudKitManager.deleteRecords([recordID]) 210→ logger.debug("Deleted \(T.recordType) from CloudKit") 211→ } catch { 212→ logger.error("Failed to delete from CloudKit: \(error.localizedDescription)") 213→ } 214→ } 215→ } 216→ 217→ /// Perform initial sync of all local data to CloudKit 218→ func performInitialSync() async { 219→ guard cloudKitManager.isSyncEnabled else { return } 220→ 221→ logger.info("Starting initial CloudKit sync...") 222→ 223→ // Push all local data 224→ let zoneID = CloudKitConfiguration.zoneID 225→ var allRecords: [CKRecord] = [] 226→ 227→ allRecords.append(contentsOf: rituals.map { $0.toRecord(in: zoneID) }) 228→ allRecords.append(contentsOf: goals.map { $0.toRecord(in: zoneID) }) 229→ allRecords.append(contentsOf: journalEntries.map { $0.toRecord(in: zoneID) }) 230→ allRecords.append(userProgress.toRecord(in: zoneID)) 231→ 232→ do { 233→ try await cloudKitManager.pushRecords(allRecords) 234→ logger.info("Initial sync completed: \(allRecords.count) records") 235→ } catch { 236→ logger.error("Initial sync failed: \(error.localizedDescription)") 237→ } 238→ 239→ // Then pull any changes from other devices 240→ await cloudKitManager.pullChanges() 241→ } 242→ 243→ // MARK: - Load Data 244→ 245→ func loadAllData() { 246→ loadRituals() 247→ loadGoals() 248→ loadJournalEntries() 249→ loadUserProgress() 250→ updateTodaySnapshot() 251→ checkFirstLaunch() 252→ 253→ logger.info("DataManager loaded: \(self.rituals.count) rituals, \(self.goals.count) goals, \(self.journalEntries.count) journal entries") 254→ } 255→ 256→ private func checkFirstLaunch() { 257→ let isFirstLaunch = !defaults.bool(forKey: Keys.hasLaunchedBefore) 258→ let goalsAreEmpty = goals.isEmpty 259→ 260→ // Populate if first launch OR if goals are empty (ensures ZAIROS data is always present) 261→ if isFirstLaunch || goalsAreEmpty { 262→ populateInitialData() 263→ defaults.set(true, forKey: Keys.hasLaunchedBefore) 264→ } 265→ } 266→ 267→ private func populateInitialData() { 268→ // Add sample goals from ZAIROS methodology 269→ goals = LifeGoal.allSampleGoals 270→ saveGoals() 271→ 272→ logger.info("Populated initial ZAIROS data") 273→ } 274→ 275→ // MARK: - Rituals 276→ 277→ private func loadRituals() { 278→ rituals = load(key: Keys.rituals, backup1: Keys.ritualsBackup1, backup2: Keys.ritualsBackup2) ?? [] 279→ ensureTodayRitual() 280→ } 281→ 282→ private func ensureTodayRitual() { 283→ let today = Calendar.current.startOfDay(for: Date()) 284→ if !rituals.contains(where: { DailyRitual.isSameDay($0.date, today) }) { 285→ let newRitual = DailyRitual(date: today) 286→ rituals.insert(newRitual, at: 0) 287→ saveRituals() 288→ } 289→ } 290→ 291→ var todayRitual: DailyRitual? { 292→ get { 293→ rituals.first { DailyRitual.isToday($0.date) } 294→ } 295→ set { 296→ if let newValue = newValue, 297→ let index = rituals.firstIndex(where: { DailyRitual.isToday($0.date) }) { 298→ rituals[index] = newValue 299→ saveRituals() 300→ updateTodaySnapshot() 301→ } 302→ } 303→ } 304→ 305→ func toggleRitualItem(_ itemId: UUID) { 306→ guard var ritual = todayRitual else { return } 307→ ritual.toggleItem(itemId) 308→ todayRitual = ritual 309→ 310→ // Check if ritual is complete for streak 311→ if ritual.isComplete { 312→ userProgress.updateStreak(completedToday: true) 313→ saveUserProgress() 314→ } 315→ } 316→ 317→ func addPushups(_ count: Int) { 318→ guard var ritual = todayRitual else { return } 319→ let previousCount = ritual.pushupCount 320→ ritual.addPushups(count) 321→ todayRitual = ritual 322→ 323→ let added = ritual.pushupCount - previousCount 324→ userProgress.addPushups(added) 325→ saveUserProgress() 326→ } 327→ 328→ func addSquats(_ count: Int) { 329→ guard var ritual = todayRitual else { return } 330→ let previousCount = ritual.squatCount 331→ ritual.addSquats(count) 332→ todayRitual = ritual 333→ 334→ let added = ritual.squatCount - previousCount 335→ userProgress.addSquats(added) 336→ saveUserProgress() 337→ } 338→ 339→ func addMeditationMinutes(_ minutes: Int) { 340→ guard var ritual = todayRitual else { return } 341→ let previousMinutes = ritual.meditationMinutes 342→ ritual.addMeditationMinutes(minutes) 343→ todayRitual = ritual 344→ 345→ let added = ritual.meditationMinutes - previousMinutes 346→ userProgress.addMeditationMinutes(added) 347→ saveUserProgress() 348→ } 349→ 350→ func resetPushups() { 351→ guard var ritual = todayRitual else { return } 352→ ritual.resetPushups() 353→ todayRitual = ritual 354→ } 355→ 356→ func resetSquats() { 357→ guard var ritual = todayRitual else { return } 358→ ritual.resetSquats() 359→ todayRitual = ritual 360→ } 361→ 362→ private func saveRituals() { 363→ save(rituals, key: Keys.rituals, backup1: Keys.ritualsBackup1, backup2: Keys.ritualsBackup2) 364→ 365→ // Sync today's ritual to CloudKit 366→ if let todayRitual = todayRitual { 367→ syncSingleToCloudKit(todayRitual) 368→ } 369→ } 370→ 371→ // MARK: - Goals 372→ 373→ private func loadGoals() { 374→ goals = load(key: Keys.goals, backup1: Keys.goalsBackup1, backup2: Keys.goalsBackup2) ?? [] 375→ } 376→ 377→ func addGoal(_ goal: LifeGoal) { 378→ var newGoal = goal 379→ newGoal.modifiedAt = Date() 380→ goals.append(newGoal) 381→ saveGoals() 382→ syncSingleToCloudKit(newGoal) 383→ } 384→ 385→ func updateGoal(_ goal: LifeGoal) { 386→ if let index = goals.firstIndex(where: { $0.id == goal.id }) { 387→ var updatedGoal = goal 388→ updatedGoal.modifiedAt = Date() 389→ goals[index] = updatedGoal 390→ saveGoals() 391→ updateTodaySnapshot() 392→ syncSingleToCloudKit(updatedGoal) 393→ } 394→ } 395→ 396→ func deleteGoal(_ goal: LifeGoal) { 397→ goals.removeAll { $0.id == goal.id } 398→ // Remove from parent's subGoals 399→ for i in goals.indices { 400→ goals[i].removeSubGoal(goal.id) 401→ } 402→ saveGoals() 403→ deleteFromCloudKit(goal) 404→ } 405→ 406→ func completeGoal(_ goalId: UUID) { 407→ if let index = goals.firstIndex(where: { $0.id == goalId }) { 408→ goals[index].markCompleted() 409→ saveGoals() 410→ updateTodaySnapshot() 411→ } 412→ } 413→ 414→ func goals(for timeframe: GoalTimeframe) -> [LifeGoal] { 415→ goals.filter { $0.timeframe == timeframe } 416→ .sorted { $0.createdAt < $1.createdAt } 417→ } 418→ 419→ func subGoals(for parentId: UUID) -> [LifeGoal] { 420→ goals.filter { $0.parentGoalId == parentId } 421→ } 422→ 423→ private func saveGoals() { 424→ save(goals, key: Keys.goals, backup1: Keys.goalsBackup1, backup2: Keys.goalsBackup2) 425→ } 426→ 427→ // MARK: - Journal Entries 428→ 429→ private func loadJournalEntries() { 430→ journalEntries = load(key: Keys.journal, backup1: Keys.journalBackup1, backup2: Keys.journalBackup2) ?? [] 431→ } 432→ 433→ func addJournalEntry(_ entry: JournalEntry) { 434→ var newEntry = entry 435→ newEntry.modifiedAt = Date() 436→ journalEntries.insert(newEntry, at: 0) 437→ userProgress.addJournalEntry() 438→ saveJournalEntries() 439→ saveUserProgress() 440→ updateTodaySnapshot() 441→ syncSingleToCloudKit(newEntry) 442→ } 443→ 444→ func updateJournalEntry(_ entry: JournalEntry) { 445→ if let index = journalEntries.firstIndex(where: { $0.id == entry.id }) { 446→ var updatedEntry = entry 447→ updatedEntry.modifiedAt = Date() 448→ journalEntries[index] = updatedEntry 449→ saveJournalEntries() 450→ syncSingleToCloudKit(updatedEntry) 451→ } 452→ } 453→ 454→ func deleteJournalEntry(_ entry: JournalEntry) { 455→ journalEntries.removeAll { $0.id == entry.id } 456→ saveJournalEntries() 457→ updateTodaySnapshot() 458→ deleteFromCloudKit(entry) 459→ } 460→ 461→ func journalEntries(for week: Int, year: Int) -> [JournalEntry] { 462→ journalEntries.filter { 463→ let entryWeek = Calendar.current.component(.weekOfYear, from: $0.date) 464→ let entryYear = Calendar.current.component(.year, from: $0.date) 465→ return entryWeek == week && entryYear == year 466→ } 467→ } 468→ 469→ var todayEntries: [JournalEntry] { 470→ journalEntries.filter { $0.isToday } 471→ } 472→ 473→ var thisWeekEntries: [JournalEntry] { 474→ let calendar = Calendar.current 475→ let currentWeek = calendar.component(.weekOfYear, from: Date()) 476→ let currentYear = calendar.component(.year, from: Date()) 477→ return journalEntries(for: currentWeek, year: currentYear) 478→ } 479→ 480→ private func saveJournalEntries() { 481→ save(journalEntries, key: Keys.journal, backup1: Keys.journalBackup1, backup2: Keys.journalBackup2) 482→ } 483→ 484→ // MARK: - User Progress 485→ 486→ private func loadUserProgress() { 487→ if let data = defaults.data(forKey: Keys.userProgress), 488→ let decoded = try? JSONDecoder().decode(UserProgress.self, from: data) { 489→ userProgress = decoded 490→ } 491→ userProgress.checkStreakStatus() 492→ } 493→ 494→ private func saveUserProgress() { 495→ userProgress.modifiedAt = Date() 496→ if let encoded = try? JSONEncoder().encode(userProgress) { 497→ defaults.set(encoded, forKey: Keys.userProgress) 498→ } 499→ syncSingleToCloudKit(userProgress) 500→ } 501→ 502→ // MARK: - Today Snapshot 503→ 504→ func updateTodaySnapshot() { 505→ var snapshot = DailySnapshot.createForToday() 506→ 507→ if let ritual = todayRitual { 508→ snapshot.updateFromRitual(ritual) 509→ } 510→ 511→ snapshot.updateFromJournal(journalEntries) 512→ snapshot.updateFromGoals(goals) 513→ 514→ todaySnapshot = snapshot 515→ } 516→ 517→ // MARK: - Daily Reset 518→ 519→ private func setupDailyReset() { 520→ // Check if we need to create a new ritual for today 521→ let calendar = Calendar.current 522→ let now = Date() 523→ let tomorrow = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: now)!) 524→ let timeUntilMidnight = tomorrow.timeIntervalSince(now) 525→ 526→ Task { 527→ try? await Task.sleep(nanoseconds: UInt64(timeUntilMidnight * 1_000_000_000)) 528→ await MainActor.run { 529→ self.ensureTodayRitual() 530→ self.updateTodaySnapshot() 531→ self.setupDailyReset() 532→ } 533→ } 534→ } 535→ 536→ // MARK: - Generic Save/Load with 3-Layer Backup 537→ 538→ private func save(_ data: T, key: String, backup1: String, backup2: String) { 539→ saveDebounceTask?.cancel() 540→ saveDebounceTask = Task { 541→ do { 542→ try await Task.sleep(nanoseconds: saveDebounceDelay) 543→ 544→ // Rotate backups 545→ if let current = defaults.data(forKey: key) { 546→ if let backup1Data = defaults.data(forKey: backup1) { 547→ defaults.set(backup1Data, forKey: backup2) 548→ } 549→ defaults.set(current, forKey: backup1) 550→ } 551→ 552→ // Save new data 553→ let encoded = try JSONEncoder().encode(data) 554→ defaults.set(encoded, forKey: key) 555→ 556→ logger.debug("Saved data for key: \(key)") 557→ } catch { 558→ logger.error("Failed to save data for key \(key): \(error.localizedDescription)") 559→ } 560→ } 561→ } 562→ 563→ private func load(key: String, backup1: String, backup2: String) -> T? { 564→ // Try primary 565→ if let data = defaults.data(forKey: key), 566→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 567→ return decoded 568→ } 569→ 570→ // Try backup 1 571→ if let data = defaults.data(forKey: backup1), 572→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 573→ logger.warning("Loaded from backup1 for key: \(key)") 574→ return decoded 575→ } 576→ 577→ // Try backup 2 578→ if let data = defaults.data(forKey: backup2), 579→ let decoded = try? JSONDecoder().decode(T.self, from: data) { 580→ logger.warning("Loaded from backup2 for key: \(key)") 581→ return decoded 582→ } 583→ 584→ return nil 585→ } 586→ 587→ // MARK: - Reset Data (for testing) 588→ 589→ func resetAllData() { 590→ rituals = [] 591→ goals = [] 592→ journalEntries = [] 593→ userProgress = UserProgress() 594→ 595→ let keys = [ 596→ Keys.rituals, Keys.ritualsBackup1, Keys.ritualsBackup2, 597→ Keys.goals, Keys.goalsBackup1, Keys.goalsBackup2, 598→ Keys.journal, Keys.journalBackup1, Keys.journalBackup2, 599→ Keys.userProgress, Keys.hasLaunchedBefore 600→ ] 601→ 602→ keys.forEach { defaults.removeObject(forKey: $0) } 603→ 604→ ensureTodayRitual() 605→ populateInitialData() 606→ updateTodaySnapshot() 607→ 608→ logger.info("All data reset") 609→ } 610→} 611→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.