Showing 5 changed files with 9 additions and 50 deletions
+1 -1
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -28,7 +28,7 @@ There are no real deployments, only test installations. Existing prototype datab
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 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, local device profile settings, operation logging, ContentView preview, Settings data maintenance, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; replace capture review actions and navigation handles before removing `ModelContainer` |
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 |
31
+| UI | Prototype exists; Snapshots/Data Types now default to the local device timeline instead of a multi-device picker. Dashboard status reads archive/cache observation rows and shows cache health, with SwiftData retained only for capture/review actions; 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 | SQL diff summaries, paged diff records, aggregate comparisons, and consolidation-evidence labels exist; legacy anomaly/count-drop review has been removed from active flows | Continue moving remaining SwiftData fallback detail paths to archive/cache DTOs |
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 |
34 34
 | Legacy device support | Simplified detail UI mode is implemented for small/accessibility layouts and as a Settings toggle | Remove SwiftData dependency and validate lower deployment targets |
+2 -1
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -221,7 +221,8 @@ Acceptance:
221 221
 
222 222
 Checklist:
223 223
 - [ ] Replace direct SwiftData `@Query` dependencies for target screens.
224
-- [x] Dashboard status reads Core Data cached observation rows and cache health, with SwiftData retained for capture review actions.
224
+- [x] Dashboard status reads Core Data cached observation rows and cache health,
225
+  with SwiftData retained only for capture/review actions.
225 226
 - [x] Observation timeline rows read Core Data cache when available, while retaining SwiftData handles for detail navigation during transition.
226 227
 - [x] Observation detail uses cached summary/type rows plus SQLite diff summaries when archive observation ids exist.
227 228
 - [x] Data Types list rows prefer Core Data cached counts plus SQLite `diffSummary` when archive observation ids exist.
+3 -0
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -108,6 +108,9 @@ The following SwiftData dependencies were removed from active flows:
108 108
 - `HealthProbe/Views/Settings/SettingsView.swift` no longer imports SwiftData.
109 109
   Its Data section now reports/rebuilds/deletes the rebuildable Core Data UI
110 110
   cache and leaves the SQLite archive untouched.
111
+- `HealthProbe/Views/Dashboard/DashboardView.swift` no longer queries
112
+  `HealthSnapshot` for status rows; Dashboard status now uses archive/cache rows
113
+  only. SwiftData remains there for capture/review actions.
111 114
 - `HealthProbe/Models/AnomalyRecord.swift`,
112 115
   `HealthProbe/Models/AnomalyType.swift`, and
113 116
   `HealthProbe/Services/AnomalyDetector.swift` were deleted. The app no longer
