@@ -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 | |
@@ -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, |
@@ -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 |
@@ -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 |
} |