Showing 14 changed files with 142 additions and 66 deletions
+1 -1
HealthProbe/ContentView.swift
@@ -22,6 +22,6 @@ struct ContentView: View {
22 22
 
23 23
 #Preview {
24 24
     ContentView()
25
-    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self, DeviceProfile.self], inMemory: true)
25
+    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
26 26
         .environment(AppSettings())
27 27
 }
+2 -2
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -27,7 +27,7 @@ There are no real deployments, only test installations. Existing prototype datab
27 27
 | HealthKit capture | Capture now opens one archive observation per user-visible snapshot and attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it | Continue moving UI/cache reads to archive-backed observation ids |
28 28
 | SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed | Move Snapshots/Data Types from SwiftData previews to archive/cache DTOs |
29 29
 | Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation |
30
-| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration has moved to a local Codable store outside `ModelContainer`. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; replace remaining Settings/local rows, capture review actions, and navigation handles before removing `ModelContainer` |
30
+| SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration and local device profile settings have moved to Codable stores outside `ModelContainer`. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; replace remaining operation/local rows, capture review actions, and navigation handles before removing `ModelContainer` |
31 31
 | UI | Prototype exists; Snapshots/Data Types now default to the local device timeline instead of a multi-device picker. Dashboard status prefers archive/cache observation rows and shows cache health; Snapshots timeline, snapshot detail summaries/type rows, and Data Types list prefer Core Data cache rows when archive observation ids exist; data type detail reads Core Data type/diff summaries and uses SQLite `diffRecords` for paged drill-down; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language, with SwiftData detail cache as transition fallback | Remove remaining SwiftData navigation handles |
32 32
 | Diff/change explanation | Prototype/legacy anomaly logic exists | Move heavy diffing into SQLite and use neutral change classifications |
33 33
 | Export | SQLite export preview, paged JSON writing, SHA256 manifest hashing, and `export_manifests` rows are in place for selected records and observation diffs | Fill remaining recovery-compatible envelope metadata, CSV export, relationship preservation, and reproducibility checks |
@@ -49,7 +49,7 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
49 49
 - SwiftData currently blocks iOS 15-era device support.
50 50
 - Existing `Anomaly*` model/service names are legacy language.
51 51
 - Some screens still imply snapshot-count monitoring rather than Time Machine inspection.
52
-- Current UI/cache layers still depend on 27 SwiftData-backed files for launch container, remaining local Settings rows, capture review actions, navigation handles, some charts, and PDF paths.
52
+- Current UI/cache layers still depend on 26 SwiftData-backed files for launch container, remaining operation/local rows, capture review actions, navigation handles, some charts, and PDF paths.
53 53
 - Snapshots timeline, snapshot detail summary/type rows, Data Types list rows, and record drill-down are archive/cache-backed for new archive v2 observations when cache rows exist, but navigation still uses SwiftData snapshot handles during the transition.
54 54
 - Legacy SwiftData-only snapshots can show diffs without archive-backed values; they are now reset for archive v2 test installs rather than migrated.
55 55
 - Capture strategy and some legacy SwiftData transition paths may still decode or cache too much data for low-end devices.
+3 -3
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -247,9 +247,9 @@ Acceptance:
247 247
 Checklist:
248 248
 - [x] Identify all remaining SwiftData imports.
249 249
 - [ ] Replace SwiftData models used by active flows. Metric timeout calibration
250
-  has been moved to a local Codable store and removed from `ModelContainer`;
251
-  `DeviceProfile`, `OperationLog`, and SwiftData snapshot/navigation handles
252
-  remain.
250
+  and local device profile settings have been moved to local Codable stores and
251
+  removed from `ModelContainer`; `OperationLog` and SwiftData
252
+  snapshot/navigation handles remain.
253 253
 - [ ] Remove/disable `ModelContainer` as required for target builds.
254 254
 - [x] Add prototype-store ignore/delete/reset path for test installs.
