@@ -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 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 | |
|
| 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; 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 | |
@@ -221,7 +221,8 @@ Acceptance: |
||
| 221 | 221 |
|
| 222 | 222 |
Checklist: |
| 223 | 223 |
- [ ] Replace direct SwiftData `@Query` dependencies for target screens. |
| 224 |
-- [x] Dashboard status reads Core Data cached observation rows and cache health, with SwiftData retained for capture review actions. |
|
| 224 |
+- [x] Dashboard status reads Core Data cached observation rows and cache health, |
|
| 225 |
+ with SwiftData retained only for capture/review actions. |
|
| 225 | 226 |
- [x] Observation timeline rows read Core Data cache when available, while retaining SwiftData handles for detail navigation during transition. |
| 226 | 227 |
- [x] Observation detail uses cached summary/type rows plus SQLite diff summaries when archive observation ids exist. |
| 227 | 228 |
- [x] Data Types list rows prefer Core Data cached counts plus SQLite `diffSummary` when archive observation ids exist. |
@@ -108,6 +108,9 @@ The following SwiftData dependencies were removed from active flows: |
||
| 108 | 108 |
- `HealthProbe/Views/Settings/SettingsView.swift` no longer imports SwiftData. |
| 109 | 109 |
Its Data section now reports/rebuilds/deletes the rebuildable Core Data UI |
| 110 | 110 |
cache and leaves the SQLite archive untouched. |
| 111 |
+- `HealthProbe/Views/Dashboard/DashboardView.swift` no longer queries |
|
| 112 |
+ `HealthSnapshot` for status rows; Dashboard status now uses archive/cache rows |
|
| 113 |
+ only. SwiftData remains there for capture/review actions. |
|
| 111 | 114 |
- `HealthProbe/Models/AnomalyRecord.swift`, |
| 112 | 115 |
`HealthProbe/Models/AnomalyType.swift`, and |
| 113 | 116 |
`HealthProbe/Services/AnomalyDetector.swift` were deleted. The app no longer |
@@ -29,7 +29,6 @@ final class DashboardViewModel {
|
||
| 29 | 29 |
var archiveCacheError: String? |
| 30 | 30 |
|
| 31 | 31 |
private let healthKit = HealthKitService.shared |
| 32 |
- private let diffService = SnapshotDiffService.shared |
|
| 33 | 32 |
private var pendingPartialSnapshot: HealthSnapshot? |
| 34 | 33 |
private var pendingAmbiguousSnapshot: HealthSnapshot? |
| 35 | 34 |
|
@@ -351,10 +350,6 @@ final class DashboardViewModel {
|
||
| 351 | 350 |
snapshotProgress = .idle |
| 352 | 351 |
} |
| 353 | 352 |
|
| 354 |
- func totalChanges(latest: HealthSnapshot, previous: HealthSnapshot) -> Int {
|
|
| 355 |
- diffService.totalAbsoluteChange(current: latest, baseline: previous) |
|
| 356 |
- } |
|
| 357 |
- |
|
| 358 | 353 |
func loadArchiveCacheStatus() {
|
| 359 | 354 |
do {
|
| 360 | 355 |
let cache = try CoreDataArchiveCacheStore() |
@@ -6,7 +6,6 @@ import UIKit |
||
| 6 | 6 |
struct DashboardView: View {
|
| 7 | 7 |
@Environment(\.modelContext) private var modelContext |
| 8 | 8 |
@Environment(AppSettings.self) private var appSettings |
| 9 |
- @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var snapshots: [HealthSnapshot] |
|
| 10 | 9 |
@State private var viewModel = DashboardViewModel() |
| 11 | 10 |
@State private var currentDeviceProfile = LocalDeviceProfileStore.profile(for: AppSettings.currentDeviceID) |
| 12 | 11 |
@State private var didAutoRequestPermissions = false |
@@ -16,17 +15,6 @@ struct DashboardView: View {
|
||
| 16 | 15 |
@State private var idleTimerWasDisabledBeforeSnapshot = false |
| 17 | 16 |
@State private var snapshotIdleTimerOverrideActive = false |
| 18 | 17 |
|
| 19 |
- init() {
|
|
| 20 |
- let deviceID = AppSettings.currentDeviceID |
|
| 21 |
- _snapshots = Query( |
|
| 22 |
- filter: #Predicate<HealthSnapshot> { $0.deviceID == deviceID },
|
|
| 23 |
- sort: \HealthSnapshot.timestamp, |
|
| 24 |
- order: .reverse |
|
| 25 |
- ) |
|
| 26 |
- } |
|
| 27 |
- |
|
| 28 |
- private var latest: HealthSnapshot? { snapshots.first }
|
|
| 29 |
- private var previous: HealthSnapshot? { snapshots.dropFirst().first }
|
|
| 30 | 18 |
private var latestArchiveObservation: CachedArchiveObservationRow? {
|
| 31 | 19 |
viewModel.latestArchiveObservation |
| 32 | 20 |
} |
@@ -36,15 +24,9 @@ struct DashboardView: View {
|
||
| 36 | 24 |
} |
| 37 | 25 |
private var currentDeviceDisplayName: String {
|
| 38 | 26 |
if !currentDeviceProfile.name.isEmpty { return currentDeviceProfile.name }
|
| 39 |
- if let latest, !latest.deviceName.isEmpty { return latest.deviceName }
|
|
| 40 | 27 |
return "Local device" |
| 41 | 28 |
} |
| 42 | 29 |
|
| 43 |
- private var latestUnavailableMetricCount: Int {
|
|
| 44 |
- guard let latest else { return 0 }
|
|
| 45 |
- return (latest.typeCounts ?? []).filter { $0.quality == .unauthorized }.count
|
|
| 46 |
- } |
|
| 47 |
- |
|
| 48 | 30 |
private var latestArchiveChangeCount: Int? {
|
| 49 | 31 |
guard let latestArchiveObservation, |
| 50 | 32 |
previousArchiveObservation != nil else {
|
@@ -1151,28 +1133,12 @@ struct DashboardView: View {
|
||
| 1151 | 1133 |
Text("\(latestArchiveObservation.trackedTypeCount)")
|
| 1152 | 1134 |
.foregroundStyle(.secondary) |
| 1153 | 1135 |
} |
| 1154 |
- } else if let latest {
|
|
| 1155 |
- InfoRow(label: "Last Snapshot") {
|
|
| 1156 |
- Text(latest.timestamp, style: .relative) |
|
| 1157 |
- .foregroundStyle(.secondary) |
|
| 1158 |
- } |
|
| 1159 |
- if latest.snapshotQuality != SnapshotQuality.complete {
|
|
| 1160 |
- if latestUnavailableMetricCount > 0 {
|
|
| 1161 |
- Label("\(latestUnavailableMetricCount) metric\(latestUnavailableMetricCount == 1 ? "" : "s") unavailable", systemImage: "exclamationmark.triangle")
|
|
| 1162 |
- .font(.caption) |
|
| 1163 |
- .foregroundStyle(Color.warningAmber) |
|
| 1164 |
- } else {
|
|
| 1165 |
- Label("Incomplete snapshot", systemImage: "exclamationmark.triangle")
|
|
| 1166 |
- .font(.caption) |
|
| 1167 |
- .foregroundStyle(Color.warningAmber) |
|
| 1168 |
- } |
|
| 1169 |
- } |
|
| 1170 | 1136 |
} else {
|
| 1171 |
- Label("No snapshots yet", systemImage: "camera.viewfinder")
|
|
| 1137 |
+ Label("No archive observations yet", systemImage: "camera.viewfinder")
|
|
| 1172 | 1138 |
.foregroundStyle(.secondary) |
| 1173 | 1139 |
} |
| 1174 | 1140 |
|
| 1175 |
- if latest != nil || latestArchiveObservation != nil {
|
|
| 1141 |
+ if latestArchiveObservation != nil {
|
|
| 1176 | 1142 |
InfoRow(label: "Device") {
|
| 1177 | 1143 |
Text(currentDeviceDisplayName) |
| 1178 | 1144 |
.foregroundStyle(.secondary) |
@@ -1219,12 +1185,6 @@ struct DashboardView: View {
|
||
| 1219 | 1185 |
Text(archiveDelta == 0 ? "None" : "\(archiveDelta) records") |
| 1220 | 1186 |
.foregroundStyle(archiveDelta == 0 ? Color.healthyGreen : Color.warningAmber) |
| 1221 | 1187 |
} |
| 1222 |
- } else if let latest, let previous {
|
|
| 1223 |
- let delta = viewModel.totalChanges(latest: latest, previous: previous) |
|
| 1224 |
- InfoRow(label: "Changes vs Previous") {
|
|
| 1225 |
- Text(delta == 0 ? "None" : "\(delta) records") |
|
| 1226 |
- .foregroundStyle(delta == 0 ? Color.healthyGreen : Color.warningAmber) |
|
| 1227 |
- } |
|
| 1228 | 1188 |
} |
| 1229 | 1189 |
} |
| 1230 | 1190 |
} |
@@ -1400,6 +1360,6 @@ extension Bundle {
|
||
| 1400 | 1360 |
|
| 1401 | 1361 |
#Preview {
|
| 1402 | 1362 |
DashboardView() |
| 1403 |
- .modelContainer(for: [HealthSnapshot.self, TypeCount.self, YearlyCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true) |
|
| 1363 |
+ .modelContainer(for: [HealthSnapshot.self], inMemory: true) |
|
| 1404 | 1364 |
.environment(AppSettings()) |
| 1405 | 1365 |
} |