Showing 4 changed files with 13 additions and 58 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 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; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; 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 reads archive/cache rows when available and no longer queries `SnapshotDelta` for list summaries; 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; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; 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 |
+3 -1
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -223,7 +223,9 @@ Checklist:
223 223
 - [ ] Replace direct SwiftData `@Query` dependencies for target screens.
224 224
 - [x] Dashboard status reads Core Data cached observation rows and cache health,
225 225
   with SwiftData retained only for capture/review actions.
226
-- [x] Observation timeline rows read Core Data cache when available, while retaining SwiftData handles for detail navigation during transition.
226
+- [x] Observation timeline rows read Core Data cache when available and no
227
+  longer query `SnapshotDelta` list summaries, while retaining SwiftData handles
228
+  for detail navigation during transition.
227 229
 - [x] Observation detail uses cached summary/type rows plus SQLite diff summaries when archive observation ids exist.
228 230
 - [x] Data Types list rows prefer Core Data cached counts plus SQLite `diffSummary` when archive observation ids exist.
229 231
 - [x] Data type detail uses SQLite `diffSummary` when archive observation ids exist.
+4 -0
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -115,6 +115,10 @@ The following SwiftData dependencies were removed from active flows:
115 115
   `HealthProbe/Views/DataTypes/DataTypeTemporalDistributionView.swift` now read
116 116
   a `TemporalDistributionInput` DTO backed by `TypeCountDetailCache`; they no
117 117
   longer import SwiftData, query timeline snapshots, or require `ModelContext`.
118
+- `HealthProbe/Views/Snapshots/SnapshotsView.swift` no longer queries
119
+  `SnapshotDelta` or runs `DeltaService` list-summary repair. Timeline change
120
+  summaries come from archive/cache rows when available; SwiftData remains there
121
+  only for temporary snapshot navigation/deletion handles.
118 122
 - `HealthProbe/Models/AnomalyRecord.swift`,
119 123
   `HealthProbe/Models/AnomalyType.swift`, and
120 124
   `HealthProbe/Services/AnomalyDetector.swift` were deleted. The app no longer
