mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(medications): store a supplement's composition, read its label with the document AI, and carry it through backup and restore
This commit is contained in:
1 parent
98d64c2052
commit
eb3cc4237f
28 files changed
+1078
-97
No files matched your search
+138
-55
@@ -18895,7 +18895,6 @@ paths:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
"501": *a30
|
||||
/api/nutrients/batch:
|
||||
post:
|
||||
tags:
|
||||
@@ -19040,8 +19039,9 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NutrientCode"
|
||||
required: true
|
||||
description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine. Energy,
|
||||
macros and sodium/potassium are out of scope.
|
||||
description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine, and
|
||||
(v1.43, responses only) `dha` and `epa`, which only a supplement's composition produces. Energy, macros and
|
||||
sodium/potassium are out of scope. Decode unknown values leniently.
|
||||
- in: query
|
||||
name: days
|
||||
schema:
|
||||
@@ -23999,10 +23999,12 @@ paths:
|
||||
- Medications
|
||||
summary: Replace a supplement's composition (v1.43)
|
||||
description: "Stores the composition per unit in canonical units and re-counts every taken dose of this medication into
|
||||
the day's micronutrients (`recompute.queued`). Needs the `nutrients` module (403 `module.disabled`,
|
||||
`meta.module: \"nutrients\"`). 404 `medications.notFound`; 422 `medications.ingredients.invalid` (body,
|
||||
duplicate or unknown code); 422 `medications.ingredients.unitMismatch` with `meta.index` and
|
||||
`meta.allowedUnits`."
|
||||
the day's micronutrients: `recompute.queued` is true when the worker has the recompute (the rows follow within
|
||||
moments), false when it ran before this answer. Needs the `nutrients` module (403 `module.disabled`,
|
||||
`meta.module: \"nutrients\"`). Delegable at MANAGE on the `medications` domain. 404 `medications.notFound`; 422
|
||||
`medications.ingredients.invalid` (body, duplicate code, or unknown code with `meta.index`); 422
|
||||
`medications.ingredients.unitMismatch` with `meta.index` and `meta.allowedUnits`; 429 above 30 a minute per
|
||||
caller."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -24024,9 +24026,27 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PutMedicationIngredientsResponse"
|
||||
"401": *a1
|
||||
"403": *a19
|
||||
"403":
|
||||
description: >-
|
||||
The opt-in `nutrients` module is off for this account: 403 `module.disabled` with `meta.module:
|
||||
"nutrients"`.
|
||||
|
||||
|
||||
Refused: this request named a record the caller may not act on (`meta.errorCode` = `sharing.access.denied`).
|
||||
Byte-identical for an account that does not exist, an account that granted nothing, a grant whose sections
|
||||
do not reach this surface, and a read grant on a request that writes — the response carries no reason, so it
|
||||
is not an account-enumeration oracle. The reason reaches the record owner's activity feed and the operator's
|
||||
audit trail instead. On the cookie transport the same code answers one further case: a session that has
|
||||
entered a shared record and sends a request without its record-context assertion, which is a client older
|
||||
than the fence — leave the record and reload. A request that names no record but the caller's own never
|
||||
reaches this response, so a client that never switches and never sends the per-request account selector will
|
||||
not see it.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"404":
|
||||
description: Medication not found.
|
||||
description: "`medications.notFound`."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -24034,17 +24054,21 @@ paths:
|
||||
"409": *a7
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
"501": *a30
|
||||
/api/medications/{id}/ingredients/extract:
|
||||
post:
|
||||
tags:
|
||||
- Medications
|
||||
summary: Read a supplement label (v1.43)
|
||||
description: Multipart `file` (JPEG, PNG, HEIC, WebP or PDF, at most 10 MB). 202 with an AI run; poll `GET
|
||||
/api/ai-runs/{id}` (kind `SUPPLEMENT_LABEL_EXTRACT`, result `SupplementLabelExtractResult`). Needs
|
||||
`ai.capabilities.supplementLabel.available` (refused like every AI capability, reason `usage_level` included)
|
||||
and the `nutrients` module. 422 `medications.ingredients.extract.fileType`, 413
|
||||
`medications.ingredients.extract.fileTooLarge`, 429 `medications.ingredients.extract.rateLimited` (six an hour).
|
||||
description: "Multipart `file` (JPEG, PNG, WebP or PDF, at most 10 MB; the bytes decide the type, not the header). HEIC
|
||||
is refused with `medications.ingredients.extract.fileType`: send the photo as JPEG. 202 with an AI run; poll
|
||||
`GET /api/ai-runs/{id}` (kind `SUPPLEMENT_LABEL_EXTRACT`, result `SupplementLabelExtractResult`). Amounts in the
|
||||
proposal are per single unit: a label printed per serving of several units is divided down, and `servingNote`
|
||||
says what was printed. Needs `ai.capabilities.supplementLabel.available` (refused like every AI capability,
|
||||
reason `usage_level` included) and the `nutrients` module. The account's own session only: provider work is
|
||||
never admitted inside somebody else's record. 404 `medications.notFound`; 422
|
||||
`medications.ingredients.extract.fileType`; 413 `medications.ingredients.extract.fileTooLarge`; 429
|
||||
`medications.ingredients.extract.rateLimited` (six an hour) or `medications.ingredients.extract.budgetExceeded`
|
||||
(the day's AI budget). A run that fails carries `medications.ingredients.extract.failed`."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -24066,23 +24090,40 @@ paths:
|
||||
responses:
|
||||
"202": *a39
|
||||
"401": *a1
|
||||
"403": *a19
|
||||
"403":
|
||||
description: "The opt-in `nutrients` module is off for this account: 403 `module.disabled` with `meta.module:
|
||||
\"nutrients\"`. The `supplementLabel` AI capability is unavailable for this record. `meta.capability` =
|
||||
`supplementLabel` and `meta.reason` says which layer refused: the operator's switch
|
||||
(`assistant.disabled.documentAi`, or `assistant.disabled.enabled` for the master switch), an owning module
|
||||
or the person's own AI opt-out (`module.disabled` with `meta.module`), a delegate inside somebody else's
|
||||
record (`ai.record.notPermitted`), or a missing consent receipt (`consent.ai.required`). Nothing was
|
||||
generated, read from a stored model answer, or enqueued."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"404":
|
||||
description: "`medications.notFound`."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"413":
|
||||
description: "`medications.ingredients.extract.fileTooLarge`."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
"501": *a30
|
||||
"503":
|
||||
description: "`aiRuns.workerUnavailable`: the run could not be handed to the background worker. Nothing was read and
|
||||
nothing was charged; try again."
|
||||
"422":
|
||||
description: "`medications.ingredients.extract.fileType`: the upload is not a JPEG, PNG, WebP or PDF, or carries no
|
||||
`file` field. No configured AI provider can serve the `supplementLabel` capability: `meta.errorCode` =
|
||||
`ai.provider.none`, `meta.reason` = `no_provider`."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429": *a5
|
||||
"503": *a18
|
||||
/api/anamnesis/life-circumstances:
|
||||
get:
|
||||
tags:
|
||||
@@ -33235,7 +33276,7 @@ components:
|
||||
type: string
|
||||
pattern: ^\d{4}-\d{2}-\d{2}$
|
||||
nutrient:
|
||||
$ref: "#/components/schemas/NutrientCode"
|
||||
$ref: "#/components/schemas/SyncableNutrientCode"
|
||||
unit:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -33254,6 +33295,37 @@ components:
|
||||
- amount
|
||||
description: One nutrient day total. day = YYYY-MM-DD in the user's IANA timezone; unit must equal the catalog's
|
||||
canonical unit for the code (mg | ug | ml) or the entry is skipped unit_mismatch.
|
||||
SyncableNutrientCode:
|
||||
type: string
|
||||
enum:
|
||||
- vitamin_a
|
||||
- thiamin
|
||||
- riboflavin
|
||||
- niacin
|
||||
- pantothenic_acid
|
||||
- vitamin_b6
|
||||
- biotin
|
||||
- folate
|
||||
- vitamin_b12
|
||||
- vitamin_c
|
||||
- vitamin_d
|
||||
- vitamin_e
|
||||
- vitamin_k
|
||||
- calcium
|
||||
- iron
|
||||
- magnesium
|
||||
- phosphorus
|
||||
- zinc
|
||||
- copper
|
||||
- manganese
|
||||
- selenium
|
||||
- chromium
|
||||
- molybdenum
|
||||
- iodine
|
||||
- water
|
||||
- caffeine
|
||||
description: "A nutrient code HealthKit may send: the 24 Dietary vitamin/mineral types plus water and caffeine. Never
|
||||
`dha` or `epa`."
|
||||
NutrientCode:
|
||||
type: string
|
||||
enum:
|
||||
@@ -33283,8 +33355,11 @@ components:
|
||||
- iodine
|
||||
- water
|
||||
- caffeine
|
||||
description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine. Energy,
|
||||
macros and sodium/potassium are out of scope.
|
||||
- dha
|
||||
- epa
|
||||
description: Closed nutrient catalog code — 24 HealthKit Dietary vitamin/mineral types plus water and caffeine, and
|
||||
(v1.43, responses only) `dha` and `epa`, which only a supplement's composition produces. Energy, macros and
|
||||
sodium/potassium are out of scope. Decode unknown values leniently.
|
||||
NutrientWaterWriteRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -35572,37 +35647,40 @@ components:
|
||||
medications.category.limitReached, medications.category.unknown, medications.course.currentOpen,
|
||||
medications.course.invalid, medications.course.invalidRange, medications.course.notFound,
|
||||
medications.course.oneShot, medications.course.overlap, medications.course.windowRequired,
|
||||
medications.ingredients.invalid, medications.intake.bulk.apple_health_not_mirrored,
|
||||
medications.ingredients.extract.budgetExceeded, medications.ingredients.extract.failed,
|
||||
medications.ingredients.extract.fileTooLarge, medications.ingredients.extract.fileType,
|
||||
medications.ingredients.extract.rateLimited, medications.ingredients.invalid,
|
||||
medications.ingredients.unitMismatch, medications.intake.bulk.apple_health_not_mirrored,
|
||||
medications.intake.bulk.invalid, medications.intake.bulk.too_large,
|
||||
medications.intake.force_slot.invalid, medications.intake.force_slot.occupied,
|
||||
medications.intake.injection_site.disallowed, medications.intake.taken_at.before_start,
|
||||
medications.mirror.limit_exceeded; mentalHealth — mentalHealth.rateLimited; module — module.disabled;
|
||||
modules — modules.body.invalid_json, modules.invalid; mood — mood.bulk-delete.invalid,
|
||||
mood.bulk.invalid, mood.bulk.too_large, mood.create.external_id_not_delegable, mood.create.invalid,
|
||||
mood.duplicate_timestamp, mood.linkedContext.invalid, mood.list.invalid, mood.not_found,
|
||||
mood.ratedFactor.out_of_range, mood.restore.invalid, mood.update.invalid; notification-prefs —
|
||||
notification-prefs.body.invalid_json; notification — notification.action.already_used,
|
||||
notification.action.expired, notification.action.invalid, notification.action.module_disabled; nutrient
|
||||
— nutrient.batch.invalid, nutrient.batch.too_large, nutrient.daily.invalid, nutrient.read.invalid,
|
||||
nutrient.water.invalid, nutrient.water.invalid_day; onboarding — onboarding.answers.invalid,
|
||||
onboarding.answers.rateLimited, onboarding.complete.invalid, onboarding.complete.recordNotFound,
|
||||
onboarding.complete.recordTargetMismatch, onboarding.disclaimer.invalid, onboarding.restart.invalid,
|
||||
onboarding.restart.rateLimited, onboarding.tour.invalid; passkey — passkey.challenge.expired,
|
||||
passkey.response.invalid, passkey.unknown, passkey.verification.failed; personal_records —
|
||||
personal_records.invalid_query; practitioner — practitioner.invalid; profile —
|
||||
profile.update.emailInUse, profile.update.emailRateLimited, profile.update.emailSecondFactorRequired,
|
||||
profile.update.invalidBody, profile.update.nothingSaved; profile_claim — profile_claim.invalid,
|
||||
profile_claim.oidc_only_unsupported, profile_claim.taken; record_write — record_write.rate_limited;
|
||||
report-selection — report-selection.body.invalid_json, report-selection.body.invalid_shape,
|
||||
report-selection.leaves.unknown; route — route.retired; search — search.invalid; share-link —
|
||||
share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited,
|
||||
share-link.documents.invalid, share-link.report.notFound, share-link.report.rateLimited,
|
||||
share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; sharing —
|
||||
sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, sharing.accept.revoked,
|
||||
sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid, sharing.invite.invalid_scope,
|
||||
sharing.invite.manage_browser_only, sharing.invite.refused, sharing.invite.self, sharing.not_permitted,
|
||||
sharing.renounce.already_ended, sharing.renounce.not_found, sharing.revoke.already_ended,
|
||||
sharing.revoke.not_found, sharing.session.changed, sharing.switch.invalid,
|
||||
medications.mirror.limit_exceeded, medications.notFound; mentalHealth — mentalHealth.rateLimited; module
|
||||
— module.disabled; modules — modules.body.invalid_json, modules.invalid; mood —
|
||||
mood.bulk-delete.invalid, mood.bulk.invalid, mood.bulk.too_large, mood.create.external_id_not_delegable,
|
||||
mood.create.invalid, mood.duplicate_timestamp, mood.linkedContext.invalid, mood.list.invalid,
|
||||
mood.not_found, mood.ratedFactor.out_of_range, mood.restore.invalid, mood.update.invalid;
|
||||
notification-prefs — notification-prefs.body.invalid_json; notification —
|
||||
notification.action.already_used, notification.action.expired, notification.action.invalid,
|
||||
notification.action.module_disabled; nutrient — nutrient.batch.invalid, nutrient.batch.too_large,
|
||||
nutrient.daily.invalid, nutrient.read.invalid, nutrient.water.invalid, nutrient.water.invalid_day;
|
||||
onboarding — onboarding.answers.invalid, onboarding.answers.rateLimited, onboarding.complete.invalid,
|
||||
onboarding.complete.recordNotFound, onboarding.complete.recordTargetMismatch,
|
||||
onboarding.disclaimer.invalid, onboarding.restart.invalid, onboarding.restart.rateLimited,
|
||||
onboarding.tour.invalid; passkey — passkey.challenge.expired, passkey.response.invalid, passkey.unknown,
|
||||
passkey.verification.failed; personal_records — personal_records.invalid_query; practitioner —
|
||||
practitioner.invalid; profile — profile.update.emailInUse, profile.update.emailRateLimited,
|
||||
profile.update.emailSecondFactorRequired, profile.update.invalidBody, profile.update.nothingSaved;
|
||||
profile_claim — profile_claim.invalid, profile_claim.oidc_only_unsupported, profile_claim.taken;
|
||||
record_write — record_write.rate_limited; report-selection — report-selection.body.invalid_json,
|
||||
report-selection.body.invalid_shape, report-selection.leaves.unknown; route — route.retired; search —
|
||||
search.invalid; share-link — share-link.document.decryptFailed, share-link.document.notFound,
|
||||
share-link.document.rateLimited, share-link.documents.invalid, share-link.report.notFound,
|
||||
share-link.report.rateLimited, share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf;
|
||||
sharing — sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending,
|
||||
sharing.accept.revoked, sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid,
|
||||
sharing.invite.invalid_scope, sharing.invite.manage_browser_only, sharing.invite.refused,
|
||||
sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended, sharing.renounce.not_found,
|
||||
sharing.revoke.already_ended, sharing.revoke.not_found, sharing.session.changed, sharing.switch.invalid,
|
||||
sharing.switch.wrong_transport; source-priority — source-priority.body.invalid_json,
|
||||
source-priority.body.invalid_shape; symptoms — symptoms.definition.limitReached,
|
||||
symptoms.definition.notFound, symptoms.episode.notOpen, symptoms.event.notFound, symptoms.invalid;
|
||||
@@ -35625,7 +35703,7 @@ components:
|
||||
capability:
|
||||
description: "On an AI refusal: the capability that is unavailable, one of the keys of `AiCapabilities` (`coach`,
|
||||
`briefing`, `periodNarrative`, `statusText`, `workoutInsights`, `reactionLines`, `aboutMeQuestions`,
|
||||
`documentAi`, `labsOcr`, `medicationExtract`)."
|
||||
`documentAi`, `labsOcr`, `medicationExtract`, `supplementLabel`)."
|
||||
type: string
|
||||
reason:
|
||||
description: "On an AI refusal: the outermost reason, one of `AiUnavailableReason`. The code maps from it:
|
||||
@@ -79968,6 +80046,9 @@ components:
|
||||
medicationExtract:
|
||||
description: The "medicationExtract" capability. Covered by the operator's "documentAi" switch.
|
||||
$ref: "#/components/schemas/AiCapabilityState"
|
||||
supplementLabel:
|
||||
description: The "supplementLabel" capability. Covered by the operator's "documentAi" switch.
|
||||
$ref: "#/components/schemas/AiCapabilityState"
|
||||
required:
|
||||
- coach
|
||||
- briefing
|
||||
@@ -79979,6 +80060,7 @@ components:
|
||||
- documentAi
|
||||
- labsOcr
|
||||
- medicationExtract
|
||||
- supplementLabel
|
||||
additionalProperties: false
|
||||
description: Every AI capability, always all present. `coach` — chat, fenced document chat, attachments, Coach memory
|
||||
upkeep, AI nudges, every Coach launcher. `briefing` — the daily briefing and every place its text is lifted
|
||||
@@ -79987,7 +80069,8 @@ components:
|
||||
paragraph on a workout. `reactionLines` — the line written after a new reading. `aboutMeQuestions` —
|
||||
model-written follow-up questions on the about-me profile (a deterministic set stays). `documentAi` — suggest,
|
||||
summary, extract, index and chat over a stored document. `labsOcr` — reading a lab report image.
|
||||
`medicationExtract` — turning a typed medication description into a schedule.
|
||||
`medicationExtract` — turning a typed medication description into a schedule. `supplementLabel` (v1.43) —
|
||||
reading a supplement's label photo into its ingredients; unavailable on the `frugal` usage level.
|
||||
AiProviderState:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user