Showing 3 changed files with 81 additions and 2 deletions
+1 -0
HealthProbe/Doc/04-project/Import-Optimization-Log.md
@@ -574,6 +574,7 @@ rows exist".
574 574
 | 2026-06-03 | `ec7ee29` | Add explicit loading states for Dashboard, Snapshots, and Data Types archive rows. | Triggered by false "no observations/no snapshots/not enough data" states during the first few seconds after app launch. Expected signal: startup shows loading state until SQLite rows are available, then shows real archive data without flicker. |
575 575
 | 2026-06-03 | `e231eaf` | Use the HealthKit registry as SQLite sample type display-name fallback. | Triggered by Snapshot detail showing raw identifiers such as `HKCategoryTypeIdentifierAppleStandHour` after UI moved from Core Data cache to SQLite summaries. Expected signal: existing and new archive rows show human-readable names such as `Stand Hours` without requiring reset/reimport. |
576 576
 | 2026-06-03 | `5fafcdd` | Expand the HealthKit type registry for full-dataset discovery while keeping the original 15-type profile as the tested default. | Triggered by the decision that import/storage cannot be considered complete based only on the restricted v1 dataset. Expected signal: Settings/authorization can expose a much broader quantity/category/workout catalog, unsupported types are explicit, and real-device coverage reports can measure full authorized backup volume. |
577
+| 2026-06-03 | committed | Add explicit capture profile controls for full-dataset discovery. | A real-device report after registry expansion still showed `Types: 15/15 processed` and the old monitored type-set hash because `selectedTypeIDs` persisted the v1 core profile in `UserDefaults`. Settings now exposes `Select All Available Types`, `Select Core Profile`, and selected/available counts so the next real-device run can deliberately switch from the v1 sample set to the expanded supported registry. |
577 578
 
578 579
 ## Current Diagnosis
579 580
 
+43 -1
HealthProbe/Utilities/AppSettings.swift
@@ -24,16 +24,46 @@ final class AppSettings {
24 24
         }
25 25
     }
26 26
 
27
+    var selectedTypeCount: Int {
28
+        selectedTypeIDs.count
29
+    }
30
+
31
+    var defaultTypeCount: Int {
32
+        Self.defaultTypeIDs.count
33
+    }
34
+
35
+    var availableTypeCount: Int {
36
+        Self.availableTypeIDs.count
37
+    }
38
+
39
+    var usesDefaultTypeProfile: Bool {
40
+        selectedTypeIDs == Self.defaultTypeIDs
41
+    }
42
+
43
+    var usesAllAvailableTypeProfile: Bool {
44
+        selectedTypeIDs == Self.availableTypeIDs
45
+    }
46
+
27 47
     static var currentDeviceID: String {
28 48
         KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id
29 49
     }
30 50
 
51
+    private static var defaultTypeIDs: Set<String> {
52
+        Set(HealthKitService.allTypes.filter { $0.isEnabledByDefault }.map(\.id))
53
+    }
54
+
55
+    private static var availableTypeIDs: Set<String> {
56
+        Set(HealthKitService.allTypes.compactMap { type in
57
+            type.objectType == nil ? nil : type.id
58
+        })
59
+    }
60
+
31 61
     init() {
32 62
         if let data = UserDefaults.standard.data(forKey: Self.selectedTypeIDsKey),
33 63
            let ids  = try? JSONDecoder().decode([String].self, from: data) {
34 64
             selectedTypeIDs = Set(ids)
35 65
         } else {
36
-            selectedTypeIDs = Set(HealthKitService.allTypes.filter { $0.isEnabledByDefault }.map { $0.id })
66
+            selectedTypeIDs = Self.defaultTypeIDs
37 67
         }
38 68
 
39 69
         if UserDefaults.standard.object(forKey: Self.adaptiveTimeoutsEnabledKey) == nil {
@@ -52,6 +82,18 @@ final class AppSettings {
52 82
         else { selectedTypeIDs.insert(type.id) }
53 83
     }
54 84
 
85
+    func selectDefaultTypes() {
86
+        selectedTypeIDs = Self.defaultTypeIDs
87
+    }
88
+
89
+    func selectAllAvailableTypes() {
90
+        selectedTypeIDs = Self.availableTypeIDs
91
+    }
92
+
93
+    func clearSelectedTypes() {
94
+        selectedTypeIDs = []
95
+    }
96
+
55 97
     private func persistTypes() {
56 98
         UserDefaults.standard.set(try? JSONEncoder().encode(Array(selectedTypeIDs)),
57 99
                                   forKey: Self.selectedTypeIDsKey)
+37 -1
HealthProbe/Views/Settings/SettingsView.swift
@@ -165,6 +165,34 @@ struct SettingsView: View {
165 165
 
166 166
     @ViewBuilder
167 167
     private var typeSelectionSections: some View {
168
+        Section("Capture Profile") {
169
+            InfoRow(label: "Selected Types") {
170
+                Text("\(appSettings.selectedTypeCount) of \(appSettings.availableTypeCount) available")
171
+                    .foregroundStyle(.secondary)
172
+            }
173
+
174
+            Button {
175
+                appSettings.selectAllAvailableTypes()
176
+            } label: {
177
+                Label("Select All Available Types", systemImage: "checkmark.circle")
178
+            }
179
+            .disabled(appSettings.usesAllAvailableTypeProfile)
180
+
181
+            Button {
182
+                appSettings.selectDefaultTypes()
183
+            } label: {
184
+                Label("Select Core Profile", systemImage: "list.bullet")
185
+            }
186
+            .disabled(appSettings.usesDefaultTypeProfile)
187
+
188
+            Button(role: .destructive) {
189
+                appSettings.clearSelectedTypes()
190
+            } label: {
191
+                Label("Clear Selection", systemImage: "xmark.circle")
192
+            }
193
+            .disabled(appSettings.selectedTypeIDs.isEmpty)
194
+        }
195
+
168 196
         ForEach(TypeCategory.allCases, id: \.self) { category in
169 197
             Section(category.rawValue) {
170 198
                 ForEach(HealthKitService.allTypes.filter { $0.category == category }) { type in
@@ -332,8 +360,16 @@ private struct TypeToggleRow: View {
332 360
             get: { appSettings.isEnabled(type) },
333 361
             set: { _ in appSettings.toggle(type) }
334 362
         )) {
335
-            Text(type.displayName)
363
+            VStack(alignment: .leading, spacing: 2) {
364
+                Text(type.displayName)
365
+                if type.objectType == nil {
366
+                    Text("Unavailable on this iOS")
367
+                        .font(.caption)
368
+                        .foregroundStyle(.secondary)
369
+                }
370
+            }
336 371
         }
372
+        .disabled(type.objectType == nil)
337 373
         .accessibilityLabel("\(type.displayName), \(appSettings.isEnabled(type) ? "enabled" : "disabled")")
338 374
     }
339 375
 }