255 255
 - [ ] Verify no old-store compatibility layer remains in active flows.
+13 -10
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -9,8 +9,8 @@ local settings stored outside SwiftData where needed.
9 9
 
10 10
 ## Current Count
11 11
 
12
-After moving timeout calibration out of SwiftData, 27 app files still have
13
-SwiftData imports.
12
+After moving timeout calibration and local device profile settings out of
13
+SwiftData, 26 app files still have SwiftData imports.
14 14
 
15 15
 ## Launch Container
16 16
 
@@ -20,8 +20,8 @@ These files keep SwiftData required at app launch:
20 20
 - `HealthProbe/ContentView.swift`
21 21
 
22 22
 Retirement path:
23
-- move remaining local settings models (`DeviceProfile`, `OperationLog`) to
24
-  non-SwiftData storage or Core Data cache/local store;
23
+- move remaining local settings model (`OperationLog`) to non-SwiftData storage
24
+  or Core Data cache/local store;
25 25
 - replace prototype snapshot model dependencies in tab roots;
26 26
 - remove `.modelContainer(...)` once no active view needs `@Query` or
27 27
   `ModelContext`.
@@ -32,7 +32,6 @@ These files define SwiftData `@Model` classes and are the largest retirement
32 32
 block:
33 33
 
34 34
 - `HealthProbe/Models/AnomalyRecord.swift`
35
-- `HealthProbe/Models/DeviceProfile.swift`
36 35
 - `HealthProbe/Models/HealthRecord.swift`
37 36
 - `HealthProbe/Models/HealthSnapshot.swift`
38 37
 - `HealthProbe/Models/OperationLog.swift`
@@ -47,8 +46,8 @@ Retirement path:
47 46
   `YearlyCount`, `TypeDistributionBin`, and `HealthRecord` active reads with
48 47
   archive/cache DTOs;
49 48
 - replace `AnomalyRecord` flows with neutral change/diff DTOs;
50
-- move `DeviceProfile` and `OperationLog` to a local non-SwiftData store before
51
-  removing the launch container.
49
+- move `OperationLog` to a local non-SwiftData store before removing the launch
50
+  container.
52 51
 
53 52
 ## Capture And Maintenance Services
54 53
 
@@ -89,7 +88,7 @@ Retirement path:
89 88
   queries plus archive ids;
90 89
 - replace detail navigation parameters from SwiftData models to observation/type
91 90
   DTOs;
92
-- move Settings device profile rows to local non-SwiftData storage;
91
+- remove remaining local-only SwiftData rows;
93 92
 - keep paged record drill-down and export paths on archive APIs.
94 93
 
95 94
 ## Removed During This Pass
@@ -104,11 +103,15 @@ The following SwiftData dependencies were removed from active flows:
104 103
   store outside `ModelContainer`.
105 104
 - `SettingsView`, `DashboardView`, and `HealthKitService` read/write timeout
106 105
   calibration through the local store.
106
+- `HealthProbe/Models/DeviceProfile.swift` was deleted.
107
+- Device display name/color settings now use
108
+  `HealthProbe/Utilities/LocalDeviceProfile.swift`, a Codable local store used
109
+  by Settings, Dashboard, Snapshots, and legacy PDF export.
107 110
 
108 111
 ## Next Recommended Slices
109 112
 
110
-1. Replace Settings local rows (`DeviceProfile`) with a small non-SwiftData
111
-   local store so Settings no longer requires `@Query` for local configuration.
113
+1. Move `OperationLog` away from SwiftData, or delete it if no active reporting
114
+   flow still needs it.
112 115
 2. Replace `ContentView` preview/container dependency after tab roots stop using
113 116
    `@Query`.
114 117
 3. Move `DashboardView` capture review actions away from `ModelContext`.
