diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 86bc7ede5..e963177b5 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -21408,9 +21408,12 @@ components: type: string minLength: 1 maxLength: 60 - description: "Apple's `HKUnit` string for `value`, captured for audit. The server does NOT convert from it: `value` is - read in the unit HealthLog stores the identifier in (`m` for walking distance, `kg` for body mass, `kcal` - for energy, `degC` for temperature). Send the sample in that unit." + description: "Apple's `HKUnit` string for `value`, captured for audit: the server records it and never validates `value` + against it. `value` is read in Apple's own `HKUnit` for the identifier — `m` for a length, `kg` for body + mass, `kcal` for energy, `degC` for temperature, a `fraction` for the percent-shaped ones (oxygen + saturation, body fat) — and HealthLog applies its storage scaling after that. A waist circumference sent as + `0.86` in `m` is stored as `86` `cm`; an oxygen saturation sent as `0.98` in `fraction` is stored as `98` + `%`. Send the sample in Apple's unit, not in HealthLog's." startDate: type: string format: date-time diff --git a/scripts/repair-apple-health-distance.ts b/scripts/repair-apple-health-distance.ts index be77a3b8f..1843f5362 100644 --- a/scripts/repair-apple-health-distance.ts +++ b/scripts/repair-apple-health-distance.ts @@ -84,6 +84,7 @@ async function main(): Promise { let totalUpdated = 0; let totalSkipped = 0; let refused = 0; + let wouldRefuse = 0; let rollupsPending = 0; for (const plan of plans) { @@ -104,7 +105,11 @@ async function main(): Promise { continue; } - totalRepairable += plan.repairable.length; + // An account with one out-of-range candidate is refused whole, so its + // rows are not "would be repaired" — counting them promised work the + // --apply run then declined to do. + if (plan.outOfRange.length > 0) wouldRefuse += 1; + else totalRepairable += plan.repairable.length; const first = plan.repairable[0]; const last = plan.repairable[plan.repairable.length - 1]; console.log( @@ -130,7 +135,8 @@ async function main(): Promise { } if (plan.outOfRange.length > 0) { console.log( - " ! this account will be REFUSED — see the reason under --apply", + " ! this account will be REFUSED whole — its rows are not in " + + "the count below; see the reason under --apply", ); } @@ -166,9 +172,13 @@ async function main(): Promise { apply ? `\nDone. ${totalUpdated} row(s) repaired, ${totalSkipped} skipped, ` + `${refused} account(s) refused.` - : `\nDry run: ${totalRepairable} row(s) would be repaired. ` + - "Read the worked examples above and decide before re-running with " + - "--apply.", + : `\nDry run: ${totalRepairable} row(s) would be repaired` + + (wouldRefuse > 0 + ? `, ${wouldRefuse} account(s) would be refused (their rows are ` + + "not in that number)" + : "") + + ". Read the worked examples above and decide before re-running " + + "with --apply.", ); if (rollupsPending > 0) { diff --git a/src/lib/measurements/repair-apple-health-distance.ts b/src/lib/measurements/repair-apple-health-distance.ts index bd2947586..19cc9364c 100644 --- a/src/lib/measurements/repair-apple-health-distance.ts +++ b/src/lib/measurements/repair-apple-health-distance.ts @@ -259,8 +259,9 @@ function outOfRangeRefusal(plan: AccountRepairPlan, factor: number): string { `leave the plausible range after x${factor}, so these rows are not the ` + "1000x class — the account was most likely re-imported on the fixed " + "build, or its archive was never in the affected unit. Nothing written. " + - "Check the dry run's worked examples against the account's Health app " + - "before forcing anything." + "There is no override: check the dry run's worked examples against the " + + "account's Health app, and if the archive really was in another unit, " + + "re-run with the matching --unit." ); } diff --git a/src/lib/openapi/routes/measurements.ts b/src/lib/openapi/routes/measurements.ts index 1f28b049b..f138557cb 100644 --- a/src/lib/openapi/routes/measurements.ts +++ b/src/lib/openapi/routes/measurements.ts @@ -58,7 +58,7 @@ const batchEntrySchema = z .min(1) .max(60) .describe( - "Apple's `HKUnit` string for `value`, captured for audit. The server does NOT convert from it: `value` is read in the unit HealthLog stores the identifier in (`m` for walking distance, `kg` for body mass, `kcal` for energy, `degC` for temperature). Send the sample in that unit.", + "Apple's `HKUnit` string for `value`, captured for audit: the server records it and never validates `value` against it. `value` is read in Apple's own `HKUnit` for the identifier — `m` for a length, `kg` for body mass, `kcal` for energy, `degC` for temperature, a `fraction` for the percent-shaped ones (oxygen saturation, body fat) — and HealthLog applies its storage scaling after that. A waist circumference sent as `0.86` in `m` is stored as `86` `cm`; an oxygen saturation sent as `0.98` in `fraction` is stored as `98` `%`. Send the sample in Apple's unit, not in HealthLog's.", ), startDate: z.iso.datetime({ offset: true }), endDate: z.iso.datetime({ offset: true }),