Showing 6 changed files with 40 additions and 39 deletions
+2 -29
HealthProbe/HealthProbeApp.swift
@@ -6,7 +6,6 @@ struct HealthProbeApp: App {
6 6
     @State private var appSettings = AppSettings()
7 7
 
8 8
     var sharedModelContainer: ModelContainer = {
9
-        destroyAllStoresAndRecreate()
10 9
         do {
11 10
             return try createModelContainer()
12 11
         } catch {
@@ -22,44 +21,19 @@ struct HealthProbeApp: App {
22 21
         .modelContainer(sharedModelContainer)
23 22
     }
24 23
 
25
-    // Wipes all SwiftData stores and CoreData-CloudKit caches so that schema-mismatched
26
-    // records are not re-imported from CloudKit after the store is deleted.
27
-    private static func destroyAllStoresAndRecreate() {
28
-        let fm = FileManager.default
29
-        let appSupportURL = URL.applicationSupportDirectory
30
-
31
-        // Remove the entire Application Support directory (store files, WAL, SHM)
32
-        try? fm.removeItem(at: appSupportURL)
33
-        try? fm.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
34
-
35
-        // Also wipe CoreData-CloudKit transaction log metadata so stale remote records
36
-        // are not re-applied to the fresh store on the next launch.
37
-        let ckMetadataURL = appSupportURL.appending(path: "com.apple.coredata.cloudkit", directoryHint: .isDirectory)
38
-        try? fm.removeItem(at: ckMetadataURL)
39
-    }
40
-
41 24
     // Two separate ModelConfiguration instances:
42
-    //   cloudKitConfig — CloudKit-enabled for audit data; OperationLog intentionally excluded
43
-    //   localConfig    — local-only for OperationLog audit trail; never synced
25
+    //   cloudConfig - audit data
26
+    //   localConfig - local-only settings and operation metadata
44 27
     //
45 28
     // ⚠️ DeviceProfile is kept local-only (not synced to CloudKit) since it's device-specific
46 29
     //    cosmetic data (name, color tag) that should not cross devices.
47 30
     private static func createModelContainer() throws -> ModelContainer {
48
-        // TEMPORARY: Use in-memory storage to debug SwiftData schema issues
49
-        // Once working, switch to disk-based storage below
50 31
         let fullSchema = Schema([
51 32
             HealthSnapshot.self, TypeCount.self, YearlyCount.self,
52 33
             SnapshotDelta.self, TypeDelta.self, AnomalyRecord.self,
53 34
             OperationLog.self, DeviceProfile.self, MetricTimeoutProfile.self,
54 35
         ])
55 36
 
56
-        #if DEBUG
57
-        // In-memory for development/testing (no persistence, clears on app relaunch)
58
-        return try ModelContainer(for: fullSchema, configurations: [
59
-            ModelConfiguration(schema: fullSchema, isStoredInMemoryOnly: true)
60
-        ])
61
-        #else
62
-        // Disk-based for production
63 37
         let appSupportURL = URL.applicationSupportDirectory
64 38
         try FileManager.default.createDirectory(at: appSupportURL, withIntermediateDirectories: true)
65 39
 
@@ -96,6 +70,5 @@ struct HealthProbeApp: App {
96 70
             for url in candidates { try? FileManager.default.removeItem(at: url) }
97 71
             return try ModelContainer(for: fullSchema, configurations: [cloudConfig, localConfig])
98 72
         }
99
-        #endif
100 73
     }
101 74
 }
+14 -2
HealthProbe/Utilities/AppSettings.swift
@@ -21,6 +21,10 @@ final class AppSettings {
21 21
         }
22 22
     }
23 23
 
24
+    static var currentDeviceID: String {
25
+        KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id
26
+    }
27
+
24 28
     init() {
25 29
         if let data = UserDefaults.standard.data(forKey: Self.selectedTypeIDsKey),
26 30
            let ids  = try? JSONDecoder().decode([String].self, from: data) {
@@ -31,9 +35,15 @@ final class AppSettings {
31 35
 
32 36
         if let data = UserDefaults.standard.data(forKey: Self.selectedDeviceIDsKey),
33 37
            let ids  = try? JSONDecoder().decode([String].self, from: data) {
34
-            selectedDeviceIDs = Set(ids)
38
+            let storedIDs = Set(ids)
39
+            let oldCurrentID = UIDevice.current.identifierForVendor?.uuidString
40
+            if let oldCurrentID, storedIDs == [oldCurrentID] {
41
+                selectedDeviceIDs = [Self.currentDeviceID]
42
+            } else {
43
+                selectedDeviceIDs = storedIDs
44
+            }
35 45
         } else {
36
-            let currentID = UIDevice.current.identifierForVendor?.uuidString ?? ""
46
+            let currentID = Self.currentDeviceID
37 47
             selectedDeviceIDs = currentID.isEmpty ? [] : [currentID]
38 48
         }
39 49
 
@@ -42,6 +52,8 @@ final class AppSettings {
42 52
         } else {
43 53
             adaptiveTimeoutsEnabled = UserDefaults.standard.bool(forKey: Self.adaptiveTimeoutsEnabledKey)
44 54
         }
55
+
56
+        persistDevices()
45 57
     }
46 58
 
47 59
     func isEnabled(_ type: MonitoredType) -> Bool { selectedTypeIDs.contains(type.id) }
+12 -2
HealthProbe/Views/Dashboard/DashboardView.swift
@@ -7,13 +7,14 @@ struct DashboardView: View {
7 7
     @Environment(\.modelContext) private var modelContext
8 8
     @Environment(AppSettings.self) private var appSettings
9 9
     @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var snapshots: [HealthSnapshot]
10
+    @Query private var deviceProfiles: [DeviceProfile]
10 11
     @State private var viewModel = DashboardViewModel()
11 12
     @State private var didAutoRequestPermissions = false
12 13
     @State private var snapshotSheetTab: SnapshotSheetTab = .progress
13 14
     @State private var expandedIssueIDs: Set<String> = []
14 15
 
15 16
     init() {
16
-        let deviceID = KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id
17
+        let deviceID = AppSettings.currentDeviceID
17 18
         _snapshots = Query(
18 19
             filter: #Predicate<HealthSnapshot> { $0.deviceID == deviceID },
19 20
             sort: \HealthSnapshot.timestamp,
@@ -23,6 +24,15 @@ struct DashboardView: View {
23 24
 
24 25
     private var latest: HealthSnapshot?   { snapshots.first }
25 26
     private var previous: HealthSnapshot? { snapshots.dropFirst().first }
27
+    private var currentDeviceProfile: DeviceProfile? {
28
+        deviceProfiles.first { $0.deviceID == AppSettings.currentDeviceID }
29
+    }
30
+
31
+    private var currentDeviceDisplayName: String {
32
+        if let name = currentDeviceProfile?.name, !name.isEmpty { return name }
33
+        guard let latest, !latest.deviceName.isEmpty else { return "Unknown device" }
34
+        return latest.deviceName
35
+    }
26 36
 
27 37
     var body: some View {
28 38
         NavigationStack {
@@ -901,7 +911,7 @@ private func remediationNoteItems(progress: SnapshotFetchProgress) -> [String] {
901 911
                         .foregroundStyle(.secondary)
902 912
                 }
903 913
                 InfoRow(label: "Device") {
904
-                    Text(latest.deviceName)
914
+                    Text(currentDeviceDisplayName)
905 915
                         .foregroundStyle(.secondary)
906 916
                 }
907 917
                 if latest.snapshotQuality != SnapshotQuality.complete {
+1 -2
HealthProbe/Views/DataTypes/DataTypesView.swift
@@ -1,6 +1,5 @@
1 1
 import SwiftUI
2 2
 import SwiftData
3
-import UIKit
4 3
 
5 4
 struct DataTypesView: View {
6 5
     @Environment(AppSettings.self) private var appSettings
@@ -23,7 +22,7 @@ struct DataTypesView: View {
23 22
     private var latest: HealthSnapshot? { displayedSnapshots.first }
24 23
 
25 24
     private var knownDevices: [DeviceEntry] {
26
-        let currentID = UIDevice.current.identifierForVendor?.uuidString ?? ""
25
+        let currentID = AppSettings.currentDeviceID
27 26
         var ids = Set(allSnapshots.map { $0.deviceID })
28 27
         if !currentID.isEmpty { ids.insert(currentID) }
29 28
         return ids.map { id in
+10 -3
HealthProbe/Views/Settings/SettingsView.swift
@@ -13,7 +13,7 @@ struct SettingsView: View {
13 13
     @State private var showDeleteConfirm = false
14 14
 
15 15
     private var currentDeviceID: String {
16
-        UIDevice.current.identifierForVendor?.uuidString ?? ""
16
+        AppSettings.currentDeviceID
17 17
     }
18 18
 
19 19
     private var currentDeviceProfile: DeviceProfile? {
@@ -58,7 +58,10 @@ struct SettingsView: View {
58 58
                     Spacer()
59 59
                     TextField("Device name", text: Binding(
60 60
                         get: { profile.name },
61
-                        set: { profile.name = $0 }
61
+                        set: {
62
+                            profile.name = $0
63
+                            try? modelContext.save()
64
+                        }
62 65
                     ))
63 66
                     .multilineTextAlignment(.trailing)
64 67
                     .foregroundStyle(.secondary)
@@ -79,7 +82,10 @@ struct SettingsView: View {
79 82
                                         .frame(width: 24, height: 24)
80 83
                                 }
81 84
                             }
82
-                            .onTapGesture { profile.colorTag = dc.rawValue }
85
+                            .onTapGesture {
86
+                                profile.colorTag = dc.rawValue
87
+                                try? modelContext.save()
88
+                            }
83 89
                             .accessibilityLabel(dc.rawValue.capitalized)
84 90
                             .accessibilityAddTraits(profile.colorTag == dc.rawValue ? .isSelected : [])
85 91
                         }
@@ -177,6 +183,7 @@ struct SettingsView: View {
177 183
     private func ensureCurrentDeviceProfile() {
178 184
         guard currentDeviceProfile == nil, !currentDeviceID.isEmpty else { return }
179 185
         modelContext.insert(DeviceProfile(deviceID: currentDeviceID))
186
+        try? modelContext.save()
180 187
     }
181 188
 
182 189
     private func ensureTimeoutProfiles() {
+1 -1
HealthProbe/Views/Snapshots/SnapshotsView.swift
@@ -22,7 +22,7 @@ struct SnapshotsView: View {
22 22
     }
23 23
 
24 24
     private var knownDevices: [DeviceEntry] {
25
-        let currentID = KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id
25
+        let currentID = AppSettings.currentDeviceID
26 26
         var ids = Set(allSnapshots.map { $0.deviceID })
27 27
         ids.insert(currentID)
28 28
         return ids.map { id in