Showing 7 changed files with 61 additions and 61 deletions
+0 -2
HealthProbe/ContentView.swift
@@ -1,5 +1,4 @@
1 1
 import SwiftUI
2
-import SwiftData
3 2
 
4 3
 struct ContentView: View {
5 4
     var body: some View {
@@ -22,6 +21,5 @@ struct ContentView: View {
22 21
 
23 22
 #Preview {
24 23
     ContentView()
25
-    .modelContainer(for: [HealthSnapshot.self, SnapshotDelta.self, TypeDelta.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
26 24
         .environment(AppSettings())
27 25
 }
+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, local device profile settings, and operation logging 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 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, local device profile settings, operation logging, ContentView preview, and Settings data maintenance have moved outside SwiftData. 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 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 25 SwiftData-backed files for launch container, capture review actions, navigation handles, some charts, and PDF paths.
52
+- Current UI/cache layers still depend on 23 SwiftData-backed files for launch container, 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.
+4 -3
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -246,9 +246,10 @@ Acceptance:
246 246
 
247 247
 Checklist:
248 248
 - [x] Identify all remaining SwiftData imports.
249
-- [ ] Replace SwiftData models used by active flows. Metric timeout calibration
250
-  local device profile settings, and operation logging have been moved to local
251
-  Codable stores and removed from `ModelContainer`; SwiftData
249
+- [ ] Replace SwiftData models used by active flows. Metric timeout
250
+  calibration, local device profile settings, and operation logging have been
251
+  moved to local Codable stores and removed from `ModelContainer`; Settings
252
+  data maintenance now uses the rebuildable Core Data cache; SwiftData
252 253
   snapshot/navigation handles remain.
253 254
 - [ ] Remove/disable `ModelContainer` as required for target builds.
254 255
 - [x] Add prototype-store ignore/delete/reset path for test installs.
+10 -9
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -9,15 +9,14 @@ local settings stored outside SwiftData where needed.
9 9
 
10 10
 ## Current Count
11 11
 
12
-After moving timeout calibration, local device profile settings, and operation
13
-logging out of SwiftData, 25 app files still have SwiftData imports.
12
+After moving local settings/data-maintenance flows out of SwiftData, 23 app
13
+files still have SwiftData imports.
14 14
 
15 15
 ## Launch Container
16 16
 
17
-These files keep SwiftData required at app launch:
17
+This file keeps SwiftData required at app launch:
18 18
 
19 19
 - `HealthProbe/HealthProbeApp.swift`
20
-- `HealthProbe/ContentView.swift`
21 20
 
22 21
 Retirement path:
23 22
 - replace prototype snapshot model dependencies in tab roots;
@@ -74,7 +73,6 @@ types:
74 73
 - `HealthProbe/Views/DataTypes/DataTypeTemporalDistributionView.swift`
75 74
 - `HealthProbe/Views/DataTypes/DataTypesView.swift`
76 75
 - `HealthProbe/Views/DataTypes/RecordChangeEvolutionChart.swift`
77
-- `HealthProbe/Views/Settings/SettingsView.swift`
78 76
 - `HealthProbe/Views/Snapshots/DataTypeSnapshotDetailView.swift`
79 77
 - `HealthProbe/Views/Snapshots/SnapshotDetailView.swift`
80 78
 - `HealthProbe/Views/Snapshots/SnapshotsView.swift`
@@ -107,10 +105,13 @@ The following SwiftData dependencies were removed from active flows:
107 105
 - Snapshot deletion logging now uses
108 106
   `HealthProbe/Utilities/LocalOperationLog.swift`, a bounded Codable local log
109 107
   outside `ModelContainer`.
108
+- `HealthProbe/ContentView.swift` no longer imports SwiftData; its preview no
109
+  longer creates a legacy model container.
110
+- `HealthProbe/Views/Settings/SettingsView.swift` no longer imports SwiftData.
111
+  Its Data section now reports/rebuilds/deletes the rebuildable Core Data UI
112
+  cache and leaves the SQLite archive untouched.
110 113
 
111 114
 ## Next Recommended Slices
112 115
 
113
-1. Replace `ContentView` preview/container dependency after tab roots stop using
114
-   `@Query`.
115
-2. Move `DashboardView` capture review actions away from `ModelContext`.
116
-3. Replace Snapshots/Data Types navigation handles with archive/cache DTOs.
116
+1. Move `DashboardView` capture review actions away from `ModelContext`.
117
+2. Replace Snapshots/Data Types navigation handles with archive/cache DTOs.
+5 -0
HealthProbe/Services/CoreDataArchiveCacheStore.swift
@@ -164,6 +164,11 @@ final class CoreDataArchiveCacheStore {
164 164
         return try container.viewContext.fetch(request).map(Self.observationRow)
165 165
     }
166 166
 
167
+    func observationCount() throws -> Int {
168
+        let request = NSFetchRequest<NSManagedObject>(entityName: "CachedObservationRow")
169
+        return try container.viewContext.count(for: request)
170
+    }
171
+
167 172
     func latestObservationRow() throws -> CachedArchiveObservationRow? {
168 173
         try observationRows(limit: 1).first
169 174
     }
+38 -45
HealthProbe/Views/Settings/SettingsView.swift
@@ -1,16 +1,11 @@
1 1
 import SwiftUI
2
-import SwiftData
3
-import HealthKit
4
-import UIKit
5 2
 
6 3
 struct SettingsView: View {
7
-    @Environment(\.modelContext) private var modelContext
8 4
     @Environment(AppSettings.self) private var appSettings
9
-    @Query private var snapshots: [HealthSnapshot]
10 5
     @AppStorage("checkFrequencyHours") private var checkFrequencyHours: Int = 6
11
-    @State private var showDeleteConfirm = false
12
-    @State private var showRepairLegacyRecordsConfirm = false
6
+    @State private var showDeleteCacheConfirm = false
13 7
     @State private var dataMaintenanceMessage: String?
8
+    @State private var archiveObservationCount: Int?
14 9
     @State private var timeoutProfiles: [LocalMetricTimeoutProfile] = []
15 10
     @State private var currentDeviceProfile: LocalDeviceProfile?
16 11
 
@@ -33,26 +28,17 @@ struct SettingsView: View {
33 28
             .onAppear {
34 29
                 loadCurrentDeviceProfile()
35 30
                 loadTimeoutProfiles()
31
+                loadArchiveCacheStatus()
36 32
             }
37 33
             .confirmationDialog(
38
-                "Delete All Audit Data",
39
-                isPresented: $showDeleteConfirm,
34
+                "Delete Rebuildable UI Cache",
35
+                isPresented: $showDeleteCacheConfirm,
40 36
                 titleVisibility: .visible
41 37
             ) {
42
-                Button("Delete All Data", role: .destructive) { deleteAllData() }
38
+                Button("Delete UI Cache", role: .destructive) { deleteArchiveCache() }
43 39
                 Button("Cancel", role: .cancel) { }
44 40
             } message: {
45
-                Text("This permanently deletes all \(snapshots.count) snapshots. This action cannot be undone.")
46
-            }
47
-            .confirmationDialog(
48
-                "Repair Legacy Records",
49
-                isPresented: $showRepairLegacyRecordsConfirm,
50
-                titleVisibility: .visible
51
-            ) {
52
-                Button("Delete Legacy Record Rows", role: .destructive) { repairLegacyRecordRows() }
53
-                Button("Cancel", role: .cancel) { }
54
-            } message: {
55
-                Text("This removes old per-record SwiftData rows left by earlier builds. Compact record archives and snapshot counts are preserved.")
41
+                Text("This deletes only derived UI/report cache rows. The SQLite archive remains untouched and can rebuild the cache.")
56 42
             }
57 43
         }
58 44
     }
@@ -178,22 +164,23 @@ struct SettingsView: View {
178 164
 
179 165
     private var dataSection: some View {
180 166
         Section("Data") {
181
-            InfoRow(label: "Stored Snapshots") {
182
-                Text("\(snapshots.count)")
167
+            InfoRow(label: "Cached Observations") {
168
+                Text(archiveObservationCount.map(String.init) ?? "Unavailable")
183 169
                     .foregroundStyle(.secondary)
184 170
             }
185
-            Button(role: .destructive) {
186
-                showDeleteConfirm = true
171
+
172
+            Button {
173
+                rebuildArchiveCache()
187 174
             } label: {
188
-                Label("Delete All Audit Data", systemImage: "trash")
175
+                Label("Rebuild UI Cache", systemImage: "arrow.triangle.2.circlepath")
189 176
             }
190
-            .disabled(snapshots.isEmpty)
191 177
 
192
-            Button {
193
-                showRepairLegacyRecordsConfirm = true
178
+            Button(role: .destructive) {
179
+                showDeleteCacheConfirm = true
194 180
             } label: {
195
-                Label("Repair Legacy Record Rows", systemImage: "wrench.and.screwdriver")
181
+                Label("Delete UI Cache", systemImage: "trash")
196 182
             }
183
+            .disabled((archiveObservationCount ?? 0) == 0)
197 184
 
198 185
             if let dataMaintenanceMessage {
199 186
                 Text(dataMaintenanceMessage)
@@ -271,27 +258,34 @@ struct SettingsView: View {
271 258
         loadTimeoutProfiles()
272 259
     }
273 260
 
274
-    private func deleteAllData() {
261
+    private func loadArchiveCacheStatus() {
262
+        do {
263
+            archiveObservationCount = try CoreDataArchiveCacheStore().observationCount()
264
+        } catch {
265
+            archiveObservationCount = nil
266
+            dataMaintenanceMessage = "Cache status failed: \(error.localizedDescription)"
267
+        }
268
+    }
269
+
270
+    private func rebuildArchiveCache() {
275 271
         do {
276
-            try modelContext.delete(model: HealthRecord.self)
277
-            try modelContext.delete(model: TypeDistributionBin.self)
278
-            try modelContext.delete(model: YearlyCount.self)
279
-            try modelContext.delete(model: TypeCount.self)
280
-            try modelContext.delete(model: HealthSnapshot.self)
281
-            try modelContext.save()
282
-            dataMaintenanceMessage = "Audit data deleted."
272
+            let summary = try CoreDataArchiveCacheStore().rebuild(fromArchiveAt: SQLiteHealthArchiveStore.defaultDatabaseURL)
273
+            archiveObservationCount = summary.observationRows
274
+            dataMaintenanceMessage = "UI cache rebuilt: \(summary.observationRows) observations."
283 275
         } catch {
284
-            dataMaintenanceMessage = "Delete failed: \(error.localizedDescription)"
276
+            dataMaintenanceMessage = "Cache rebuild failed: \(error.localizedDescription)"
277
+            loadArchiveCacheStatus()
285 278
         }
286 279
     }
287 280
 
288
-    private func repairLegacyRecordRows() {
281
+    private func deleteArchiveCache() {
289 282
         do {
290
-            try modelContext.delete(model: HealthRecord.self)
291
-            try modelContext.save()
292
-            dataMaintenanceMessage = "Legacy record rows deleted."
283
+            try CoreDataArchiveCacheStore().deleteCache()
284
+            archiveObservationCount = 0
285
+            dataMaintenanceMessage = "UI cache deleted. Archive data was preserved."
293 286
         } catch {
294
-            dataMaintenanceMessage = "Repair failed: \(error.localizedDescription)"
287
+            dataMaintenanceMessage = "Cache delete failed: \(error.localizedDescription)"
288
+            loadArchiveCacheStatus()
295 289
         }
296 290
     }
297 291
 }
@@ -426,6 +420,5 @@ private func formatDuration(_ seconds: TimeInterval) -> String {
426 420
 
427 421
 #Preview {
428 422
     SettingsView()
429
-        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
430 423
         .environment(AppSettings())
431 424
 }
+2 -0
HealthProbeTests/CoreDataArchiveCacheStoreTests.swift
@@ -42,6 +42,7 @@ final class CoreDataArchiveCacheStoreTests: XCTestCase {
42 42
         let context = cache.container.viewContext
43 43
 
44 44
         XCTAssertEqual(summary.observationRows, 4)
45
+        XCTAssertEqual(try cache.observationCount(), 4)
45 46
         XCTAssertEqual(summary.typeSummaryRows, 4)
46 47
         XCTAssertGreaterThanOrEqual(summary.dailyAggregateRows, 1)
47 48
         XCTAssertEqual(summary.archiveHealthRows, 1)
@@ -90,6 +91,7 @@ final class CoreDataArchiveCacheStoreTests: XCTestCase {
90 91
         _ = try cache.rebuild(fromArchiveAt: archiveURL)
91 92
         try cache.deleteCache()
92 93
 
94
+        XCTAssertEqual(try cache.observationCount(), 0)
93 95
         XCTAssertEqual(try count("CachedObservationRow", in: cache.container.viewContext), 0)
94 96
         let integrityReport = try await archive.checkIntegrity()
95 97
         XCTAssertTrue(integrityReport.passed)