+5 -56
HealthProbe/Views/Snapshots/SnapshotsView.swift
@@ -4,7 +4,6 @@ import SwiftData
4 4
 struct SnapshotsView: View {
5 5
     @Environment(\.modelContext) private var modelContext
6 6
     @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var allSnapshots: [HealthSnapshot]
7
-    @Query private var allDeltas: [SnapshotDelta]
8 7
     @State private var viewModel = SnapshotsViewModel()
9 8
     @State private var profileMap: [String: LocalDeviceProfile] = [:]
10 9
 
@@ -20,16 +19,12 @@ struct SnapshotsView: View {
20 19
     private var timelineReloadID: String {
21 20
         [
22 21
             String(allSnapshots.count),
23
-            allSnapshots.compactMap(\.archiveObservationID).map(String.init).joined(separator: ","),
24
-            String(allDeltas.count)
22
+            allSnapshots.compactMap(\.archiveObservationID).map(String.init).joined(separator: ",")
25 23
         ].joined(separator: "|")
26 24
     }
27 25
 
28 26
     private var snapshotItems: [SnapshotListItem] {
29 27
         let baselines = viewModel.baselines(for: displayedSnapshots)
30
-        let deltaSummaryBySnapshotID = allDeltas.reduce(into: [UUID: SnapshotDeltaListSummary]()) { partial, delta in
31
-            partial[delta.toSnapshotID] = delta.listSummary
32
-        }
33 28
 
34 29
         if let archiveRows = viewModel.archiveRows {
35 30
             let snapshotsByObservationID = Dictionary(uniqueKeysWithValues: displayedSnapshots.compactMap { snapshot in
@@ -42,7 +37,6 @@ struct SnapshotsView: View {
42 37
                     snapshot: snapshot,
43 38
                     baseline: snapshot.flatMap { baselines[$0.id] },
44 39
                     archiveRow: row,
45
-                    deltaSummary: snapshot.flatMap { deltaSummaryBySnapshotID[$0.id] },
46 40
                     showsDeltaSummary: viewModel.comparisonMode == .previous
47 41
                 )
48 42
             }
@@ -53,7 +47,6 @@ struct SnapshotsView: View {
53 47
                 snapshot: snapshot,
54 48
                 baseline: baselines[snapshot.id] ?? nil,
55 49
                 archiveRow: nil,
56
-                deltaSummary: deltaSummaryBySnapshotID[snapshot.id],
57 50
                 showsDeltaSummary: viewModel.comparisonMode == .previous
58 51
             )
59 52
         }
@@ -83,9 +76,6 @@ struct SnapshotsView: View {
83 76
             }
84 77
             .navigationTitle("Snapshots")
85 78
             .toolbar { toolbarContent }
86
-            .task(id: allDeltas.count) {
87
-                repairDeltaListSummariesIfNeeded()
88
-            }
89 79
             .task(id: timelineReloadID) {
90 80
                 loadDeviceProfiles()
91 81
                 await viewModel.loadArchiveRows()
@@ -109,7 +99,6 @@ struct SnapshotsView: View {
109 99
                         snapshot: snapshot,
110 100
                         baseline: item.baseline,
111 101
                         archiveRow: item.archiveRow,
112
-                        deltaSummary: item.deltaSummary,
113 102
                         showsDeltaSummary: item.showsDeltaSummary,
114 103
                         isSelectedBaseline: viewModel.selectedBaseline?.id == snapshot.id,
115 104
                         profile: profileMap[snapshot.deviceID]
@@ -143,7 +132,6 @@ struct SnapshotsView: View {
143 132
                     snapshot: nil,
144 133
                     baseline: item.baseline,
145 134
                     archiveRow: item.archiveRow,
146
-                    deltaSummary: item.deltaSummary,
147 135
                     showsDeltaSummary: item.showsDeltaSummary,
148 136
                     isSelectedBaseline: false,
149 137
                     profile: nil
@@ -174,14 +162,6 @@ struct SnapshotsView: View {
174 162
         }
175 163
     }
176 164
 
177
-    private func repairDeltaListSummariesIfNeeded() {
178
-        do {
179
-            _ = try DeltaService.rebuildMissingListSummaries(context: modelContext, maxCount: 64)
180
-        } catch {
181
-            // Keep the list responsive even if summary repair fails.
182
-        }
183
-    }
184
-
185 165
     private func loadDeviceProfiles() {
186 166
         let profiles = LocalDeviceProfileStore.allProfiles()
187 167
         profileMap = Dictionary(uniqueKeysWithValues: profiles.compactMap {
@@ -194,7 +174,6 @@ private struct SnapshotListItem: Identifiable {
194 174
     let snapshot: HealthSnapshot?
195 175
     let baseline: HealthSnapshot?
196 176
     let archiveRow: CachedArchiveObservationRow?
197
-    let deltaSummary: SnapshotDeltaListSummary?
198 177
     let showsDeltaSummary: Bool
199 178
 
200 179
     var id: String {
@@ -211,7 +190,6 @@ private struct SnapshotRow: View {
211 190
     let snapshot: HealthSnapshot?
212 191
     let baseline: HealthSnapshot?
213 192
     let archiveRow: CachedArchiveObservationRow?
214
-    let deltaSummary: SnapshotDeltaListSummary?
215 193
     let showsDeltaSummary: Bool
216 194
     let isSelectedBaseline: Bool
217 195
     let profile: LocalDeviceProfile?
@@ -271,31 +249,7 @@ private struct SnapshotRow: View {
271 249
             return parts.joined(separator: " • ")
272 250
         }
273 251
 
274
-        guard let deltaSummary else { return nil }
275
-
276
-        var parts: [String] = []
277
-
278
-        if deltaSummary.absoluteRecordChangeCount > 0 {
279
-            parts.append("\(deltaSummary.absoluteRecordChangeCount) record change\(deltaSummary.absoluteRecordChangeCount == 1 ? "" : "s")")
280
-        }
281
-
282
-        if deltaSummary.changedMetricCount > 0 {
283
-            parts.append("\(deltaSummary.changedMetricCount) metric change\(deltaSummary.changedMetricCount == 1 ? "" : "s")")
284
-        }
285
-
286
-        if deltaSummary.appearedMetricCount > 0 {
287
-            parts.append("\(deltaSummary.appearedMetricCount) metric new")
288
-        }
289
-
290
-        if deltaSummary.disappearedMetricCount > 0 {
291
-            parts.append("\(deltaSummary.disappearedMetricCount) metric missing")
292
-        }
293
-
294
-        if parts.isEmpty {
295
-            return "No changes"
296
-        }
297
-
298
-        return parts.joined(separator: " • ")
252
+        return nil
299 253
     }
300 254
 
301 255
     private var deltaSummaryColor: Color {
@@ -305,10 +259,7 @@ private struct SnapshotRow: View {
305 259
             return Color.healthyGreen
306 260
         }
307 261
 
308
-        guard let deltaSummary else { return .secondary }
309
-        if deltaSummary.disappearedMetricCount > 0 { return Color.criticalRed }
310
-        if deltaSummary.hasChanges { return Color.warningAmber }
311
-        return Color.healthyGreen
262
+        return .secondary
312 263
     }
313 264
 
314 265
     private var deltaSummaryIconName: String {
@@ -319,9 +270,7 @@ private struct SnapshotRow: View {
319 270
             return hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle"
320 271
         }
321 272
 
322
-        return deltaSummary == nil || deltaSummary?.hasChanges == false
323
-            ? "checkmark.circle"
324
-            : "arrow.triangle.2.circlepath"
273
+        return "checkmark.circle"
325 274
     }
326 275
 
327 276
     private var hasOSVersionChange: Bool {
@@ -431,6 +380,6 @@ private struct SnapshotRow: View {
431 380
 
432 381
 #Preview {
433 382
     SnapshotsView()
434
-    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
383
+    .modelContainer(for: [HealthSnapshot.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
435 384
         .environment(AppSettings())
436 385
 }