@@ -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 |
} |
@@ -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. |
@@ -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. |
@@ -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. |
|
@@ -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 |
} |
@@ -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 |
} |
@@ -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) |