+0 -5
HealthProbe/ViewModels/DashboardViewModel.swift
@@ -29,7 +29,6 @@ final class DashboardViewModel {
29 29
     var archiveCacheError: String?
30 30
 
31 31
     private let healthKit = HealthKitService.shared
32
-    private let diffService = SnapshotDiffService.shared
33 32
     private var pendingPartialSnapshot: HealthSnapshot?
34 33
     private var pendingAmbiguousSnapshot: HealthSnapshot?
35 34
 
@@ -351,10 +350,6 @@ final class DashboardViewModel {
351 350
         snapshotProgress = .idle
352 351
     }
353 352
 
354
-    func totalChanges(latest: HealthSnapshot, previous: HealthSnapshot) -> Int {
355
-        diffService.totalAbsoluteChange(current: latest, baseline: previous)
356
-    }
357
-
358 353
     func loadArchiveCacheStatus() {
359 354
         do {
360 355
             let cache = try CoreDataArchiveCacheStore()
+3 -43
HealthProbe/Views/Dashboard/DashboardView.swift
@@ -6,7 +6,6 @@ import UIKit
6 6
 struct DashboardView: View {
7 7
     @Environment(\.modelContext) private var modelContext
8 8
     @Environment(AppSettings.self) private var appSettings
9
-    @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var snapshots: [HealthSnapshot]
10 9
     @State private var viewModel = DashboardViewModel()
11 10
     @State private var currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID)
12 11
     @State private var didAutoRequestPermissions = false
@@ -16,17 +15,6 @@ struct DashboardView: View {
16 15
     @State private var idleTimerWasDisabledBeforeSnapshot = false
17 16
     @State private var snapshotIdleTimerOverrideActive = false
18 17
 
19
-    init() {
20
-        let deviceID = AppSettings.currentDeviceID
21
-        _snapshots = Query(
22
-            filter: #Predicate<HealthSnapshot> { $0.deviceID == deviceID },
23
-            sort: \HealthSnapshot.timestamp,
24
-            order: .reverse
25
-        )
26
-    }
27
-
28
-    private var latest: HealthSnapshot?   { snapshots.first }
29
-    private var previous: HealthSnapshot? { snapshots.dropFirst().first }
30 18
     private var latestArchiveObservation: CachedArchiveObservationRow? {
31 19
         viewModel.latestArchiveObservation
32 20
     }
@@ -36,15 +24,9 @@ struct DashboardView: View {
36 24
     }
37 25
     private var currentDeviceDisplayName: String {
38 26
         if !currentDeviceProfile.name.isEmpty { return currentDeviceProfile.name }
39
-        if let latest, !latest.deviceName.isEmpty { return latest.deviceName }
40 27
         return "Local device"
41 28
     }
42 29
 
43
-    private var latestUnavailableMetricCount: Int {
44
-        guard let latest else { return 0 }
45
-        return (latest.typeCounts ?? []).filter { $0.quality == .unauthorized }.count
46
-    }
47
-
48 30
     private var latestArchiveChangeCount: Int? {
49 31
         guard let latestArchiveObservation,
50 32
               previousArchiveObservation != nil else {
@@ -1151,28 +1133,12 @@ struct DashboardView: View {
1151 1133
                     Text("\(latestArchiveObservation.trackedTypeCount)")
1152 1134
                         .foregroundStyle(.secondary)
1153 1135
                 }
1154
-            } else if let latest {
1155
-                InfoRow(label: "Last Snapshot") {
1156
-                    Text(latest.timestamp, style: .relative)
1157
-                        .foregroundStyle(.secondary)
1158
-                }
1159
-                if latest.snapshotQuality != SnapshotQuality.complete {
1160
-                    if latestUnavailableMetricCount > 0 {
1161
-                        Label("\(latestUnavailableMetricCount) metric\(latestUnavailableMetricCount == 1 ? "" : "s") unavailable", systemImage: "exclamationmark.triangle")
1162
-                            .font(.caption)
1163
-                            .foregroundStyle(Color.warningAmber)
1164
-                    } else {
1165
-                        Label("Incomplete snapshot", systemImage: "exclamationmark.triangle")
1166
-                            .font(.caption)
1167
-                            .foregroundStyle(Color.warningAmber)
1168
-                    }
1169
-                }
1170 1136
             } else {
1171
-                Label("No snapshots yet", systemImage: "camera.viewfinder")
1137
+                Label("No archive observations yet", systemImage: "camera.viewfinder")
1172 1138
                     .foregroundStyle(.secondary)
1173 1139
             }
1174 1140
 
1175
-            if latest != nil || latestArchiveObservation != nil {
1141
+            if latestArchiveObservation != nil {
1176 1142
                 InfoRow(label: "Device") {
1177 1143
                     Text(currentDeviceDisplayName)
1178 1144
                         .foregroundStyle(.secondary)
@@ -1219,12 +1185,6 @@ struct DashboardView: View {
1219 1185
                     Text(archiveDelta == 0 ? "None" : "\(archiveDelta) records")
1220 1186
                         .foregroundStyle(archiveDelta == 0 ? Color.healthyGreen : Color.warningAmber)
1221 1187
                 }
1222
-            } else if let latest, let previous {
1223
-                let delta = viewModel.totalChanges(latest: latest, previous: previous)
1224
-                InfoRow(label: "Changes vs Previous") {
1225
-                    Text(delta == 0 ? "None" : "\(delta) records")
1226
-                        .foregroundStyle(delta == 0 ? Color.healthyGreen : Color.warningAmber)
1227
-                }
1228 1188
             }
1229 1189
         }
1230 1190
     }
@@ -1400,6 +1360,6 @@ extension Bundle {
1400 1360
 
1401 1361
 #Preview {
1402 1362
     DashboardView()
1403
-        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
1363
+        .modelContainer(for: [HealthSnapshot.self], inMemory: true)
1404 1364
         .environment(AppSettings())
1405 1365
 }