@@ -575,6 +575,7 @@ rows exist". |
||
| 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 | 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. | |
| 578 |
+| 2026-06-03 | pending | Migrate legacy core-profile selections to full available capture by default. | A follow-up real-device report still showed the old `4907...` monitored type-set hash and `Types: 15/15 processed`, proving the running app still used the old persisted selected type set. New installs and pre-profile settings that exactly match the old core profile now migrate to `All available`; only an explicit `Select Core Profile` action persists the core subset. Settings also shows the active profile label (`All available`, `Core`, or `Custom`) for quick verification before capture. | |
|
| 578 | 579 |
|
| 579 | 580 |
## Current Diagnosis |
| 580 | 581 |
|
@@ -3,11 +3,18 @@ import Foundation |
||
| 3 | 3 |
@Observable |
| 4 | 4 |
final class AppSettings {
|
| 5 | 5 |
private static let selectedTypeIDsKey = "hp_selectedTypeIDs" |
| 6 |
+ private static let captureProfileKey = "hp_captureProfile" |
|
| 6 | 7 |
static let adaptiveTimeoutsEnabledKey = "hp_adaptiveTimeoutsEnabled" |
| 7 | 8 |
static let simplifiedUIModeEnabledKey = "hp_simplifiedUIModeEnabled" |
| 8 | 9 |
static let typeDetailCacheBackfillVersionKey = "hp_typeDetailCacheBackfillVersion" |
| 9 | 10 |
static let currentTypeDetailCacheBackfillVersion = 2 |
| 10 | 11 |
|
| 12 |
+ private enum CaptureProfile: String {
|
|
| 13 |
+ case allAvailable |
|
| 14 |
+ case core |
|
| 15 |
+ case custom |
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 11 | 18 |
var selectedTypeIDs: Set<String> {
|
| 12 | 19 |
didSet { persistTypes() }
|
| 13 | 20 |
} |
@@ -44,6 +51,12 @@ final class AppSettings {
|
||
| 44 | 51 |
selectedTypeIDs == Self.availableTypeIDs |
| 45 | 52 |
} |
| 46 | 53 |
|
| 54 |
+ var captureProfileDescription: String {
|
|
| 55 |
+ if usesAllAvailableTypeProfile { return "All available" }
|
|
| 56 |
+ if usesDefaultTypeProfile { return "Core" }
|
|
| 57 |
+ return "Custom" |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 47 | 60 |
static var currentDeviceID: String {
|
| 48 | 61 |
KeychainService.resolveDeviceID(swiftDataStoreIsEmpty: false).id |
| 49 | 62 |
} |
@@ -59,11 +72,32 @@ final class AppSettings {
|
||
| 59 | 72 |
} |
| 60 | 73 |
|
| 61 | 74 |
init() {
|
| 75 |
+ let persistedTypeIDs: Set<String>? |
|
| 62 | 76 |
if let data = UserDefaults.standard.data(forKey: Self.selectedTypeIDsKey), |
| 63 | 77 |
let ids = try? JSONDecoder().decode([String].self, from: data) {
|
| 64 |
- selectedTypeIDs = Set(ids) |
|
| 78 |
+ persistedTypeIDs = Set(ids) |
|
| 65 | 79 |
} else {
|
| 80 |
+ persistedTypeIDs = nil |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ let persistedProfile = UserDefaults.standard.string(forKey: Self.captureProfileKey) |
|
| 84 |
+ .flatMap(CaptureProfile.init(rawValue:)) |
|
| 85 |
+ |
|
| 86 |
+ switch persistedProfile {
|
|
| 87 |
+ case .allAvailable: |
|
| 88 |
+ selectedTypeIDs = Self.availableTypeIDs |
|
| 89 |
+ case .core: |
|
| 66 | 90 |
selectedTypeIDs = Self.defaultTypeIDs |
| 91 |
+ case .custom: |
|
| 92 |
+ selectedTypeIDs = persistedTypeIDs ?? Self.availableTypeIDs |
|
| 93 |
+ case nil: |
|
| 94 |
+ if let persistedTypeIDs, persistedTypeIDs != Self.defaultTypeIDs {
|
|
| 95 |
+ selectedTypeIDs = persistedTypeIDs |
|
| 96 |
+ Self.persistCaptureProfile(.custom) |
|
| 97 |
+ } else {
|
|
| 98 |
+ selectedTypeIDs = Self.availableTypeIDs |
|
| 99 |
+ Self.persistCaptureProfile(.allAvailable) |
|
| 100 |
+ } |
|
| 67 | 101 |
} |
| 68 | 102 |
|
| 69 | 103 |
if UserDefaults.standard.object(forKey: Self.adaptiveTimeoutsEnabledKey) == nil {
|
@@ -78,19 +112,23 @@ final class AppSettings {
|
||
| 78 | 112 |
func isEnabled(_ type: MonitoredType) -> Bool { selectedTypeIDs.contains(type.id) }
|
| 79 | 113 |
|
| 80 | 114 |
func toggle(_ type: MonitoredType) {
|
| 115 |
+ Self.persistCaptureProfile(.custom) |
|
| 81 | 116 |
if selectedTypeIDs.contains(type.id) { selectedTypeIDs.remove(type.id) }
|
| 82 | 117 |
else { selectedTypeIDs.insert(type.id) }
|
| 83 | 118 |
} |
| 84 | 119 |
|
| 85 | 120 |
func selectDefaultTypes() {
|
| 121 |
+ Self.persistCaptureProfile(.core) |
|
| 86 | 122 |
selectedTypeIDs = Self.defaultTypeIDs |
| 87 | 123 |
} |
| 88 | 124 |
|
| 89 | 125 |
func selectAllAvailableTypes() {
|
| 126 |
+ Self.persistCaptureProfile(.allAvailable) |
|
| 90 | 127 |
selectedTypeIDs = Self.availableTypeIDs |
| 91 | 128 |
} |
| 92 | 129 |
|
| 93 | 130 |
func clearSelectedTypes() {
|
| 131 |
+ Self.persistCaptureProfile(.custom) |
|
| 94 | 132 |
selectedTypeIDs = [] |
| 95 | 133 |
} |
| 96 | 134 |
|
@@ -98,4 +136,8 @@ final class AppSettings {
|
||
| 98 | 136 |
UserDefaults.standard.set(try? JSONEncoder().encode(Array(selectedTypeIDs)), |
| 99 | 137 |
forKey: Self.selectedTypeIDsKey) |
| 100 | 138 |
} |
| 139 |
+ |
|
| 140 |
+ private static func persistCaptureProfile(_ profile: CaptureProfile) {
|
|
| 141 |
+ UserDefaults.standard.set(profile.rawValue, forKey: Self.captureProfileKey) |
|
| 142 |
+ } |
|
| 101 | 143 |
} |
@@ -166,6 +166,11 @@ struct SettingsView: View {
|
||
| 166 | 166 |
@ViewBuilder |
| 167 | 167 |
private var typeSelectionSections: some View {
|
| 168 | 168 |
Section("Capture Profile") {
|
| 169 |
+ InfoRow(label: "Profile") {
|
|
| 170 |
+ Text(appSettings.captureProfileDescription) |
|
| 171 |
+ .foregroundStyle(.secondary) |
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 169 | 174 |
InfoRow(label: "Selected Types") {
|
| 170 | 175 |
Text("\(appSettings.selectedTypeCount) of \(appSettings.availableTypeCount) available")
|
| 171 | 176 |
.foregroundStyle(.secondary) |