Showing 6 changed files with 294 additions and 56 deletions
+3 -3
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 | 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 reads archive/cache observation rows and shows cache health, with SwiftData retained only for capture/review actions; Snapshots timeline reads archive/cache rows when available and no longer queries `SnapshotDelta` for list summaries; snapshot detail summaries/type rows require Core Data cache rows and no longer fall back to `SnapshotDelta`/`TypeDelta`; Data Types list rows no longer fall back to SwiftData `TypeCount` traversal; data type detail reads Core Data type/diff summaries, uses SQLite `diffRecords` for paged drill-down, and no longer queries `SnapshotDelta`/`TypeDelta` or rebuilds legacy detail caches from the UI; 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, with already-existing 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 reads archive/cache rows when available and no longer queries `SnapshotDelta` for list summaries; snapshot detail summaries/type rows require Core Data cache rows and no longer fall back to `SnapshotDelta`/`TypeDelta`; Data Types root no longer imports SwiftData and opens archive/cache-backed detail rows; data type detail reads Core Data type/diff summaries, uses SQLite `diffRecords` for paged drill-down, and no longer queries `SnapshotDelta`/`TypeDelta` or rebuilds legacy detail caches from the UI; 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, with already-existing 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; the old direct `HealthSnapshot.typeCounts` diff helper has been retired | 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 |
@@ -48,8 +48,8 @@ Detailed checkable milestones live in [`Refactoring-Plan.md`](Refactoring-Plan.m
48 48
 
49 49
 - SwiftData currently blocks iOS 15-era device support.
50 50
 - Some screens still imply snapshot-count monitoring rather than Time Machine inspection.
51
-- Current UI/cache layers still depend on 19 SwiftData-backed files for launch container, capture review actions, navigation handles, some transition detail paths, and PDF paths.
52
-- 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.
51
+- Current UI/cache layers still depend on 18 SwiftData-backed files for launch container, capture review actions, Snapshots navigation handles, some transition detail paths, and PDF paths.
52
+- Snapshots timeline, snapshot detail summary/type rows, Data Types list/detail rows, and record drill-down are archive/cache-backed for new archive v2 observations when cache rows exist, but Snapshots navigation still uses SwiftData snapshot handles during the transition.
53 53
 - Legacy SwiftData-only snapshots can show diffs without archive-backed values; they are now reset for archive v2 test installs rather than migrated.
54 54
 - Capture strategy and some legacy SwiftData transition paths may still decode or cache too much data for low-end devices.
55 55
 - Old prototype database compatibility is no longer required.
+1 -0
HealthProbe/Doc/04-project/Refactoring-Plan.md
@@ -230,6 +230,7 @@ Checklist:
230 230
   summaries and no longer falls back to legacy `SnapshotDelta`/`TypeDelta`
231 231
   rows.
232 232
 - [x] Data Types list rows use Core Data cached counts plus SQLite `diffSummary` and no longer fall back to SwiftData `TypeCount` traversal.
233
+- [x] Data Types root reads Core Data cached observation rows directly and no longer imports SwiftData.
233 234
 - [x] Data type detail uses Core Data/SQLite `diffSummary` when archive observation ids exist and no longer queries `SnapshotDelta`/`TypeDelta` or rebuilds legacy detail caches from the UI.
234 235
 - [x] Remove unused legacy Type Evolution timeline and direct `HealthSnapshot.typeCounts` diff helper.
235 236
 - [x] Data type new/missing drill-down pages through SQLite `diffRecords` when archive observation ids exist.
+7 -4
HealthProbe/Doc/04-project/SwiftData-Retirement-Inventory.md
@@ -9,9 +9,9 @@ local settings stored outside SwiftData where needed.
9 9
 
10 10
 ## Current Count
11 11
 
12
-After removing the data type detail `SnapshotDelta` query, 19 app files still
13
-have SwiftData imports because active navigation still uses legacy snapshot
14
-handles.
12
+After moving the Data Types root to archive/cache observations, 18 app files
13
+still have SwiftData imports because capture, Snapshots navigation, and legacy
14
+detail transition paths still use prototype snapshot handles.
15 15
 
16 16
 ## Launch Container
17 17
 
@@ -68,7 +68,6 @@ types:
68 68
 
69 69
 - `HealthProbe/ViewModels/DashboardViewModel.swift`
70 70
 - `HealthProbe/Views/Dashboard/DashboardView.swift`
71
-- `HealthProbe/Views/DataTypes/DataTypesView.swift`
72 71
 - `HealthProbe/Views/Snapshots/DataTypeSnapshotDetailView.swift`
73 72
 - `HealthProbe/Views/Snapshots/SnapshotDetailView.swift`
74 73
 - `HealthProbe/Views/Snapshots/SnapshotsView.swift`
@@ -124,6 +123,10 @@ The following SwiftData dependencies were removed from active flows:
124 123
   small observation contexts and builds rows from Core Data cache + SQLite
125 124
   archive diff APIs. It no longer falls back to legacy direct SwiftData
126 125
   `TypeCount` relationship traversal.
126
+- `HealthProbe/Views/DataTypes/DataTypesView.swift` no longer imports
127
+  SwiftData or queries `HealthSnapshot`; it loads Core Data cached observation
128
+  rows and opens `DataTypeArchiveDetailView`, an archive/cache-only detail view
129
+  with paged SQLite new/missing record drill-down.
127 130
 - `HealthProbe/Views/Snapshots/SnapshotDetailView.swift` no longer queries
128 131
   `SnapshotDelta`/`TypeDelta` or carries the old SwiftData type-delta/chart
129 132
   fallback. Snapshot detail type rows now require archive/cache summaries; the
+31 -9
HealthProbe/ViewModels/DataTypesViewModel.swift
@@ -1,18 +1,30 @@
1 1
 import Foundation
2 2
 
3 3
 struct DataTypeSnapshotContext: Equatable, Sendable {
4
-    let id: UUID
4
+    let observationID: Int64
5 5
     let observedAt: Date
6
-    let archiveObservationID: Int64?
7 6
 }
8 7
 
9 8
 @Observable
10 9
 final class DataTypesViewModel {
11 10
     var filter: DiffFilter = .all
12 11
     var comparisonMode: ComparisonMode = .previous
12
+    var observationRows: [CachedArchiveObservationRow] = []
13
+    var observationRowsError: String?
13 14
     var archiveDiffs: [TypeDiff]?
14 15
     var archiveDiffError: String?
15 16
 
17
+    var observationContexts: [DataTypeSnapshotContext] {
18
+        observationRows
19
+            .sorted { $0.observedAt > $1.observedAt }
20
+            .map {
21
+                DataTypeSnapshotContext(
22
+                    observationID: $0.observationID,
23
+                    observedAt: $0.observedAt
24
+                )
25
+            }
26
+    }
27
+
16 28
     func diffs() -> [TypeDiff] {
17 29
         guard let archiveDiffs else { return [] }
18 30
         return apply(filter: filter, to: archiveDiffs)
@@ -27,20 +39,30 @@ final class DataTypesViewModel {
27 39
         archiveDiffError = nil
28 40
     }
29 41
 
42
+    @MainActor
43
+    func loadArchiveRows(limit: Int = 200) async {
44
+        do {
45
+            let cache = try CoreDataArchiveCacheStore()
46
+            observationRows = try cache.observationRows(limit: limit)
47
+            observationRowsError = nil
48
+        } catch {
49
+            observationRows = []
50
+            observationRowsError = error.localizedDescription
51
+        }
52
+    }
53
+
30 54
     @MainActor
31 55
     func loadArchiveDiffs(current: DataTypeSnapshotContext?, snapshots: [DataTypeSnapshotContext]) async {
32 56
         guard let current,
33
-              let baseline = resolveBaseline(for: current, in: snapshots),
34
-              let currentObservationID = current.archiveObservationID,
35
-              let baselineObservationID = baseline.archiveObservationID else {
57
+              let baseline = resolveBaseline(for: current, in: snapshots) else {
36 58
             clearArchiveDiffs()
37 59
             return
38 60
         }
39 61
 
40 62
         do {
41 63
             let cache = try CoreDataArchiveCacheStore()
42
-            let currentSummaries = try cache.typeSummaries(observationID: currentObservationID)
43
-            let baselineSummaries = try cache.typeSummaries(observationID: baselineObservationID)
64
+            let currentSummaries = try cache.typeSummaries(observationID: current.observationID)
65
+            let baselineSummaries = try cache.typeSummaries(observationID: baseline.observationID)
44 66
             let currentByType = Dictionary(uniqueKeysWithValues: currentSummaries.map { ($0.sampleTypeIdentifier, $0) })
45 67
             let baselineByType = Dictionary(uniqueKeysWithValues: baselineSummaries.map { ($0.sampleTypeIdentifier, $0) })
46 68
             let allTypeIdentifiers = Set(currentByType.keys).union(baselineByType.keys)
@@ -52,8 +74,8 @@ final class DataTypesViewModel {
52 74
                 let summary = currentByType[typeIdentifier]
53 75
                 let baselineSummary = baselineByType[typeIdentifier]
54 76
                 let diff = try await SQLiteHealthArchiveStore.shared.diffSummary(HealthArchiveDiffRequest(
55
-                    fromObservationID: baselineObservationID,
56
-                    toObservationID: currentObservationID,
77
+                    fromObservationID: baseline.observationID,
78
+                    toObservationID: current.observationID,
57 79
                     sampleTypeIdentifier: typeIdentifier
58 80
                 ))
59 81
                 archiveRows.append(TypeDiff(
+237 -0
HealthProbe/Views/DataTypes/DataTypeArchiveDetailView.swift
@@ -0,0 +1,237 @@
1
+import SwiftUI
2
+
3
+struct DataTypeArchiveDetailView: View {
4
+    let current: DataTypeSnapshotContext
5
+    let baseline: DataTypeSnapshotContext
6
+    let timeline: [DataTypeSnapshotContext]
7
+    let typeIdentifier: String
8
+    let displayName: String
9
+    let initialDiff: TypeDiff
10
+
11
+    @State private var currentSummary: CachedArchiveTypeSummary?
12
+    @State private var baselineSummary: CachedArchiveTypeSummary?
13
+    @State private var diff: TypeDiff
14
+    @State private var loadError: String?
15
+    @State private var showAddedRecords = false
16
+    @State private var showDisappearedRecords = false
17
+
18
+    private var taskID: String {
19
+        "\(baseline.observationID)|\(current.observationID)|\(typeIdentifier)"
20
+    }
21
+
22
+    private var currentCount: Int {
23
+        currentSummary?.visibleRecordCount ?? diff.currentCount
24
+    }
25
+
26
+    private var baselineCount: Int {
27
+        baselineSummary?.visibleRecordCount ?? diff.previousCount
28
+    }
29
+
30
+    private var evolutionSnapshots: [RecordChangeEvolutionSnapshot] {
31
+        let sorted = timeline.sorted { $0.observedAt < $1.observedAt }
32
+        return sorted.enumerated().map { index, context in
33
+            RecordChangeEvolutionSnapshot(
34
+                id: context.chartID,
35
+                timestamp: context.observedAt,
36
+                localSequenceNumber: index,
37
+                previousSnapshotID: index > 0 ? sorted[index - 1].chartID : nil,
38
+                archiveObservationID: context.observationID,
39
+                count: context.observationID == current.observationID ? currentCount : 0
40
+            )
41
+        }
42
+    }
43
+
44
+    init(
45
+        current: DataTypeSnapshotContext,
46
+        baseline: DataTypeSnapshotContext,
47
+        timeline: [DataTypeSnapshotContext],
48
+        typeIdentifier: String,
49
+        displayName: String,
50
+        initialDiff: TypeDiff
51
+    ) {
52
+        self.current = current
53
+        self.baseline = baseline
54
+        self.timeline = timeline
55
+        self.typeIdentifier = typeIdentifier
56
+        self.displayName = displayName
57
+        self.initialDiff = initialDiff
58
+        _diff = State(initialValue: initialDiff)
59
+    }
60
+
61
+    var body: some View {
62
+        ScrollView {
63
+            VStack(spacing: 16) {
64
+                DataTypeRangeIndicator(
65
+                    earliestDate: currentSummary?.earliestStartDate,
66
+                    latestDate: currentSummary?.latestEndDate,
67
+                    quality: .complete
68
+                )
69
+
70
+                RecordChangeComparisonCard(
71
+                    displayName: displayName,
72
+                    currentCount: currentCount,
73
+                    previousCount: baselineSummary == nil && !diff.previousTracked ? nil : baselineCount,
74
+                    addedCount: diff.appearedCount,
75
+                    disappearedCount: diff.disappearedCount,
76
+                    isCurrentValid: currentCount >= 0,
77
+                    isPreviousTracked: baselineSummary != nil || diff.previousTracked,
78
+                    onAddedTap: {
79
+                        if diff.appearedCount > 0 {
80
+                            showAddedRecords = true
81
+                        }
82
+                    },
83
+                    onDisappearedTap: {
84
+                        if diff.disappearedCount > 0 {
85
+                            showDisappearedRecords = true
86
+                        }
87
+                    }
88
+                )
89
+
90
+                RecordChangeEvolutionChart(
91
+                    snapshots: evolutionSnapshots,
92
+                    currentSnapshotID: current.chartID,
93
+                    typeIdentifier: typeIdentifier,
94
+                    displayName: displayName
95
+                )
96
+
97
+                if let loadError {
98
+                    Label(loadError, systemImage: "exclamationmark.triangle.fill")
99
+                        .font(.caption)
100
+                        .foregroundStyle(Color.warningAmber)
101
+                        .frame(maxWidth: .infinity, alignment: .leading)
102
+                }
103
+            }
104
+            .padding(16)
105
+        }
106
+        .navigationTitle(displayName)
107
+        .navigationBarTitleDisplayMode(.inline)
108
+        .task(id: taskID) {
109
+            await loadArchiveDetail()
110
+        }
111
+        .navigationDestination(isPresented: $showAddedRecords) {
112
+            DataTypeRecordListView(
113
+                title: "New Records",
114
+                displayName: displayName,
115
+                totalCount: diff.appearedCount,
116
+                mode: .addedDiff(
117
+                    typeIdentifier: typeIdentifier,
118
+                    afterDate: baseline.observedAt,
119
+                    beforeDate: current.observedAt,
120
+                    fromObservationID: baseline.observationID,
121
+                    toObservationID: current.observationID
122
+                ),
123
+                tint: Color.healthyGreen
124
+            )
125
+        }
126
+        .navigationDestination(isPresented: $showDisappearedRecords) {
127
+            DataTypeRecordListView(
128
+                title: "Missing Records",
129
+                displayName: displayName,
130
+                totalCount: diff.disappearedCount,
131
+                mode: .disappearedDiff(
132
+                    typeIdentifier: typeIdentifier,
133
+                    fromObservationID: baseline.observationID,
134
+                    toObservationID: current.observationID
135
+                ),
136
+                tint: Color.criticalRed
137
+            )
138
+        }
139
+    }
140
+
141
+    @MainActor
142
+    private func loadArchiveDetail() async {
143
+        do {
144
+            let cache = try CoreDataArchiveCacheStore()
145
+            currentSummary = try cache.typeSummaries(observationID: current.observationID)
146
+                .first { $0.sampleTypeIdentifier == typeIdentifier }
147
+            baselineSummary = try cache.typeSummaries(observationID: baseline.observationID)
148
+                .first { $0.sampleTypeIdentifier == typeIdentifier }
149
+
150
+            if let cachedDiff = try cache.diffSummary(
151
+                fromObservationID: baseline.observationID,
152
+                toObservationID: current.observationID,
153
+                sampleTypeIdentifier: typeIdentifier
154
+            ) {
155
+                diff = TypeDiff(
156
+                    id: typeIdentifier,
157
+                    typeIdentifier: typeIdentifier,
158
+                    displayName: currentSummary?.displayName ?? baselineSummary?.displayName ?? displayName,
159
+                    currentCount: currentSummary?.visibleRecordCount ?? 0,
160
+                    previousCount: baselineSummary?.visibleRecordCount ?? 0,
161
+                    previousTracked: baselineSummary != nil,
162
+                    appearedCount: cachedDiff.appearedCount,
163
+                    disappearedCount: cachedDiff.disappearedCount,
164
+                    representationChangedCount: cachedDiff.representationChangedCount
165
+                )
166
+                loadError = nil
167
+                return
168
+            }
169
+
170
+            let archiveDiff = try await SQLiteHealthArchiveStore.shared.diffSummary(HealthArchiveDiffRequest(
171
+                fromObservationID: baseline.observationID,
172
+                toObservationID: current.observationID,
173
+                sampleTypeIdentifier: typeIdentifier
174
+            ))
175
+            diff = TypeDiff(
176
+                id: typeIdentifier,
177
+                typeIdentifier: typeIdentifier,
178
+                displayName: currentSummary?.displayName ?? baselineSummary?.displayName ?? displayName,
179
+                currentCount: currentSummary?.visibleRecordCount ?? 0,
180
+                previousCount: baselineSummary?.visibleRecordCount ?? 0,
181
+                previousTracked: baselineSummary != nil,
182
+                appearedCount: archiveDiff.appearedCount,
183
+                disappearedCount: archiveDiff.disappearedCount,
184
+                representationChangedCount: archiveDiff.representationChangedCount
185
+            )
186
+            loadError = nil
187
+        } catch {
188
+            loadError = error.localizedDescription
189
+        }
190
+    }
191
+}
192
+
193
+private extension DataTypeSnapshotContext {
194
+    var chartID: UUID {
195
+        let masked = UInt64(bitPattern: observationID) & 0xffff_ffff_ffff
196
+        let suffix = String(format: "%012llx", masked)
197
+        return UUID(uuidString: "00000000-0000-0000-0000-\(suffix)") ?? UUID()
198
+    }
199
+}
200
+
201
+#Preview {
202
+    NavigationStack {
203
+        DataTypeArchiveDetailView(
204
+            current: DataTypeSnapshotContext(
205
+                observationID: 2,
206
+                observedAt: Date.now
207
+            ),
208
+            baseline: DataTypeSnapshotContext(
209
+                observationID: 1,
210
+                observedAt: Date.now.addingTimeInterval(-86_400)
211
+            ),
212
+            timeline: [
213
+                DataTypeSnapshotContext(
214
+                    observationID: 1,
215
+                    observedAt: Date.now.addingTimeInterval(-86_400)
216
+                ),
217
+                DataTypeSnapshotContext(
218
+                    observationID: 2,
219
+                    observedAt: Date.now
220
+                )
221
+            ],
222
+            typeIdentifier: "HKQuantityTypeIdentifierStepCount",
223
+            displayName: "Step Count",
224
+            initialDiff: TypeDiff(
225
+                id: "HKQuantityTypeIdentifierStepCount",
226
+                typeIdentifier: "HKQuantityTypeIdentifierStepCount",
227
+                displayName: "Step Count",
228
+                currentCount: 1200,
229
+                previousCount: 980,
230
+                previousTracked: true,
231
+                appearedCount: 240,
232
+                disappearedCount: 20,
233
+                representationChangedCount: 0
234
+            )
235
+        )
236
+    }
237
+}
+15 -40
HealthProbe/Views/DataTypes/DataTypesView.swift
@@ -1,35 +1,14 @@
1 1
 import SwiftUI
2
-import SwiftData
3 2
 
4 3
 struct DataTypesView: View {
5
-    @Query(sort: \HealthSnapshot.timestamp, order: .reverse) private var allSnapshots: [HealthSnapshot]
6 4
     @State private var viewModel = DataTypesViewModel()
7 5
 
8
-    private var displayedSnapshots: [HealthSnapshot] {
9
-        guard let deviceID = localDeviceID else { return [] }
10
-        return allSnapshots.filter { $0.deviceID == deviceID }
11
-    }
12
-
13
-    private var latest: HealthSnapshot? { displayedSnapshots.first }
14
-
15 6
     private var displayedSnapshotContexts: [DataTypeSnapshotContext] {
16
-        displayedSnapshots.map {
17
-            DataTypeSnapshotContext(
18
-                id: $0.id,
19
-                observedAt: $0.timestamp,
20
-                archiveObservationID: $0.archiveObservationID
21
-            )
22
-        }
7
+        viewModel.observationContexts
23 8
     }
24 9
 
25 10
     private var latestContext: DataTypeSnapshotContext? {
26
-        latest.map {
27
-            DataTypeSnapshotContext(
28
-                id: $0.id,
29
-                observedAt: $0.timestamp,
30
-                archiveObservationID: $0.archiveObservationID
31
-            )
32
-        }
11
+        displayedSnapshotContexts.first
33 12
     }
34 13
 
35 14
     private var currentBaseline: DataTypeSnapshotContext? {
@@ -39,25 +18,17 @@ struct DataTypesView: View {
39 18
 
40 19
     private var archiveDiffTaskID: String {
41 20
         [
42
-            latestContext?.id.uuidString ?? "none",
43
-            currentBaseline?.id.uuidString ?? "none",
21
+            "\(latestContext?.observationID ?? -1)",
22
+            "\(currentBaseline?.observationID ?? -1)",
23
+            "\(viewModel.observationRows.count)",
44 24
             String(describing: viewModel.comparisonMode)
45 25
         ].joined(separator: "|")
46 26
     }
47 27
 
48
-    private var localDeviceID: String? {
49
-        let currentID = AppSettings.currentDeviceID
50
-        if allSnapshots.contains(where: { $0.deviceID == currentID }) {
51
-            return currentID
52
-        }
53
-
54
-        return allSnapshots.first?.deviceID
55
-    }
56
-
57 28
     var body: some View {
58 29
         NavigationStack {
59 30
             Group {
60
-                if displayedSnapshots.count < 2 {
31
+                if displayedSnapshotContexts.count < 2 {
61 32
                     EmptyStateView(
62 33
                         icon: "waveform.path.ecg",
63 34
                         title: "Not Enough Data",
@@ -70,6 +41,7 @@ struct DataTypesView: View {
70 41
             .navigationTitle("Data Types")
71 42
             .toolbar { filterPicker }
72 43
             .task(id: archiveDiffTaskID) {
44
+                await viewModel.loadArchiveRows()
73 45
                 await viewModel.loadArchiveDiffs(current: latestContext, snapshots: displayedSnapshotContexts)
74 46
             }
75 47
         }
@@ -90,11 +62,15 @@ struct DataTypesView: View {
90 62
             } else {
91 63
                 ForEach(diffs) { diff in
92 64
                     NavigationLink(destination: {
93
-                        if let latest = latest {
94
-                            DataTypeSnapshotDetailView(
95
-                                snapshot: latest,
65
+                        if let latestContext,
66
+                           let currentBaseline {
67
+                            DataTypeArchiveDetailView(
68
+                                current: latestContext,
69
+                                baseline: currentBaseline,
70
+                                timeline: displayedSnapshotContexts,
96 71
                                 typeIdentifier: diff.typeIdentifier,
97
-                                displayName: diff.displayName
72
+                                displayName: diff.displayName,
73
+                                initialDiff: diff
98 74
                             )
99 75
                         }
100 76
                     }) {
@@ -254,6 +230,5 @@ private enum DeltaIndicator {
254 230
 
255 231
 #Preview {
256 232
     DataTypesView()
257
-        .modelContainer(for: [HealthSnapshot.self, TypeCount.self, HealthRecord.self, TypeDistributionBin.self], inMemory: true)
258 233
         .environment(AppSettings())
259 234
 }