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