+2 -2
HealthProbe/HealthProbeApp.swift
@@ -33,7 +33,7 @@ struct HealthProbeApp: App {
33 33
         let fullSchema = Schema([
34 34
             HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self,
35 35
             SnapshotDelta.self, TypeDelta.self, AnomalyRecord.self,
36
-            OperationLog.self, DeviceProfile.self,
36
+            OperationLog.self,
37 37
         ])
38 38
 
39 39
         let appSupportURL = URL.applicationSupportDirectory
@@ -44,7 +44,7 @@ struct HealthProbeApp: App {
44 44
             HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self,
45 45
             SnapshotDelta.self, TypeDelta.self, AnomalyRecord.self,
46 46
         ])
47
-        let localModels = Schema([OperationLog.self, DeviceProfile.self])
47
+        let localModels = Schema([OperationLog.self])
48 48
 
49 49
         let uiCacheConfig = ModelConfiguration(
50 50
             "ui-cache",
+0 -12
HealthProbe/Models/DeviceProfile.swift
@@ -1,12 +0,0 @@
1
-import Foundation
2
-import SwiftData
3
-
4
-@Model final class DeviceProfile {
5
-    var deviceID: String = ""
6
-    var name: String = ""
7
-    var colorTag: String = "blue"
8
-
9
-    init(deviceID: String) {
10
-        self.deviceID = deviceID
11
-    }
12
-}
+56 -0
HealthProbe/Utilities/LocalDeviceProfile.swift
@@ -0,0 +1,56 @@
1
+import Foundation
2
+
3
+struct LocalDeviceProfile: Codable, Equatable, Identifiable, Sendable {
4
+    var id: String { deviceID }
5
+    var deviceID: String
6
+    var name: String = ""
7
+    var colorTag: String = "blue"
8
+
9
+    init(deviceID: String, name: String = "", colorTag: String = "blue") {
10
+        self.deviceID = deviceID
11
+        self.name = name
12
+        self.colorTag = colorTag
13
+    }
14
+}
15
+
16
+enum LocalDeviceProfileStore {
17
+    private static let key = "hp_localDeviceProfiles"
18
+
19
+    static func allProfiles() -> [LocalDeviceProfile] {
20
+        load().sorted {
21
+            $0.deviceID.localizedCaseInsensitiveCompare($1.deviceID) == .orderedAscending
22
+        }
23
+    }
24
+
25
+    static func profile(for deviceID: String) -> LocalDeviceProfile {
26
+        guard !deviceID.isEmpty else { return LocalDeviceProfile(deviceID: deviceID) }
27
+        return load().first { $0.deviceID == deviceID } ?? LocalDeviceProfile(deviceID: deviceID)
28
+    }
29
+
30
+    static func save(_ profile: LocalDeviceProfile) {
31
+        guard !profile.deviceID.isEmpty else { return }
32
+        var profiles = load()
33
+        if let index = profiles.firstIndex(where: { $0.deviceID == profile.deviceID }) {
34
+            profiles[index] = profile
35
+        } else {
36
+            profiles.append(profile)
37
+        }
38
+        save(profiles)
39
+    }
40
+
41
+    static func removeAll() {
42
+        UserDefaults.standard.removeObject(forKey: key)
43
+    }
44
+
45
+    private static func load() -> [LocalDeviceProfile] {
46
+        guard let data = UserDefaults.standard.data(forKey: key) else { return [] }
47
+        return (try? JSONDecoder().decode([LocalDeviceProfile].self, from: data)) ?? []
48
+    }
49
+
50
+    private static func save(_ profiles: [LocalDeviceProfile]) {
51
+        let sorted = profiles.sorted {
52
+            $0.deviceID.localizedCaseInsensitiveCompare($1.deviceID) == .orderedAscending
53
+        }
54
+        UserDefaults.standard.set(try? JSONEncoder().encode(sorted), forKey: key)
55
+    }
56
+}
+1 -1
HealthProbe/Utilities/SnapshotPDFExporter.swift
@@ -35,7 +35,7 @@ enum SnapshotPDFExporter {
35 35
     static func extractReportData(
36 36
         snapshot: HealthSnapshot,
37 37
         baseline: HealthSnapshot?,
38
-        profile: DeviceProfile?
38
+        profile: LocalDeviceProfile?
39 39
     ) -> SnapshotReportData {
40 40
         let profileName: String? = {
41 41
             guard let n = profile?.name, !n.isEmpty else { return nil }
+3 -6
HealthProbe/Views/Dashboard/DashboardView.swift
@@ -7,8 +7,8 @@ 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]
11 10
     @State private var viewModel = DashboardViewModel()
11
+    @State private var currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID)
12 12
     @State private var didAutoRequestPermissions = false
13 13
     @State private var snapshotSheetTab: SnapshotSheetTab = .progress
14 14
     @State private var diagnosticReport: DiagnosticReport?
@@ -34,12 +34,8 @@ struct DashboardView: View {
34 34
         guard viewModel.archiveObservationRows.count > 1 else { return nil }
35 35
         return viewModel.archiveObservationRows[1]
36 36
     }
37
-    private var currentDeviceProfile: DeviceProfile? {
38
-        deviceProfiles.first { $0.deviceID == AppSettings.currentDeviceID }
39
-    }
40
-
41 37
     private var currentDeviceDisplayName: String {
42
-        if let name = currentDeviceProfile?.name, !name.isEmpty { return name }
38
+        if !currentDeviceProfile.name.isEmpty { return currentDeviceProfile.name }
43 39
         if let latest, !latest.deviceName.isEmpty { return latest.deviceName }
44 40
         return "Local device"
45 41
     }
@@ -79,6 +75,7 @@ struct DashboardView: View {
79 75
             progressSheet
80 76
         }
81 77
         .task {
78
+            currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID)
82 79
             viewModel.loadArchiveCacheStatus()
83 80
             if !didAutoRequestPermissions && !HealthKitService.shared.hasRequestedPermissionsBefore {
84 81
                 didAutoRequestPermissions = true
+1 -1
HealthProbe/Views/DataTypes/DataTypesView.swift
@@ -234,6 +234,6 @@ private enum DeltaIndicator {
234 234
 
235 235
 #Preview {
236 236
     DataTypesView()
237
-        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self, DeviceProfile.self], inMemory: true)
237
+        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
238 238
         .environment(AppSettings())
239 239
 }
+22 -16
HealthProbe/Views/Settings/SettingsView.swift
@@ -7,21 +7,17 @@ struct SettingsView: View {
7 7
     @Environment(\.modelContext) private var modelContext
8 8
     @Environment(AppSettings.self) private var appSettings
9 9
     @Query private var snapshots: [HealthSnapshot]
10
-    @Query private var deviceProfiles: [DeviceProfile]
11 10
     @AppStorage("checkFrequencyHours") private var checkFrequencyHours: Int = 6
12 11
     @State private var showDeleteConfirm = false
13 12
     @State private var showRepairLegacyRecordsConfirm = false
14 13
     @State private var dataMaintenanceMessage: String?
15 14
     @State private var timeoutProfiles: [LocalMetricTimeoutProfile] = []
15
+    @State private var currentDeviceProfile: LocalDeviceProfile?
16 16
 
17 17
     private var currentDeviceID: String {
18 18
         AppSettings.currentDeviceID
19 19
     }
20 20
 
21
-    private var currentDeviceProfile: DeviceProfile? {
22
-        deviceProfiles.first { $0.deviceID == currentDeviceID }
23
-    }
24
-
25 21
     var body: some View {
26 22
         NavigationStack {
27 23
             List {
@@ -35,7 +31,7 @@ struct SettingsView: View {
35 31
             }
36 32
             .navigationTitle("Settings")
37 33
             .onAppear {
38
-                ensureCurrentDeviceProfile()
34
+                loadCurrentDeviceProfile()
39 35
                 loadTimeoutProfiles()
40 36
             }
41 37
             .confirmationDialog(
@@ -71,9 +67,10 @@ struct SettingsView: View {
71 67
                     Spacer()
72 68
                     TextField("Device name", text: Binding(
73 69
                         get: { profile.name },
74
-                        set: {
75
-                            profile.name = $0
76
-                            try? modelContext.save()
70
+                        set: { newName in
71
+                            updateCurrentDeviceProfile { profile in
72
+                                profile.name = newName
73
+                            }
77 74
                         }
78 75
                     ))
79 76
                     .multilineTextAlignment(.trailing)
@@ -96,8 +93,9 @@ struct SettingsView: View {
96 93
                                 }
97 94
                             }
98 95
                             .onTapGesture {
99
-                                profile.colorTag = dc.rawValue
100
-                                try? modelContext.save()
96
+                                updateCurrentDeviceProfile { profile in
97
+                                    profile.colorTag = dc.rawValue
98
+                                }
101 99
                             }
102 100
                             .accessibilityLabel(dc.rawValue.capitalized)
103 101
                             .accessibilityAddTraits(profile.colorTag == dc.rawValue ? .isSelected : [])
@@ -220,10 +218,18 @@ struct SettingsView: View {
220 218
 
221 219
     // MARK: - Actions
222 220
 
223
-    private func ensureCurrentDeviceProfile() {
224
-        guard currentDeviceProfile == nil, !currentDeviceID.isEmpty else { return }
225
-        modelContext.insert(DeviceProfile(deviceID: currentDeviceID))
226
-        try? modelContext.save()
221
+    private func loadCurrentDeviceProfile() {
222
+        currentDeviceProfile = LocalDeviceProfileStore.profile(for: currentDeviceID)
223
+        if let currentDeviceProfile {
224
+            LocalDeviceProfileStore.save(currentDeviceProfile)
225
+        }
226
+    }
227
+
228
+    private func updateCurrentDeviceProfile(_ mutate: (inout LocalDeviceProfile) -> Void) {
229
+        var profile = currentDeviceProfile ?? LocalDeviceProfileStore.profile(for: currentDeviceID)
230
+        mutate(&profile)
231
+        currentDeviceProfile = profile
232
+        LocalDeviceProfileStore.save(profile)
227 233
     }
228 234
 
229 235
     private func loadTimeoutProfiles() {
@@ -420,6 +426,6 @@ private func formatDuration(_ seconds: TimeInterval) -> String {
420 426
 
421 427
 #Preview {
422 428
     SettingsView()
423
-        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self, DeviceProfile.self], inMemory: true)
429
+        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
424 430
         .environment(AppSettings())
425 431
 }
+3 -3
HealthProbe/Views/Snapshots/SnapshotDetailView.swift
@@ -6,7 +6,7 @@ import UIKit
6 6
 struct SnapshotDetailView: View {
7 7
     let snapshot: HealthSnapshot
8 8
     let baseline: HealthSnapshot?
9
-    let profile: DeviceProfile?
9
+    let profile: LocalDeviceProfile?
10 10
 
11 11
     @Query(sort: \HealthSnapshot.timestamp) private var allSnapshots: [HealthSnapshot]
12 12
     @Query private var allDeltas: [SnapshotDelta]
@@ -1152,8 +1152,8 @@ private struct ShareSheet: UIViewControllerRepresentable {
1152 1152
                 deviceID: "preview-device"
1153 1153
             ),
1154 1154
             baseline: nil,
1155
-            profile: DeviceProfile(deviceID: "preview-device")
1155
+            profile: LocalDeviceProfile(deviceID: "preview-device")
1156 1156
         )
1157 1157
     }
1158
-    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self, DeviceProfile.self], inMemory: true)
1158
+    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
1159 1159
 }
+11 -9
HealthProbe/Views/Snapshots/SnapshotsView.swift
@@ -5,14 +5,8 @@ struct SnapshotsView: View {
5 5
     @Environment(\.modelContext) private var modelContext
6 6
     @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var allSnapshots: [HealthSnapshot]
7 7
     @Query private var allDeltas: [SnapshotDelta]
8
-    @Query private var deviceProfiles: [DeviceProfile]
9 8
     @State private var viewModel = SnapshotsViewModel()
10
-
11
-    private var profileMap: [String: DeviceProfile] {
12
-        Dictionary(uniqueKeysWithValues: deviceProfiles.compactMap {
13
-            $0.deviceID.isEmpty ? nil : ($0.deviceID, $0)
14
-        })
15
-    }
9
+    @State private var profileMap: [String: LocalDeviceProfile] = [:]
16 10
 
17 11
     private var displayedSnapshots: [HealthSnapshot] {
18 12
         guard let deviceID = localDeviceID else { return [] }
@@ -93,6 +87,7 @@ struct SnapshotsView: View {
93 87
                 repairDeltaListSummariesIfNeeded()
94 88
             }
95 89
             .task(id: timelineReloadID) {
90
+                loadDeviceProfiles()
96 91
                 await viewModel.loadArchiveRows()
97 92
             }
98 93
         }
@@ -186,6 +181,13 @@ struct SnapshotsView: View {
186 181
             // Keep the list responsive even if summary repair fails.
187 182
         }
188 183
     }
184
+
185
+    private func loadDeviceProfiles() {
186
+        let profiles = LocalDeviceProfileStore.allProfiles()
187
+        profileMap = Dictionary(uniqueKeysWithValues: profiles.compactMap {
188
+            $0.deviceID.isEmpty ? nil : ($0.deviceID, $0)
189
+        })
190
+    }
189 191
 }
190 192
 
191 193
 private struct SnapshotListItem: Identifiable {
@@ -212,7 +214,7 @@ private struct SnapshotRow: View {
212 214
     let deltaSummary: SnapshotDeltaListSummary?
213 215
     let showsDeltaSummary: Bool
214 216
     let isSelectedBaseline: Bool
215
-    let profile: DeviceProfile?
217
+    let profile: LocalDeviceProfile?
216 218
 
217 219
     private static let dateFormatter: DateFormatter = {
218 220
         let f = DateFormatter()
@@ -435,6 +437,6 @@ private struct SnapshotRow: View {
435 437
 
436 438
 #Preview {
437 439
     SnapshotsView()
438
-    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self, DeviceProfile.self], inMemory: true)
440
+    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
439 441
         .environment(AppSettings())
440 442
 }
+24 -0
HealthProbeTests/LocalDeviceProfileTests.swift
@@ -0,0 +1,24 @@
1
+import XCTest
2
+@testable import HealthProbe
3
+
4
+final class LocalDeviceProfileTests: XCTestCase {
5
+    override func tearDown() {
6
+        LocalDeviceProfileStore.removeAll()
7
+        super.tearDown()
8
+    }
9
+
10
+    func testSavesAndLoadsDeviceProfile() {
11
+        let profile = LocalDeviceProfile(deviceID: "device-a", name: "Test Phone", colorTag: "green")
12
+
13
+        LocalDeviceProfileStore.save(profile)
14
+
15
+        let loaded = LocalDeviceProfileStore.profile(for: "device-a")
16
+        XCTAssertEqual(loaded, profile)
17
+    }
18
+
19
+    func testEmptyDeviceIDIsNotPersisted() {
20
+        LocalDeviceProfileStore.save(LocalDeviceProfile(deviceID: "", name: "Ignored", colorTag: "red"))
21
+
22
+        XCTAssertTrue(LocalDeviceProfileStore.allProfiles().isEmpty)
23
+    }
24
+}