@@ -576,6 +576,7 @@ rows exist". |
||
| 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 | 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. | |
| 579 |
+| 2026-06-03 | pending | Harden quantity unit conversion for full-profile imports. | The first 127-metric run crashed while archiving `HKQuantityTypeIdentifierDietaryWater`: the previous fallback converted unknown quantities with `.count()`, and HealthKit raised `NSInvalidArgumentException` for incompatible `mL` to `count`. The archive now maps known extended units, stores quantity rows with nil numeric/unit when a future type is unmapped, and removes the unsafe display fallback. Next run should pass Water and reveal any remaining type-specific unit gaps. | |
|
| 579 | 580 |
|
| 580 | 581 |
## Current Diagnosis |
| 581 | 582 |
|
@@ -1789,7 +1789,7 @@ final class HealthKitService {
|
||
| 1789 | 1789 |
return nil |
| 1790 | 1790 |
} |
| 1791 | 1791 |
|
| 1792 |
- private static func quantityDisplayValue(for sample: HKQuantitySample) -> String {
|
|
| 1792 |
+ private static func quantityDisplayValue(for sample: HKQuantitySample) -> String? {
|
|
| 1793 | 1793 |
let identifier = sample.quantityType.identifier |
| 1794 | 1794 |
switch identifier {
|
| 1795 | 1795 |
case HKQuantityTypeIdentifier.stepCount.rawValue: |
@@ -1819,7 +1819,7 @@ final class HealthKitService {
|
||
| 1819 | 1819 |
.unitDivided(by: .minute()) |
| 1820 | 1820 |
return "\(format(sample.quantity.doubleValue(for: unit), maximumFractionDigits: 1)) mL/kg/min" |
| 1821 | 1821 |
default: |
| 1822 |
- return "\(format(sample.quantity.doubleValue(for: .count()), maximumFractionDigits: 2))" |
|
| 1822 |
+ return nil |
|
| 1823 | 1823 |
} |
| 1824 | 1824 |
} |
| 1825 | 1825 |
|
@@ -3784,33 +3784,120 @@ private struct ArchiveSampleRow {
|
||
| 3784 | 3784 |
String(format: "%.6f", date.timeIntervalSince1970) |
| 3785 | 3785 |
} |
| 3786 | 3786 |
|
| 3787 |
- nonisolated private static func quantityPayload(_ sample: HKSample) -> (kind: String, value: Double, unit: String)? {
|
|
| 3787 |
+ nonisolated private static func quantityPayload(_ sample: HKSample) -> (kind: String, value: Double?, unit: String?)? {
|
|
| 3788 | 3788 |
guard let sample = sample as? HKQuantitySample else { return nil }
|
| 3789 | 3789 |
let identifier = sample.quantityType.identifier |
| 3790 |
+ guard let storageUnit = quantityStorageUnit(for: identifier) else {
|
|
| 3791 |
+ return ("quantity", nil, nil)
|
|
| 3792 |
+ } |
|
| 3793 |
+ return ("quantity", sample.quantity.doubleValue(for: storageUnit.unit), storageUnit.label)
|
|
| 3794 |
+ } |
|
| 3795 |
+ |
|
| 3796 |
+ nonisolated private static func quantityStorageUnit(for identifier: String) -> (unit: HKUnit, label: String)? {
|
|
| 3790 | 3797 |
switch identifier {
|
| 3791 | 3798 |
case HKQuantityTypeIdentifier.heartRate.rawValue, |
| 3792 |
- HKQuantityTypeIdentifier.restingHeartRate.rawValue: |
|
| 3793 |
- return ("quantity", sample.quantity.doubleValue(for: HKUnit.count().unitDivided(by: .minute())), "count/min")
|
|
| 3799 |
+ HKQuantityTypeIdentifier.restingHeartRate.rawValue, |
|
| 3800 |
+ "HKQuantityTypeIdentifierWalkingHeartRateAverage": |
|
| 3801 |
+ return (HKUnit.count().unitDivided(by: .minute()), "count/min") |
|
| 3794 | 3802 |
case HKQuantityTypeIdentifier.respiratoryRate.rawValue: |
| 3795 |
- return ("quantity", sample.quantity.doubleValue(for: HKUnit.count().unitDivided(by: .minute())), "count/min")
|
|
| 3796 |
- case HKQuantityTypeIdentifier.activeEnergyBurned.rawValue: |
|
| 3797 |
- return ("quantity", sample.quantity.doubleValue(for: .kilocalorie()), "kcal")
|
|
| 3798 |
- case HKQuantityTypeIdentifier.distanceWalkingRunning.rawValue: |
|
| 3799 |
- return ("quantity", sample.quantity.doubleValue(for: .meter()), "m")
|
|
| 3800 |
- case HKQuantityTypeIdentifier.appleExerciseTime.rawValue: |
|
| 3801 |
- return ("quantity", sample.quantity.doubleValue(for: .minute()), "min")
|
|
| 3803 |
+ return (HKUnit.count().unitDivided(by: .minute()), "count/min") |
|
| 3804 |
+ case HKQuantityTypeIdentifier.activeEnergyBurned.rawValue, |
|
| 3805 |
+ "HKQuantityTypeIdentifierBasalEnergyBurned", |
|
| 3806 |
+ "HKQuantityTypeIdentifierDietaryEnergyConsumed": |
|
| 3807 |
+ return (.kilocalorie(), "kcal") |
|
| 3808 |
+ case HKQuantityTypeIdentifier.distanceWalkingRunning.rawValue, |
|
| 3809 |
+ "HKQuantityTypeIdentifierDistanceCycling", |
|
| 3810 |
+ "HKQuantityTypeIdentifierDistanceSwimming", |
|
| 3811 |
+ "HKQuantityTypeIdentifierDistanceDownhillSnowSports", |
|
| 3812 |
+ "HKQuantityTypeIdentifierDistanceWheelchair", |
|
| 3813 |
+ "HKQuantityTypeIdentifierHeight", |
|
| 3814 |
+ "HKQuantityTypeIdentifierWaistCircumference", |
|
| 3815 |
+ "HKQuantityTypeIdentifierWalkingStepLength", |
|
| 3816 |
+ "HKQuantityTypeIdentifierSixMinuteWalkTestDistance": |
|
| 3817 |
+ return (.meter(), "m") |
|
| 3818 |
+ case "HKQuantityTypeIdentifierWalkingSpeed", |
|
| 3819 |
+ "HKQuantityTypeIdentifierStairAscentSpeed", |
|
| 3820 |
+ "HKQuantityTypeIdentifierStairDescentSpeed": |
|
| 3821 |
+ return (.meter().unitDivided(by: .second()), "m/s") |
|
| 3822 |
+ case HKQuantityTypeIdentifier.appleExerciseTime.rawValue, |
|
| 3823 |
+ "HKQuantityTypeIdentifierAppleMoveTime", |
|
| 3824 |
+ "HKQuantityTypeIdentifierAppleStandTime": |
|
| 3825 |
+ return (.minute(), "min") |
|
| 3802 | 3826 |
case HKQuantityTypeIdentifier.environmentalAudioExposure.rawValue, |
| 3803 | 3827 |
HKQuantityTypeIdentifier.headphoneAudioExposure.rawValue: |
| 3804 |
- return ("quantity", sample.quantity.doubleValue(for: .decibelAWeightedSoundPressureLevel()), "dBASPL")
|
|
| 3805 |
- case HKQuantityTypeIdentifier.bodyMass.rawValue: |
|
| 3806 |
- return ("quantity", sample.quantity.doubleValue(for: HKUnit.gramUnit(with: .kilo)), "kg")
|
|
| 3828 |
+ return (.decibelAWeightedSoundPressureLevel(), "dBASPL") |
|
| 3829 |
+ case HKQuantityTypeIdentifier.bodyMass.rawValue, |
|
| 3830 |
+ "HKQuantityTypeIdentifierLeanBodyMass": |
|
| 3831 |
+ return (HKUnit.gramUnit(with: .kilo), "kg") |
|
| 3807 | 3832 |
case HKQuantityTypeIdentifier.vo2Max.rawValue: |
| 3808 |
- let unit = HKUnit.literUnit(with: .milli) |
|
| 3809 |
- .unitDivided(by: HKUnit.gramUnit(with: .kilo)) |
|
| 3810 |
- .unitDivided(by: .minute()) |
|
| 3811 |
- return ("quantity", sample.quantity.doubleValue(for: unit), "mL/kg/min")
|
|
| 3833 |
+ return ( |
|
| 3834 |
+ HKUnit.literUnit(with: .milli) |
|
| 3835 |
+ .unitDivided(by: HKUnit.gramUnit(with: .kilo)) |
|
| 3836 |
+ .unitDivided(by: .minute()), |
|
| 3837 |
+ "mL/kg/min" |
|
| 3838 |
+ ) |
|
| 3839 |
+ case HKQuantityTypeIdentifier.stepCount.rawValue, |
|
| 3840 |
+ "HKQuantityTypeIdentifierFlightsClimbed", |
|
| 3841 |
+ "HKQuantityTypeIdentifierSwimmingStrokeCount", |
|
| 3842 |
+ "HKQuantityTypeIdentifierPushCount", |
|
| 3843 |
+ "HKQuantityTypeIdentifierInhalerUsage", |
|
| 3844 |
+ "HKQuantityTypeIdentifierBodyMassIndex": |
|
| 3845 |
+ return (.count(), "count") |
|
| 3846 |
+ case "HKQuantityTypeIdentifierHeartRateVariabilitySDNN": |
|
| 3847 |
+ return (HKUnit.secondUnit(with: .milli), "ms") |
|
| 3848 |
+ case "HKQuantityTypeIdentifierAtrialFibrillationBurden", |
|
| 3849 |
+ "HKQuantityTypeIdentifierOxygenSaturation", |
|
| 3850 |
+ "HKQuantityTypeIdentifierBodyFatPercentage", |
|
| 3851 |
+ "HKQuantityTypeIdentifierPeripheralPerfusionIndex", |
|
| 3852 |
+ "HKQuantityTypeIdentifierWalkingAsymmetryPercentage", |
|
| 3853 |
+ "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage", |
|
| 3854 |
+ "HKQuantityTypeIdentifierAppleWalkingSteadiness": |
|
| 3855 |
+ return (.percent(), "%") |
|
| 3856 |
+ case "HKQuantityTypeIdentifierForcedVitalCapacity", |
|
| 3857 |
+ "HKQuantityTypeIdentifierForcedExpiratoryVolume1": |
|
| 3858 |
+ return (.liter(), "L") |
|
| 3859 |
+ case "HKQuantityTypeIdentifierPeakExpiratoryFlowRate": |
|
| 3860 |
+ return (.liter().unitDivided(by: .minute()), "L/min") |
|
| 3861 |
+ case "HKQuantityTypeIdentifierBodyTemperature", |
|
| 3862 |
+ "HKQuantityTypeIdentifierBasalBodyTemperature": |
|
| 3863 |
+ return (.degreeCelsius(), "degC") |
|
| 3864 |
+ case "HKQuantityTypeIdentifierBloodPressureSystolic", |
|
| 3865 |
+ "HKQuantityTypeIdentifierBloodPressureDiastolic": |
|
| 3866 |
+ return (.millimeterOfMercury(), "mmHg") |
|
| 3867 |
+ case "HKQuantityTypeIdentifierBloodGlucose": |
|
| 3868 |
+ return (HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)), "mg/dL") |
|
| 3869 |
+ case "HKQuantityTypeIdentifierInsulinDelivery": |
|
| 3870 |
+ return (.internationalUnit(), "IU") |
|
| 3871 |
+ case "HKQuantityTypeIdentifierElectrodermalActivity": |
|
| 3872 |
+ return (HKUnit.siemenUnit(with: .micro), "uS") |
|
| 3873 |
+ case "HKQuantityTypeIdentifierDietaryWater": |
|
| 3874 |
+ return (HKUnit.literUnit(with: .milli), "mL") |
|
| 3875 |
+ case "HKQuantityTypeIdentifierDietaryProtein", |
|
| 3876 |
+ "HKQuantityTypeIdentifierDietaryCarbohydrates", |
|
| 3877 |
+ "HKQuantityTypeIdentifierDietaryFiber", |
|
| 3878 |
+ "HKQuantityTypeIdentifierDietarySugar", |
|
| 3879 |
+ "HKQuantityTypeIdentifierDietaryFatTotal", |
|
| 3880 |
+ "HKQuantityTypeIdentifierDietaryFatSaturated", |
|
| 3881 |
+ "HKQuantityTypeIdentifierDietaryFatMonounsaturated", |
|
| 3882 |
+ "HKQuantityTypeIdentifierDietaryFatPolyunsaturated", |
|
| 3883 |
+ "HKQuantityTypeIdentifierDietaryCholesterol", |
|
| 3884 |
+ "HKQuantityTypeIdentifierDietarySodium", |
|
| 3885 |
+ "HKQuantityTypeIdentifierDietaryPotassium", |
|
| 3886 |
+ "HKQuantityTypeIdentifierDietaryCalcium", |
|
| 3887 |
+ "HKQuantityTypeIdentifierDietaryIron", |
|
| 3888 |
+ "HKQuantityTypeIdentifierDietaryMagnesium", |
|
| 3889 |
+ "HKQuantityTypeIdentifierDietaryZinc", |
|
| 3890 |
+ "HKQuantityTypeIdentifierDietaryVitaminA", |
|
| 3891 |
+ "HKQuantityTypeIdentifierDietaryVitaminB6", |
|
| 3892 |
+ "HKQuantityTypeIdentifierDietaryVitaminB12", |
|
| 3893 |
+ "HKQuantityTypeIdentifierDietaryVitaminC", |
|
| 3894 |
+ "HKQuantityTypeIdentifierDietaryVitaminD", |
|
| 3895 |
+ "HKQuantityTypeIdentifierDietaryVitaminE", |
|
| 3896 |
+ "HKQuantityTypeIdentifierDietaryVitaminK", |
|
| 3897 |
+ "HKQuantityTypeIdentifierDietaryCaffeine": |
|
| 3898 |
+ return (.gram(), "g") |
|
| 3812 | 3899 |
default: |
| 3813 |
- return ("quantity", sample.quantity.doubleValue(for: .count()), "count")
|
|
| 3900 |
+ return nil |
|
| 3814 | 3901 |
} |
| 3815 | 3902 |
} |
| 3816 | 3903 |
|