Showing 4 changed files with 50 additions and 1 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, the hot write path now reuses prepared SQLite statements within grouped page writes instead of reparsing the same SQL for every sample, processes sample rows in a lower-allocation streaming loop, batches same-page deleted-object evidence in one transaction, adds composite indexes for visibility-range and sample-uuid hot lookups, and opens SQLite connections with import-friendly busy timeout / synchronous / temp-store pragmas | Continue moving capture/Dashboard actions 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, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view/view-model access, 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; stop returning/storing `HealthSnapshot` bridge handles before removing `ModelContainer` |
31
-| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and Dashboard view/view-model code no longer imports SwiftData or reads `ModelContext`; capture/review actions now route through DTOs and snapshot ids, with the remaining legacy bridge isolated in `HealthKitService`. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; 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 | Stop writing prototype `HealthSnapshot` bridge rows during capture/review |
31
+| UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and Dashboard view/view-model code no longer imports SwiftData or reads `ModelContext`; capture/review actions now route through DTOs and snapshot ids, with the remaining legacy bridge isolated in `HealthKitService`. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; 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; Settings can now schedule a full test-database reset for the next app launch | Stop writing prototype `HealthSnapshot` bridge rows during capture/review |
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; the old direct `HealthSnapshot.typeCounts` diff helper has been retired | Keep active diff/count views on 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 |
+8 -0
HealthProbe/Services/PrototypeStoreResetPolicy.swift
@@ -9,6 +9,14 @@ enum PrototypeStoreResetPolicy {
9 9
     static let currentGeneration = 1
10 10
     static let defaultsKey = "hp_prototypeStoreResetGeneration"
11 11
 
12
+    static func isResetScheduled(defaults: UserDefaults = .standard) -> Bool {
13
+        defaults.integer(forKey: defaultsKey) < currentGeneration
14
+    }
15
+
16
+    static func requestResetOnNextLaunch(defaults: UserDefaults = .standard) {
17
+        defaults.set(max(currentGeneration - 1, 0), forKey: defaultsKey)
18
+    }
19
+
12 20
     static func applyIfNeeded(
13 21
         appSupportURL: URL = .applicationSupportDirectory,
14 22
         defaults: UserDefaults = .standard,
+31 -0
HealthProbe/Views/Settings/SettingsView.swift
@@ -4,10 +4,12 @@ struct SettingsView: View {
4 4
     @Environment(AppSettings.self) private var appSettings
5 5
     @AppStorage("checkFrequencyHours") private var checkFrequencyHours: Int = 6
6 6
     @State private var showDeleteCacheConfirm = false
7
+    @State private var showResetDataConfirm = false
7 8
     @State private var dataMaintenanceMessage: String?
8 9
     @State private var archiveObservationCount: Int?
9 10
     @State private var timeoutProfiles: [LocalMetricTimeoutProfile] = []
10 11
     @State private var currentDeviceProfile: LocalDeviceProfile?
12
+    @State private var resetScheduled = PrototypeStoreResetPolicy.isResetScheduled()
11 13
 
12 14
     private var currentDeviceID: String {
13 15
         AppSettings.currentDeviceID
@@ -40,6 +42,16 @@ struct SettingsView: View {
40 42
             } message: {
41 43
                 Text("This deletes only derived UI/report cache rows. The SQLite archive remains untouched and can rebuild the cache.")
42 44
             }
45
+            .confirmationDialog(
46
+                "Reset Test Databases on Next Launch",
47
+                isPresented: $showResetDataConfirm,
48
+                titleVisibility: .visible
49
+            ) {
50
+                Button("Schedule Reset", role: .destructive) { scheduleTestDataReset() }
51
+                Button("Cancel", role: .cancel) { }
52
+            } message: {
53
+                Text("This schedules deletion of the archive, rebuildable cache, and legacy prototype stores on the next app launch. Force close and reopen HealthProbe after scheduling the reset.")
54
+            }
43 55
         }
44 56
     }
45 57
 
@@ -182,6 +194,18 @@ struct SettingsView: View {
182 194
             }
183 195
             .disabled((archiveObservationCount ?? 0) == 0)
184 196
 
197
+            Button(role: .destructive) {
198
+                showResetDataConfirm = true
199
+            } label: {
200
+                Label("Reset Test Databases on Next Launch", systemImage: "trash.slash")
201
+            }
202
+
203
+            if resetScheduled {
204
+                Text("Reset scheduled. Force close and reopen HealthProbe to recreate the archive and cache from scratch.")
205
+                    .font(.caption)
206
+                    .foregroundStyle(.secondary)
207
+            }
208
+
185 209
             if let dataMaintenanceMessage {
186 210
                 Text(dataMaintenanceMessage)
187 211
                     .font(.caption)
@@ -259,6 +283,7 @@ struct SettingsView: View {
259 283
     }
260 284
 
261 285
     private func loadArchiveCacheStatus() {
286
+        resetScheduled = PrototypeStoreResetPolicy.isResetScheduled()
262 287
         do {
263 288
             archiveObservationCount = try CoreDataArchiveCacheStore().observationCount()
264 289
         } catch {
@@ -288,6 +313,12 @@ struct SettingsView: View {
288 313
             loadArchiveCacheStatus()
289 314
         }
290 315
     }
316
+
317
+    private func scheduleTestDataReset() {
318
+        PrototypeStoreResetPolicy.requestResetOnNextLaunch()
319
+        resetScheduled = true
320
+        dataMaintenanceMessage = "Reset scheduled. Force close and reopen HealthProbe to wipe test databases and start from a clean archive timeline."
321
+    }
291 322
 }
292 323
 
293 324
 // MARK: - Subviews
+10 -0
HealthProbeTests/PrototypeStoreResetPolicyTests.swift
@@ -58,4 +58,14 @@ final class PrototypeStoreResetPolicyTests: XCTestCase {
58 58
         XCTAssertFalse(second.didReset)
59 59
         XCTAssertTrue(second.removedURLs.isEmpty)
60 60
     }
61
+
62
+    func testRequestResetOnNextLaunchMarksResetPending() throws {
63
+        defaults.set(PrototypeStoreResetPolicy.currentGeneration, forKey: PrototypeStoreResetPolicy.defaultsKey)
64
+
65
+        XCTAssertFalse(PrototypeStoreResetPolicy.isResetScheduled(defaults: defaults))
66
+
67
+        PrototypeStoreResetPolicy.requestResetOnNextLaunch(defaults: defaults)
68
+
69
+        XCTAssertTrue(PrototypeStoreResetPolicy.isResetScheduled(defaults: defaults))
70
+    }
61 71
 }