diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 68054df07..b22647ae1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,17 +24,6 @@ updates: - "patch" - "minor" ignore: - # zod 4.5 made `z.iso.datetime()` require seconds, so a request carrying - # `2026-01-01T06:15Z` that passes today would be refused with a 422 on - # every route that validates a timestamp, and 4.6 rewrites nullable and - # primitive unions in the generated JSON Schema from `anyOf` to `type` - # arrays, which reshapes the OpenAPI document and every MCP tool schema. - # Both are contract changes for the web client, the iOS app and MCP - # callers, not a routine bump, so the move past 4.4 gets its own change - # with the timestamp producers audited. Patch releases on 4.4 still - # arrive. - - dependency-name: "zod" - versions: [">=4.5.0"] # Next 16.4.0 breaks URL state after history traversal: with a sheet # opened through `?doc=`, Back leaves it open, and `[` / `]` sometimes # do not move `?day=`. Reproduced in three end-to-end journeys (3 of 40 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index f735b0d4e..e2c9ca555 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1700,7 +1700,7 @@ paths: which widens the export instead of erroring. Validate on your side. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive lower bound. Parsed with the plain Date constructor, so an ISO date (`2026-07-14`) works as well as a full instant — and an UNPARSEABLE value is silently DROPPED from the filter rather than refused, which widens the export instead of erroring. Validate on your side. @@ -1710,7 +1710,7 @@ paths: description: Inclusive upper bound, with the same forgiving parse as `since`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive upper bound, with the same forgiving parse as `since`. - in: query name: granularity @@ -1775,7 +1775,7 @@ paths: which widens the export instead of erroring. Validate on your side. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive lower bound. Parsed with the plain Date constructor, so an ISO date (`2026-07-14`) works as well as a full instant — and an UNPARSEABLE value is silently DROPPED from the filter rather than refused, which widens the export instead of erroring. Validate on your side. @@ -1785,7 +1785,7 @@ paths: description: Inclusive upper bound, with the same forgiving parse as `since`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive upper bound, with the same forgiving parse as `since`. responses: "200": @@ -1830,7 +1830,7 @@ paths: which widens the export instead of erroring. Validate on your side. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive lower bound. Parsed with the plain Date constructor, so an ISO date (`2026-07-14`) works as well as a full instant — and an UNPARSEABLE value is silently DROPPED from the filter rather than refused, which widens the export instead of erroring. Validate on your side. @@ -1840,7 +1840,7 @@ paths: description: Inclusive upper bound, with the same forgiving parse as `since`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive upper bound, with the same forgiving parse as `since`. responses: "200": @@ -4111,7 +4111,7 @@ paths: description: Window start (ISO-8601 instant). type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: true description: Window start (ISO-8601 instant). - in: query @@ -4120,7 +4120,7 @@ paths: description: Window end (ISO-8601 instant). type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: true description: Window end (ISO-8601 instant). responses: @@ -4283,13 +4283,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: limit schema: @@ -4512,7 +4512,7 @@ paths: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ notes: anyOf: - type: string @@ -4804,13 +4804,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: until schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: sportType schema: @@ -5130,13 +5130,13 @@ paths: description: When the dose was taken. Defaults to now. Ignored unless the status is `taken`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ snoozedUntil: description: How long to defer. Defaults to 30 minutes from now. Deliberately UNBOUNDED — nothing here caps how far out it may sit. Ignored unless the status is `snoozed`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ injectionSite: description: Site recorded alongside a `taken` toggle, validated server-side against the medication's effective allowed set. Dropped for a non-injection medication, for one with site tracking off, and for a skip or @@ -5157,7 +5157,7 @@ paths: for a skip or snooze. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - intakeId - status @@ -5276,7 +5276,7 @@ paths: effectiveFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ doseValue: type: number minimum: 0 @@ -5547,13 +5547,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: limit schema: @@ -6963,7 +6963,7 @@ paths: placeholder for such a slot is not returned. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Window start (inclusive). Defaults to 90 days before `to`; clamped to a 366-day span floor. Recorded doses are returned from this instant. A slot from before the medication's `createdAt` appears only when a recorded row (a take, a skip, or a miss carried in with restored history) claims it; the server's own placeholder for @@ -6975,7 +6975,7 @@ paths: time within a day after `to`) is still returned. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Window end (inclusive). Defaults to now. Must be ≥ `from`. A dose recorded ahead of its slot (the slot's time within a day after `to`) is still returned. responses: @@ -10348,7 +10348,7 @@ paths: parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ responses: "200": description: The effective (trimmed) state echoed back plus the freshly derived pending questions and the advanced @@ -10509,9 +10509,9 @@ paths: pattern: ^\d{4}-(0[1-9]|1[0-2])$ - type: "null" secondhandSmoke: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" required: - schema - products @@ -10605,9 +10605,9 @@ paths: - heavy_physical - type: "null" nightWork: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" exposures: maxItems: 5 type: array @@ -10680,13 +10680,13 @@ paths: type: number const: 1 napRegularly: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" snoringReported: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" dietPattern: anyOf: - type: string @@ -15260,13 +15260,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ - in: query name: limit schema: @@ -15842,13 +15842,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: status schema: @@ -16007,7 +16007,7 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: true responses: "200": @@ -16483,13 +16483,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: limit schema: @@ -16732,7 +16732,7 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: true responses: "200": @@ -16839,13 +16839,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: sortDir schema: @@ -23922,13 +23922,13 @@ paths: schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: to schema: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - in: query name: definitionId schema: @@ -25337,9 +25337,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" categoryKey: type: string const: custom @@ -25357,9 +25357,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" isActive: type: boolean description: "Partial custom-symptom edit; at least one of `label` / `icon` / `isActive` is required. `isActive: false` @@ -25420,9 +25420,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: type: string enum: @@ -25471,7 +25471,7 @@ components: loggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: default: MANUAL type: string @@ -25554,9 +25554,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: anyOf: - type: string @@ -25613,7 +25613,7 @@ components: loggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Partial day-log edit. Every field optional; `note` re-encrypts (explicit null clears it). `date` / `source` / `externalId` are immutable on update. CycleDayLogBulkRequest: @@ -25678,9 +25678,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: type: string enum: @@ -25729,7 +25729,7 @@ components: loggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: default: MANUAL type: string @@ -25765,7 +25765,7 @@ components: loggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - action - date @@ -25833,11 +25833,11 @@ components: startDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ days: type: integer minimum: 1 @@ -25895,12 +25895,12 @@ components: rangeStart: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ rangeEnd: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" selection: type: object @@ -25931,7 +25931,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - label - rangeStart @@ -26003,7 +26003,7 @@ components: type: string maxLength: 254 format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ + pattern: ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ password: type: string minLength: 12 @@ -26483,7 +26483,7 @@ components: email: type: string format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ + pattern: ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ username: type: string minLength: 3 @@ -26531,7 +26531,7 @@ components: anyOf: - type: string format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ + pattern: ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - type: "null" heightCm: anyOf: @@ -26540,9 +26540,9 @@ components: maximum: 300 - type: "null" dateOfBirth: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" gender: description: Stored gender. Null or an empty string clears it; an omitted field is left untouched. anyOf: @@ -26599,15 +26599,15 @@ components: - type: "null" insuranceNumber: description: German KVNR. Empty/null clears it; mod-10 check enforced. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerIkNumber: description: German insurer institution number (IKNR). Optional; empty/null clears it. A non-empty value must be exactly 9 digits (no checksum enforced). - anyOf: - - type: string - - type: "null" + type: + - string + - "null" currentPassword: description: "The account password, sent only alongside a NEW `email`. Moving the account to a new address needs a fresh proof because single sign-on links an existing account by it: a browser session that signed in or completed @@ -26748,7 +26748,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ notes: type: string maxLength: 200 @@ -26799,15 +26799,15 @@ components: startDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ value: type: number externalId: @@ -26906,11 +26906,11 @@ components: startDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endDate: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ sleepStage: description: HKCategoryValueSleepAnalysis codepoint; only for sleep samples. type: integer @@ -27072,11 +27072,11 @@ components: startedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ totalEnergyKcal: type: number minimum: 0 @@ -27150,6 +27150,7 @@ components: maximum: 90 items: type: number + minItems: 2 required: - type - coordinates @@ -27161,7 +27162,7 @@ components: t: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ speedMs: type: number minimum: 0 @@ -27184,7 +27185,7 @@ components: t: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ hr: type: integer minimum: 20 @@ -27369,7 +27370,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ notes: anyOf: - type: string @@ -27389,14 +27390,14 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipped: type: boolean scheduledFor: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ forceSlotInstant: description: "Late-take override on edit: pin the edited dose onto the named scheduled slot instead of re-attributing by window band. Must be a real scheduled slot of this medication on its day (validated server-side); a non-slot @@ -27405,7 +27406,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Edit a single intake event. v1.15.18 re-runs window-band slot attribution whenever `takenAt` or `skipped` change, snapping `scheduledFor` to the matched slot (or the take's own time when it falls in no window). @@ -27486,12 +27487,12 @@ components: description: ISO instant; defaults to `takenAt` then now() when omitted. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: description: ISO instant of the take; omit + `skipped:false` = pending. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ skipped: description: Default false. type: boolean @@ -27515,7 +27516,7 @@ components: description: Pin a taken entry onto a named real scheduled slot; ignored on non-taken entries. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ doseTaken: description: Per-entry dose override; persisted only on a taken entry. type: string @@ -27955,7 +27956,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version description: PUT body for the medications list presentation — the presentation fields plus the optional @@ -28151,13 +28152,13 @@ components: logic can pin the dose to a schedule slot. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: description: When the dose was actually taken. NULL when `skipped` is true; defaults to `now()` for non-skipped intakes. Must not lie in the future (5-minute clock-skew allowance) nor more than 5 years in the past. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ skipped: default: false description: True to log a skipped slot (no consumption, no inventory decrement, one-shot medications stay active). @@ -28189,7 +28190,7 @@ components: attribution." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ doseTaken: description: Dose actually consumed for THIS intake when it deviates from (or documents) the medication's configured dose, e.g. a half tablet or a titration step. Free text, max 50 characters. Omit to record the take under @@ -28229,13 +28230,13 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" purchasedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" manufacturer: description: Marketing-authorisation holder / maker as printed on the carton. Free text, max 120 chars. @@ -28270,7 +28271,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" markAsUsedUp: description: "Terminal override: zero the remaining units and mark the container USED_UP (physically discarded)." @@ -28279,7 +28280,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" unitsRemaining: description: Absolute remaining-unit correction (0–1000, fractional allowed). Clamped server-side to the item's @@ -28438,12 +28439,12 @@ components: validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Inclusive start instant of the manual era (ISO 8601). validUntil: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Exclusive end instant of the manual era (ISO 8601). Must precede the current plan. timesOfDay: description: Daily dose times (HH:mm, user local) the era ran at. 1–8 entries; duplicates are collapsed. @@ -28543,37 +28544,37 @@ components: const: "" - type: "null" model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" baseUrl: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" compatBaseUrl: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" compatModel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" compatKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" anthropicKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" localKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" openaiKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" responseTimeoutSeconds: anyOf: - type: integer @@ -28717,7 +28718,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Coach prompt-tuning preferences to persist, plus the optional optimistic-concurrency `baseUpdatedAt` token. The guarded write covers both `coachPrefsJson` and the mirrored `insightsExcludeMetrics` column atomically. Omit the token for the legacy unconditional write. @@ -28759,7 +28760,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - pillars additionalProperties: false @@ -28824,7 +28825,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ additionalProperties: false description: Partial module enable/disable update. Each key is an optional boolean; an omitted key is left untouched, `false` disables the module, `true` (re-)enables it. Only the toggleable "secondary domains" are accepted — a @@ -28907,7 +28908,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "Partial per-user notification preferences. Every category and field is optional; the server deep-merges the supplied keys over the persisted row, so a PATCH touching only `medication` leaves the siblings intact. `medication.lowStockRunwayDays` (1–60, nullable) is the low-stock alert threshold in remaining runway days — @@ -29629,7 +29630,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version description: PUT body for the Insights layout — the layout fields plus the optional optimistic-concurrency base token @@ -29743,7 +29744,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" additionalProperties: false description: "v1.21.3 — confirm or update a Coach plan's lifecycle. `status` moves a `proposed` plan to `active` @@ -30112,7 +30113,7 @@ components: recordedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "When the strip was recorded on-device, with offset. Part of the recording's identity — see `status: duplicate`." samplingFrequency: @@ -30286,7 +30287,7 @@ components: moodLoggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: ISO instant the entry was logged. source: description: Defaults to MANUAL. @@ -30456,7 +30457,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" note: description: One free-text note for the whole context. AES-256-GCM at rest; the ciphertext never rides the wire. @@ -30475,9 +30476,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" categoryKey: type: string minLength: 1 @@ -30496,9 +30497,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" isActive: type: boolean categoryKey: @@ -30524,9 +30525,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - label description: Create a per-user custom mood-tag group. `label` is encrypted at rest; `icon` must come from the curated @@ -30539,9 +30540,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" isActive: type: boolean description: Partial custom-group edit; at least one field required. `isActive:false` retires the group without touching @@ -30577,7 +30578,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Mood-tag layout to merge (preserve-when-absent), plus the optional optimistic-concurrency `baseUpdatedAt` token. The two manage cards write different fields of the same blob; echo the token to have a stale write refused with 409 instead of resurrecting the other card's overwritten field. Omit it for the legacy @@ -30797,7 +30798,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" note: anyOf: @@ -30808,7 +30809,7 @@ components: moodLoggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: default: MANUAL type: string @@ -31055,7 +31056,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" note: anyOf: @@ -31066,7 +31067,7 @@ components: moodLoggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "Per-field mood-entry edit. Omitted fields keep the stored value. `tagKeys`, `ratedFactors` and `context` replace their whole set when present (`null` clears). `a2`-`a5`: explicit null clears; `a1` null re-derives from the label. Editing `mood` without `a1` re-derives pleasantness the same way it re-derives `score`. Editing @@ -31229,7 +31230,7 @@ components: - type: string maxLength: 254 format: email - pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ + pattern: ^(?:[A-Za-z0-9_'+\-]+\.)*[A-Za-z0-9_'+\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$ - type: string const: "" enabled: @@ -31267,7 +31268,7 @@ components: signedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - kind - artefact @@ -31316,7 +31317,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "Bounded: no future instants beyond a 5-min clock-skew tolerance, nothing before 1900." glucoseContext: description: Optional even on a BLOOD_GLUCOSE row — a sensor export classifies nothing per sample. @@ -31375,7 +31376,7 @@ components: re-import of the same file dedups instead of duplicating. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ externalId: description: "Source-stable id (e.g. a Daylio row id) making re-import idempotent (upsert on `(userId, source=IMPORT, externalId)`). Same stability rule as the measurement `externalId`: an unstable id skips the row." @@ -31418,7 +31419,7 @@ components: pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$ - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" notifyHour: default: 9 @@ -31496,7 +31497,7 @@ components: pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$ - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" notifyHour: type: integer @@ -31569,7 +31570,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ note: anyOf: - type: string @@ -31622,21 +31623,21 @@ components: minLength: 1 maxLength: 40 referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceText: anyOf: - anyOf: @@ -31648,7 +31649,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ note: anyOf: - anyOf: @@ -31728,7 +31729,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ biomarkerId: type: string minLength: 1 @@ -31799,13 +31800,13 @@ components: minLength: 1 maxLength: 40 lowerBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" upperBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" context: anyOf: - type: string @@ -31858,7 +31859,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ status: default: DONE type: string @@ -31951,7 +31952,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ status: type: string enum: @@ -32252,7 +32253,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ antigenSlug: anyOf: - type: string @@ -32344,7 +32345,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ antigenSlug: anyOf: - type: string @@ -32496,12 +32497,12 @@ components: onsetAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ resolvedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" parentConditionId: anyOf: @@ -32562,12 +32563,12 @@ components: onsetAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ resolvedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" parentConditionId: anyOf: @@ -32604,7 +32605,7 @@ components: resolvedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Mark an episode recovered. `resolvedAt` defaults to 'now' when omitted, so the one-tap 'mark recovered' affordance can send an empty body. A CHRONIC_ONGOING episode has no recovery date by design and 422s. UpsertIllnessDayLogRequest: @@ -32649,7 +32650,7 @@ components: loggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - date description: Upsert one day's symptom / functional-impact / fever row for an episode (keyed on `(episodeId, date)`). @@ -32680,7 +32681,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ tz: type: string maxLength: 64 @@ -32746,7 +32747,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" reaction: anyOf: @@ -32801,7 +32802,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" reaction: anyOf: @@ -33139,9 +33140,9 @@ components: - ATC - type: "null" value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: anyOf: - type: string @@ -33153,13 +33154,13 @@ components: maxLength: 80 - type: "null" referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceText: anyOf: - type: string @@ -33293,13 +33294,13 @@ components: minLength: 1 maxLength: 40 targetLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" targetHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" decimals: anyOf: - type: integer @@ -33324,7 +33325,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ note: anyOf: - type: string @@ -33344,7 +33345,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ note: anyOf: - anyOf: @@ -33733,7 +33734,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ required: - lastStopId - status @@ -34401,7 +34402,7 @@ components: an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version description: PUT body for the dashboard layout — the layout fields plus the optional optimistic-concurrency base token @@ -34482,7 +34483,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" access: default: READ @@ -34650,7 +34651,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" required: - identifier @@ -35451,7 +35452,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ idempotencyKey: type: string maxLength: 128 @@ -35486,9 +35487,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - label description: Define a symptom of your own. `label` (1-40 characters) is encrypted at rest; `icon` is one of the cycle @@ -35502,9 +35503,9 @@ components: minLength: 1 maxLength: 40 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" isActive: type: boolean sortOrder: @@ -35523,7 +35524,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ intensity: type: integer minimum: 0 @@ -35551,7 +35552,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ intensity: type: integer minimum: 0 @@ -35717,7 +35718,7 @@ components: baseUpdatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - sections additionalProperties: false @@ -35751,10 +35752,13 @@ components: - type: integer minimum: 0 maximum: 4 + items: false + minItems: 3 + maxItems: 3 takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tz: type: string minLength: 1 @@ -35874,32 +35878,32 @@ components: description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dose: - anyOf: - - type: string - - type: "null" description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. + type: + - string + - "null" unitsPerDose: - anyOf: - - type: number - - type: "null" description: v1.37.10 (#219) — per-slot inventory-units override (may be a split-pill fraction). NULL means the schedule inherits the medication-level `unitsPerDose`. Kept raw so an edit surface can distinguish an explicit value from inheritance; consumers wanting the effective figure read `resolvedUnitsPerDose`. + type: + - number + - "null" resolvedUnitsPerDose: type: number description: v1.37.19 — the EFFECTIVE units one dose of this slot consumes, resolved server-side (`schedule.unitsPerDose ?? medication.unitsPerDose`, matching the intake consumption resolver). Always present; clients never re-derive the inheritance rule. daysOfWeek: - anyOf: - - type: string - - type: "null" description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first; the field is kept for pre-v1.5 rows. v1.6.0 drops the column. + type: + - string + - "null" timesOfDay: type: array items: @@ -35914,12 +35918,12 @@ components: - type: "null" description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span. rrule: - anyOf: - - type: string - - type: "null" description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with `rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on legacy rows that haven't been touched since the migration). + type: + - string + - "null" rollingIntervalDays: anyOf: - type: integer @@ -36005,17 +36009,17 @@ components: type: string description: The minted `custom:` key. Stable, and the value a day-log's symptom list carries for this symptom. label: - anyOf: - - type: string - - type: "null" description: The decrypted label. Null when the stored ciphertext could not be decrypted — one unreadable row fails soft rather than taking the whole catalogue read down, and the client falls back to the generic label. + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" description: Lucide icon name from the closed allow-list, or null. The allow-list is limited to names the iOS client maps to an SF Symbol, so a custom symptom never falls back to the generic glyph on one platform. + type: + - string + - "null" custom: type: boolean const: true @@ -36276,17 +36280,17 @@ components: type: string description: The minted `custom:` key. Stable, and the value a day-log's symptom list carries for this symptom. label: - anyOf: - - type: string - - type: "null" description: The decrypted label. Null when the stored ciphertext could not be decrypted — one unreadable row fails soft rather than taking the whole catalogue read down, and the client falls back to the generic label. + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" description: Lucide icon name from the closed allow-list, or null. The allow-list is limited to names the iOS client maps to an SF Symbol, so a custom symptom never falls back to the generic glyph on one platform. + type: + - string + - "null" custom: type: boolean const: true @@ -36365,9 +36369,9 @@ components: date: type: string cycleId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" flow: anyOf: - $ref: "#/components/schemas/FlowLevel" @@ -36375,9 +36379,9 @@ components: intermenstrualBleeding: type: boolean basalBodyTempC: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" temperatureExcluded: type: boolean ovulationTest: @@ -36412,9 +36416,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: anyOf: - $ref: "#/components/schemas/HomeTestResult" @@ -36424,9 +36428,9 @@ components: - $ref: "#/components/schemas/HomeTestResult" - type: "null" contraceptive: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" symptoms: type: array items: @@ -36445,15 +36449,15 @@ components: - severity additionalProperties: false note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" source: type: string externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -36461,12 +36465,12 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ deletedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" required: - id @@ -36623,9 +36627,9 @@ components: type: object properties: removedCycleId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" restored: type: array items: @@ -36758,13 +36762,13 @@ components: startDate: type: string endDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" periodEndDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lengthDays: anyOf: - type: integer @@ -36772,9 +36776,9 @@ components: maximum: 9007199254740991 - type: "null" ovulationDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" ovulationConfirmed: type: boolean isPredicted: @@ -36786,7 +36790,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - startDate @@ -36877,9 +36881,9 @@ components: confidence: type: number basalBodyTempC: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" temperatureExcluded: type: boolean ovulationTest: @@ -36927,9 +36931,9 @@ components: - $ref: "#/components/schemas/HomeTestResult" - type: "null" contraceptive: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasNote: type: boolean description: Whether the day carries a note. The note text is only on the day-log read. @@ -36966,7 +36970,7 @@ components: generatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - generatedAt additionalProperties: false @@ -37011,17 +37015,17 @@ components: nextPeriodStartHigh: type: string fertileWindowStart: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fertileWindowEnd: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" predictedOvulation: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" ovulationConfirmed: type: boolean confidence: @@ -37100,11 +37104,11 @@ components: - fraction additionalProperties: false cycleStartDate: - anyOf: - - type: string - - type: "null" description: First day of the current cycle (YYYY-MM-DD). Still set while OVERDUE — the last logged period start remains a fact once the count stops. + type: + - string + - "null" overdueDays: anyOf: - type: integer @@ -37124,13 +37128,13 @@ components: type: object properties: start: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" end: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" active: type: boolean required: @@ -37178,9 +37182,9 @@ components: maximum: 9007199254740991 - type: "null" lengthVariabilityDays: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avgPeriodLengthDays: anyOf: - type: integer @@ -37559,7 +37563,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - goal - cycleTrackingEnabled @@ -37871,10 +37875,10 @@ components: type: string description: Release the removal shipped in, without the `v`. replacedBy: - anyOf: - - type: string - - type: "null" description: Where the capability went, or null when it went nowhere. + type: + - string + - "null" reason: type: string description: Why it was removed, in one sentence, in English. @@ -37932,16 +37936,16 @@ components: description: The running build's version. From the image build arg when present, falling back to package.json for a local dev run. buildSha: - anyOf: - - type: string - - type: "null" description: Short Git SHA baked at image build time. Null for a local dev run, which is how a client tells a released image from one. + type: + - string + - "null" builtAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: ISO-8601 build timestamp. Null for a local dev run. license: @@ -38072,13 +38076,13 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" checked_at: description: When the check ran. Absent when `unknown`. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ reason: description: "Only on `unknown`: `network_error`, or `github_status_` when the feed answered but not with a success." @@ -38102,9 +38106,9 @@ components: rangeStart: type: string rangeEnd: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" protected: type: boolean documentCount: @@ -38118,13 +38122,13 @@ components: createdAt: type: string revokedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastAccessAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" accessCount: type: number active: @@ -38192,9 +38196,9 @@ components: rangeStart: type: string rangeEnd: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" protected: type: boolean documentCount: @@ -38208,13 +38212,13 @@ components: createdAt: type: string revokedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastAccessAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" accessCount: type: number active: @@ -38384,39 +38388,39 @@ components: webPushGlobal: type: boolean webPushVapidPublicKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" webPushVapidPrivateKeyEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" webPushVapidSubject: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" apiGlobal: type: boolean umamiEnabled: type: boolean umamiScriptUrl: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" umamiWebsiteId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glitchtipEnabled: type: boolean glitchtipDsn: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glitchtipEnvironment: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reminderLateMinutes: type: integer minimum: -9007199254740991 @@ -38426,25 +38430,25 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 adminAiKeyEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" adminAiModel: type: string adminAiBaseUrl: type: string adminCodexAccessTokenEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" adminCodexRefreshTokenEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" adminCodexAccountIdEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" adminCodexTokenExpiresAt: anyOf: - type: string @@ -38462,9 +38466,9 @@ components: - {} - type: "null" defaultUserTimezone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" assistantEnabled: type: boolean assistantCoachEnabled: @@ -38540,78 +38544,78 @@ components: - type: object properties: heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dateOfBirth: anyOf: - type: string minLength: 1 - type: "null" gender: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasDiabetes: type: boolean displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insuranceNumber: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insuranceNumberEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerIkNumber: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastReportPracticeName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" avatarBytes: anyOf: - type: string minLength: 1 - type: "null" avatarContentType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" avatarUpdatedAt: anyOf: - type: string minLength: 1 - type: "null" homeLat: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" homeLon: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" homeLabel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" homeTimezone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" homeSince: anyOf: - type: string @@ -38627,17 +38631,17 @@ components: timezone: type: string locale: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unitPreference: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glucoseUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timeFormat: type: string dateFormat: @@ -38661,25 +38665,25 @@ components: moodReminderEnabled: type: boolean aiProvider: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiModel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiBaseUrl: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiCompatBaseUrl: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiCompatModel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiProviderChain: {} aiResponseTimeoutSeconds: anyOf: @@ -38698,9 +38702,9 @@ components: disableCoach: type: boolean aiUsageLevel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" coachPrefsJson: {} documentsAutoAiRead: type: boolean @@ -38720,9 +38724,9 @@ components: minLength: 1 - type: "null" disclaimerAcknowledgedVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" passkeyUpgradeNudgeDismissed: type: boolean additionalProperties: {} @@ -38819,13 +38823,13 @@ components: value: type: number valueMin: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" unit: type: string minLength: 1 @@ -38852,22 +38856,22 @@ components: - EXTERNAL - HEALTH_CONNECT notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notesEncrypted: anyOf: - type: string minLength: 1 - type: "null" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aggregationProvenance: anyOf: - type: string @@ -38908,9 +38912,9 @@ components: - FIRED - type: "null" deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -39026,13 +39030,13 @@ components: criticalAlarmEnabled: type: boolean atcCode: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" rxNormCode: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lowStockNotifiedAt: anyOf: - type: string @@ -39061,9 +39065,9 @@ components: - APPLE_HEALTH - type: "null" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -39086,23 +39090,22 @@ components: type: string minLength: 1 label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dose: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unitsPerDose: - anyOf: - - anyOf: - - type: string - - type: number - - type: "null" + type: + - string + - number + - "null" daysOfWeek: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timesOfDay: type: array items: @@ -39114,9 +39117,9 @@ components: maximum: 9007199254740991 - type: "null" rrule: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" rollingIntervalDays: anyOf: - type: integer @@ -39198,9 +39201,9 @@ components: minimum: 1 maximum: 5 notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notesEncrypted: anyOf: - type: string @@ -39260,9 +39263,9 @@ components: minLength: 1 maxLength: 8 enteredAmount: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" enteredUnit: anyOf: - type: string @@ -39301,9 +39304,9 @@ components: type: string minLength: 1 note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string @@ -39369,17 +39372,17 @@ components: minLength: 1 - type: "null" manufacturer: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" doseStrength: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notesEncrypted: anyOf: - type: string @@ -39715,9 +39718,9 @@ components: - IMPORT - APPLE_HEALTH idempotencyKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -39735,17 +39738,17 @@ components: - UPPER_ARM_RIGHT - type: "null" doseTaken: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" inventoryConsumption: anyOf: - {} - type: "null" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" updatedAt: type: string minLength: 1 @@ -39812,15 +39815,13 @@ components: maximum: 10 - type: "null" tags: - anyOf: - - anyOf: - - type: "null" - - type: string - - type: "null" + type: + - "null" + - string note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string @@ -39830,16 +39831,16 @@ components: type: string minLength: 1 externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" loggedAt: type: string minLength: 1 tz: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncedAt: type: string minLength: 1 @@ -39886,9 +39887,9 @@ components: - type: object properties: workStatus: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" workMinutes: anyOf: - type: integer @@ -39914,17 +39915,17 @@ components: maximum: 9007199254740991 - type: "null" contactCircles: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" contactForm: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" contactExtent: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" contactQuality: anyOf: - type: integer @@ -39938,9 +39939,9 @@ components: maximum: 9007199254740991 - type: "null" leisureCategories: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" leisureMinutes: anyOf: - type: integer @@ -39960,9 +39961,9 @@ components: maximum: 9007199254740991 - type: "null" eventType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" eventValence: anyOf: - type: integer @@ -39975,9 +39976,9 @@ components: minLength: 1 - type: "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notesEncrypted: anyOf: - type: string @@ -40010,9 +40011,9 @@ components: - PERIMENOPAUSE - OFF cycleTrackingEnabled: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" typicalCycleLength: anyOf: - type: integer @@ -40067,13 +40068,13 @@ components: type: string minLength: 1 endDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" periodEndDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lengthDays: anyOf: - type: integer @@ -40081,17 +40082,17 @@ components: maximum: 9007199254740991 - type: "null" ovulationDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" ovulationConfirmed: type: boolean isPredicted: type: boolean tz: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -40128,9 +40129,9 @@ components: type: string minLength: 1 cycleId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" flow: anyOf: - type: string @@ -40144,9 +40145,9 @@ components: intermenstrualBleeding: type: boolean basalBodyTempC: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" temperatureExcluded: type: boolean ovulationTest: @@ -40192,9 +40193,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: anyOf: - type: string @@ -40226,17 +40227,17 @@ components: - EMERGENCY - type: "null" sensitiveEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" notesEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sensitive: anyOf: - anyOf: @@ -40245,9 +40246,9 @@ components: sexualActivity: type: boolean protectedSex: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" pregnancyTest: anyOf: - type: string @@ -40301,13 +40302,13 @@ components: - EXTERNAL - HEALTH_CONNECT externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" tz: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -40370,9 +40371,9 @@ components: type: string minLength: 1 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sortOrder: default: 0 type: integer @@ -40382,13 +40383,13 @@ components: default: true type: boolean labelEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - key - labelKey @@ -40412,9 +40413,9 @@ components: type: string minLength: 1 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sortOrder: type: integer minimum: -9007199254740991 @@ -40422,13 +40423,13 @@ components: isActive: type: boolean labelEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - key @@ -40474,22 +40475,22 @@ components: default: true type: boolean icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sortOrder: default: 0 type: integer minimum: -9007199254740991 maximum: 9007199254740991 labelEncrypted: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" scaleMin: default: 1 type: integer @@ -40524,50 +40525,50 @@ components: type: string minLength: 1 panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" analyte: type: string minLength: 1 value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unit: type: string referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sourceValue: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" takenAt: type: string minLength: 1 @@ -40575,17 +40576,17 @@ components: type: string minLength: 1 biomarkerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" biomarkerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -40623,27 +40624,27 @@ components: unit: type: string lowerBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" upperBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hidden: type: boolean analyteKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" context: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aliases: type: array items: @@ -40753,13 +40754,13 @@ components: minLength: 1 - type: "null" parentConditionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -40816,9 +40817,9 @@ components: maximum: 9007199254740991 - type: "null" feverC: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" symptoms: default: [] type: array @@ -40838,9 +40839,9 @@ components: - key additionalProperties: {} note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" updatedAt: type: string minLength: 1 @@ -40850,9 +40851,9 @@ components: minLength: 1 - type: "null" tz: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -40889,9 +40890,9 @@ components: type: string minLength: 1 icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sortOrder: type: integer minimum: -9007199254740991 @@ -40921,9 +40922,9 @@ components: minimum: 0 maximum: 10 note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string @@ -40991,9 +40992,9 @@ components: type: string minLength: 1 note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string @@ -41062,13 +41063,13 @@ components: minLength: 1 - type: "null" reaction: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -41134,9 +41135,9 @@ components: maximum: 9007199254740991 - type: "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -41171,13 +41172,13 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 totalEnergyKcal: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" totalDistanceM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avgHeartRate: anyOf: - type: integer @@ -41203,9 +41204,9 @@ components: maximum: 9007199254740991 - type: "null" elevationM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pauseDurationSec: anyOf: - type: integer @@ -41232,13 +41233,13 @@ components: - EXTERNAL - HEALTH_CONNECT externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" metadata: $ref: "#/components/schemas/__schema0" createdAt: @@ -41277,13 +41278,13 @@ components: - SICK_NOTE - OTHER title: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" filename: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" mimeType: type: string minLength: 1 @@ -41301,13 +41302,13 @@ components: - CONFIRMED - DISCARDED reportDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" documentDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" contentEncrypted: type: string minLength: 1 @@ -41320,13 +41321,13 @@ components: type: string minLength: 1 providerType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" errorReason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" summaryEncrypted: anyOf: - type: string @@ -41351,9 +41352,9 @@ components: minLength: 1 - type: "null" lastIndexOutcome: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sourceSystem: anyOf: - type: string @@ -41375,9 +41376,9 @@ components: aiReadDeferred: type: boolean summary: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -41440,13 +41441,13 @@ components: needsReview: type: boolean committedRecordId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" committedRecordType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dataEncrypted: type: string minLength: 1 @@ -41494,9 +41495,9 @@ components: type: string minLength: 1 externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -41517,24 +41518,24 @@ components: minLength: 1 aboutMe: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" conditions: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" allergies: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" coachFocus: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiIncludedSections: default: - ABOUT_ME @@ -41606,19 +41607,19 @@ components: - type: "null" emergencyContacts: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" emergencyImplants: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" emergencyNote: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" emergencyContactsEncrypted: anyOf: - type: string @@ -41702,9 +41703,9 @@ components: - HEALTH_GOALS value: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" valueEncrypted: type: string minLength: 1 @@ -41756,14 +41757,14 @@ components: minLength: 1 targetLow: default: null - anyOf: - - type: number - - type: "null" + type: + - number + - "null" targetHigh: default: null - anyOf: - - type: number - - type: "null" + type: + - number + - "null" decimals: default: null anyOf: @@ -41773,9 +41774,9 @@ components: - type: "null" description: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" correlationEnabled: default: false type: boolean @@ -41809,9 +41810,9 @@ components: minLength: 1 note: default: null - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string @@ -41905,9 +41906,9 @@ components: - type: "null" dismissedEffectSize: default: null - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dismissedSampleSize: default: null anyOf: @@ -42224,21 +42225,21 @@ components: type: string minLength: 1 specialty: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" practice: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" phone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" locationEncrypted: anyOf: - type: string @@ -42255,9 +42256,9 @@ components: minLength: 1 - type: "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -42307,9 +42308,9 @@ components: - OTHER - PROCEDURE practitionerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reasonEncrypted: anyOf: - type: string @@ -42326,17 +42327,17 @@ components: minLength: 1 - type: "null" reason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" outcome: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" bodySite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" laterality: anyOf: - type: string @@ -42346,9 +42347,9 @@ components: - BOTH - type: "null" reminderId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -42486,13 +42487,13 @@ components: type: string minLength: 1 antigenSlug: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" vaccineName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" doseNumber: anyOf: - type: integer @@ -42506,9 +42507,9 @@ components: maximum: 9007199254740991 - type: "null" lotNumber: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" site: anyOf: - type: string @@ -42522,30 +42523,30 @@ components: - OTHER - type: "null" practitionerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" encounterId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reminderId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" customVaccineId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteEncrypted: anyOf: - type: string minLength: 1 - type: "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -42705,9 +42706,9 @@ components: minLength: 1 - type: "null" summary: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" summaryUpdatedAt: anyOf: - type: string @@ -42741,17 +42742,17 @@ components: content: type: string metricSourceJson: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" providerType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" promptVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" tokensUsed: anyOf: - type: integer @@ -42759,27 +42760,27 @@ components: maximum: 9007199254740991 - type: "null" model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" resultsEncrypted: anyOf: - type: string minLength: 1 - type: "null" resultsJson: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" trailEncrypted: anyOf: - type: string minLength: 1 - type: "null" trailJson: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -42855,15 +42856,15 @@ components: type: string minLength: 1 tz: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" source: type: string externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -42944,9 +42945,9 @@ components: minimum: 0 maximum: 100 sourceConversationId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" source: type: string minLength: 1 @@ -42957,9 +42958,9 @@ components: minLength: 1 - type: "null" sourceMessageId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -43005,18 +43006,18 @@ components: minLength: 1 - type: "null" target: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" outcomeEncrypted: anyOf: - type: string minLength: 1 - type: "null" outcome: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" status: type: string reviewDate: @@ -43025,9 +43026,9 @@ components: minLength: 1 - type: "null" sourceConversationId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -43060,13 +43061,13 @@ components: note: type: string metric: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" relatedPlanId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" triggerKind: type: string dueAt: @@ -43075,18 +43076,18 @@ components: minLength: 1 - type: "null" contextCue: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" status: type: string source: type: string minLength: 1 sourceConversationId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastSurfacedAt: anyOf: - type: string @@ -43214,9 +43215,9 @@ components: maximum: 9007199254740991 - type: "null" rrule: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" anchorDate: anyOf: - type: string @@ -43238,9 +43239,9 @@ components: minimum: 0 maximum: 23 location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" nextDueAt: anyOf: - type: string @@ -43254,9 +43255,9 @@ components: enabled: type: boolean vaccinationAntigen: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" snoozedUntil: anyOf: - type: string @@ -43419,9 +43420,9 @@ components: - WHO5_SCORE - SCI_SCORE metricSlot: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" direction: type: string enum: @@ -43436,9 +43437,9 @@ components: type: string minLength: 1 sourceMeasurementId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" source: type: string enum: @@ -43459,9 +43460,9 @@ components: - EXTERNAL - HEALTH_CONNECT externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string minLength: 1 @@ -43501,17 +43502,17 @@ components: type: string pattern: ^\d{4}-\d{2}-\d{2}$ lat: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" lon: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" locationLabel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" locationEncrypted: anyOf: - type: string @@ -43524,21 +43525,21 @@ components: - TRAVEL - DEVICE tempMin: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" tempMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" tempMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" apparentMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sunshineSec: anyOf: - type: integer @@ -43552,25 +43553,25 @@ components: maximum: 9007199254740991 - type: "null" precipSum: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pressureMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pressureDelta: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" humidityMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" cloudMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" weatherCode: anyOf: - type: integer @@ -43578,85 +43579,85 @@ components: maximum: 9007199254740991 - type: "null" apparentMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pm25Mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pm25Max: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pm10Mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" no2Mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" so2Mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" coMean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" o3Max8h: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" eaqiMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" usaqiMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" uvIndexMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dustMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" aodMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenAlderMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenBirchMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenGrassMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenMugwortMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenOliveMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pollenRagweedMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" aqDomain: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aqHours: anyOf: - type: integer @@ -43697,17 +43698,17 @@ components: type: string pattern: ^\d{4}-\d{2}-\d{2}$ lat: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" lon: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" locationEncrypted: anyOf: - type: string @@ -43776,9 +43777,9 @@ components: minLength: 1 - type: "null" isTravelDay: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" fetchedAt: type: string minLength: 1 @@ -43841,13 +43842,13 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 durationSeconds: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" lead: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" averageHeartRate: anyOf: - type: integer @@ -44052,25 +44053,25 @@ components: type: object properties: displayName: - anyOf: - - type: string - - type: "null" description: The profile's name, as its guardians set it. + type: + - string + - "null" expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ guardians: type: array items: type: object properties: displayName: - anyOf: - - type: string - - type: "null" description: "The guardian's display name; null when they set none. Never a login name or an id: the holder of the link has proved nothing yet." + type: + - string + - "null" proposal: $ref: "#/components/schemas/ProfileClaimGuardianAccess" required: @@ -44089,9 +44090,9 @@ components: - sso ssoProviderName: description: v1.43 — the identity provider's display name for the "Continue with …" button; null without one. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - displayName - expiresAt @@ -44167,14 +44168,14 @@ components: tokenExpiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ refreshToken: description: Refresh token (`hlr_<64hex>`); only present for native-policy callers. type: string refreshTokenExpiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - user additionalProperties: false @@ -44327,7 +44328,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ expiresInSeconds: type: integer minimum: -9007199254740991 @@ -44382,7 +44383,7 @@ components: verifiedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ required: - method - verifiedAt @@ -44574,12 +44575,12 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ lastUsedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" required: - id @@ -44811,13 +44812,13 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Registered at. lastUsedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When this passkey last completed a verified assertion; null when it has never been used to sign in. Stamped on every successful login, so a client can render a real 'last used' rather than assuming never. @@ -44860,25 +44861,25 @@ components: type: string description: Coarse device label derived from the User-Agent. ipMasked: - anyOf: - - type: string - - type: "null" description: IP with the host portion masked — never the full address. + type: + - string + - "null" location: - anyOf: - - type: string - - type: "null" description: Resolved coarse location, when available. + type: + - string + - "null" lastActiveAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ isCurrent: type: boolean description: True for the session making this request. @@ -44957,9 +44958,9 @@ components: username: type: string displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - username @@ -45038,22 +45039,22 @@ components: id: type: string label: - anyOf: - - type: string - - type: "null" description: Coarse, IP-free device label (e.g. 'Firefox on macOS'). + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ lastUsedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ isCurrent: type: boolean description: True for the device making this request. @@ -45147,20 +45148,20 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" ipMasked: - anyOf: - - type: string - - type: "null" description: Host-masked IP. + type: + - string + - "null" carrier: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - action - createdAt @@ -45223,11 +45224,11 @@ components: type: boolean description: An OIDC provider is configured on this instance. buttonLabel: - anyOf: - - type: string - - type: "null" description: Operator-set label for the SSO button (`OIDC_BUTTON_LABEL`, defaulting to “Single Sign-On”). Null when no provider is configured. + type: + - string + - "null" only: type: boolean description: "`OIDC_ONLY=true` — password and passkey login are refused with 403 `oidc_only`. A client that offers those @@ -45361,19 +45362,19 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Stamped fire-and-forget on use; null for an unused token. expiresAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ revoked: type: boolean description: Revoked tokens stay in the list rather than disappearing, so a client must filter on this rather than treat @@ -45417,7 +45418,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - token - name @@ -45453,7 +45454,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - token - name @@ -45489,7 +45490,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - token - name @@ -45547,9 +45548,9 @@ components: username: type: string email: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - username @@ -45611,39 +45612,39 @@ components: username: type: string email: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" role: type: string heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dateOfBirth: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" gender: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timezone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerIkNumber: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasInsuranceNumber: type: boolean description: Whether an insurance number is stored. The number itself is encrypted at rest and is never returned — only @@ -45876,12 +45877,12 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" serverNow: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ measurements: type: object properties: @@ -45889,7 +45890,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer @@ -45911,7 +45912,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer @@ -45933,7 +45934,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" liveCount: type: integer @@ -46004,12 +46005,12 @@ components: serverNow: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ cursor: - anyOf: - - type: string - - type: "null" description: Opaque cursor to echo into the next request. + type: + - string + - "null" hasMore: type: boolean description: False once the client is caught up as of `serverNow`. @@ -46132,55 +46133,55 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: $ref: "#/components/schemas/MeasurementSource" notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" userId: type: string valueMin: description: Bucket MINIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueMax: description: Bucket MAXIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" externalId: - anyOf: - - type: string - - type: "null" description: Cross-device dedup key (UUID string or `stats::`). + type: + - string + - "null" externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glucoseContext: description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor export, a device sync). Never invent one. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sleepStage: description: Set only on SLEEP_DURATION rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" rhythmClassification: description: Device classification verdict; set only on EVENT rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" deviceType: description: "`watch | band | ring | phone | scale | other | unknown`." - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: type: integer exclusiveMinimum: 0 @@ -46191,16 +46192,16 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ dayKey: type: string sampleCount: @@ -46250,10 +46251,10 @@ components: id: type: string externalId: - anyOf: - - type: string - - type: "null" description: The identity key the client dedups on for measurements. + type: + - string + - "null" syncVersion: type: integer exclusiveMinimum: 0 @@ -46261,11 +46262,11 @@ components: deletedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - externalId @@ -46333,18 +46334,18 @@ components: - type: "null" description: The day's context, or null when the entry carries none. Null and an object of nulls are different answers. tags: - anyOf: - - type: string - - type: "null" description: JSON array of tag keys, or null. + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" moodLoggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: type: string syncVersion: @@ -46355,7 +46356,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - date @@ -46387,11 +46388,11 @@ components: deletedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - syncVersion @@ -46409,12 +46410,12 @@ components: scheduledFor: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipped: type: boolean @@ -46431,7 +46432,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - medicationId @@ -46456,11 +46457,11 @@ components: deletedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - syncVersion @@ -46474,10 +46475,10 @@ components: id: type: string externalId: - anyOf: - - type: string - - type: "null" description: The client's own key for the day log, when it set one. + type: + - string + - "null" syncVersion: type: integer minimum: 0 @@ -46485,11 +46486,11 @@ components: deletedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - externalId @@ -46510,11 +46511,11 @@ components: deletedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - syncVersion @@ -46561,7 +46562,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "Day-bucket start (ISO-8601): the user's local midnight. A window longer than the daily cap is served from a coarser tier (a UTC day, week or month); its rows are stamped at noon UTC of the bucket's first day, which falls inside the bucket's own week or month in every zone." @@ -46572,14 +46573,14 @@ components: maximum: 9007199254740991 minValue: description: Per-day minimum (spot metrics only; absent for cumulative). - anyOf: - - type: number - - type: "null" + type: + - number + - "null" maxValue: description: Per-day maximum (spot metrics only; absent for cumulative). - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - type - value @@ -46721,16 +46722,16 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Point timestamp (ISO-8601). value: type: number description: Primary value in the top-level `unit`. secondary: - anyOf: - - type: number - - type: "null" description: Diastolic value for `kind=bloodPressure` (paired within ±5 min); null for every other kind. + type: + - number + - "null" sleepStages: description: Per-stage hours for a `kind=sleep` night (CORE/DEEP/REM/…); null/absent for non-sleep kinds. anyOf: @@ -46744,16 +46745,16 @@ components: description: "v1.19.2 (iOS #34 extension) — per-point MINIMUM for `kind=pulse`. On an aggregated hourly heart-rate bucket `value` is the hour's average and this is the hour's low; null on a per-sample PULSE row and absent for every other kind." - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueMax: description: "v1.19.2 (iOS #34 extension) — per-point MAXIMUM for `kind=pulse`. On an aggregated hourly heart-rate bucket `value` is the hour's average and this is the hour's high; null on a per-sample PULSE row and absent for every other kind." - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - id - at @@ -46956,55 +46957,55 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: $ref: "#/components/schemas/MeasurementSource" notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" userId: type: string valueMin: description: Bucket MINIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueMax: description: Bucket MAXIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" externalId: description: External-system dedup key; null for manual entries. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glucoseContext: description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor export, a device sync). Never invent one. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sleepStage: description: Set only on SLEEP_DURATION rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" rhythmClassification: description: Device classification verdict; set only on EVENT rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" deviceType: description: "`watch | band | ring | phone | scale | other | unknown`." - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: description: Bumped on every server-side mutation; the last-writer-wins input for cross-device reconciliation. type: integer @@ -47015,16 +47016,16 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ dayKey: type: string sampleCount: @@ -47056,7 +47057,7 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Bucket start. count: type: integer @@ -47107,7 +47108,7 @@ components: the read started from. The window is a year ending at `to` (or now); page further back by moving `to`." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - total - limit @@ -47146,55 +47147,55 @@ components: measuredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: $ref: "#/components/schemas/MeasurementSource" notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" userId: type: string valueMin: description: Bucket MINIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueMax: description: Bucket MAXIMUM — set only on an hourly heart-rate `stats:` row. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" externalId: description: External-system dedup key; null for manual entries. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" externalSourceVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" glucoseContext: description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor export, a device sync). Never invent one. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sleepStage: description: Set only on SLEEP_DURATION rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" rhythmClassification: description: Device classification verdict; set only on EVENT rows. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" deviceType: description: "`watch | band | ring | phone | scale | other | unknown`." - anyOf: - - type: string - - type: "null" + type: + - string + - "null" syncVersion: description: Bumped on every server-side mutation; the last-writer-wins input for cross-device reconciliation. type: integer @@ -47205,16 +47206,16 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ dayKey: type: string sampleCount: @@ -47544,9 +47545,9 @@ components: type: object properties: night: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" main: anyOf: - type: object @@ -47561,11 +47562,11 @@ components: start: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ end: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ asleepMinutes: type: integer minimum: 0 @@ -47632,11 +47633,11 @@ components: start: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ end: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ minutes: type: integer minimum: 0 @@ -47665,9 +47666,9 @@ components: type: string description: MeasurementSource value of the writer bucket. deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -47715,11 +47716,11 @@ components: start: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ end: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ asleepMinutes: type: integer minimum: 0 @@ -47786,11 +47787,11 @@ components: start: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ end: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ minutes: type: integer minimum: 0 @@ -47819,9 +47820,9 @@ components: type: string description: MeasurementSource value of the writer bucket. deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -47940,15 +47941,15 @@ components: description: "`learning` until enough free-day nights exist (no band asserted); `ready` once the free-day sample is large enough." msfMinutes: - anyOf: - - type: number - - type: "null" description: Mid-sleep on free days (minutes-of-day), null while learning. + type: + - number + - "null" msfScMinutes: - anyOf: - - type: number - - type: "null" description: Sleep-debt-corrected mid-sleep on free days (MSFsc, minutes-of-day), null while learning. + type: + - number + - "null" band: anyOf: - type: string @@ -47961,10 +47962,10 @@ components: - type: "null" description: MCTQ chronotype band off MSFsc, null while learning. socialJetlagMinutes: - anyOf: - - type: number - - type: "null" description: Social jetlag = circular |MSF_work − MSF_free| in minutes, null when one side is missing. + type: + - number + - "null" freeNightsCounted: type: integer minimum: 0 @@ -48083,20 +48084,20 @@ components: id: type: string sportType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" startedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" endedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" durationSec: anyOf: @@ -48105,13 +48106,13 @@ components: maximum: 9007199254740991 - type: "null" distanceM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" activeEnergyKcal: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avgHr: anyOf: - type: integer @@ -48127,9 +48128,9 @@ components: source: $ref: "#/components/schemas/MeasurementSource" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasRoute: type: boolean hasHrSeries: @@ -48176,11 +48177,11 @@ components: startedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ endedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ dayKey: type: string durationSec: @@ -48188,13 +48189,13 @@ components: minimum: 0 maximum: 9007199254740991 distanceM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" activeEnergyKcal: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avgHr: anyOf: - type: integer @@ -48236,9 +48237,9 @@ components: maximum: 9007199254740991 - type: "null" elevationM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" pauseDurationSec: anyOf: - type: integer @@ -48248,9 +48249,9 @@ components: source: $ref: "#/components/schemas/MeasurementSource" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" metadata: anyOf: - {} @@ -48292,9 +48293,9 @@ components: canonicalId: type: string previousWorkoutId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - sportType @@ -48479,9 +48480,9 @@ components: minimum: 0 maximum: 9007199254740991 avgDistanceM: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avgAvgHr: anyOf: - type: integer @@ -48502,7 +48503,7 @@ components: generatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - paragraph - generatedAt @@ -48770,13 +48771,13 @@ components: scheduledAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: The slot the dose belongs to — the event's `scheduledFor`, renamed on this wire only. takenAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" status: type: string @@ -48794,7 +48795,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The medication's deferral stamp, repeated on each of its events. Null when the medication is not deferred. required: @@ -48861,12 +48862,12 @@ components: scheduledFor: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipped: type: boolean @@ -48896,17 +48897,17 @@ components: - IMPORT - APPLE_HEALTH idempotencyKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ injectionSite: anyOf: - type: string @@ -48923,11 +48924,11 @@ components: description: Recorded injection site for a site-tracked medication (GLP-1 rotation surface). NULL when the medication does not track sites or none was recorded. doseTaken: - anyOf: - - type: string - - type: "null" description: Free-text dose actually taken when it differed from the scheduled dose (titration weeks, split doses). NULL = the scheduled dose. + type: + - string + - "null" inventoryConsumption: anyOf: - {} @@ -48935,11 +48936,11 @@ components: description: JSON ledger of the container decrements this intake caused (`[{itemId, units}]`), written by the consumption hook. NULL when nothing was consumed (skipped / no tracked inventory). externalId: - anyOf: - - type: string - - type: "null" description: v1.28 — client-supplied stable id for externally-mirrored intakes (Apple Health sync); the dedup key for re-synced rows. NULL for native rows. + type: + - string + - "null" syncVersion: type: integer minimum: -9007199254740991 @@ -48949,7 +48950,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Soft-delete tombstone. Non-null rows are excluded from every list/aggregate read; sync readers use it to propagate deletions. @@ -49011,13 +49012,13 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" injectionSite: - anyOf: - - type: string - - type: "null" description: Recorded site, or null when none was captured. + type: + - string + - "null" required: - takenAt - injectionSite @@ -49043,16 +49044,16 @@ components: effectiveFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ doseValue: type: number doseUnit: type: string note: - anyOf: - - type: string - - type: "null" description: Titration note, decrypted on read. Null when none was recorded. + type: + - string + - "null" required: - id - effectiveFrom @@ -49148,7 +49149,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - medicationId @@ -49239,12 +49240,12 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 token: - anyOf: - - type: string - - type: "null" description: The raw `hlk_<64 hex>` token, and the ONLY time it is ever returned. Null when the endpoint was already enabled and nothing was minted — check `created` rather than inferring from this. Store it on receipt; it is hashed at rest and cannot be shown again. + type: + - string + - "null" created: type: boolean description: True when this call minted a token, false when one existed. @@ -49438,7 +49439,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ category: type: string description: Derived SERVER-side from `entry` through the authoritative taxonomy. Never accepted from a client — a @@ -49451,15 +49452,15 @@ components: minimum: 1 maximum: 5 notes: - anyOf: - - type: string - - type: "null" description: The free-text note, decrypted on read. Stored encrypted; the ciphertext column is stripped before the row leaves the server. + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - userId @@ -49801,29 +49802,29 @@ components: description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. + type: + - string + - "null" rxNormCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. + type: + - string + - "null" pausedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" remindersActive: type: boolean @@ -49836,7 +49837,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueOverdue: type: boolean @@ -49844,7 +49845,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval countdown's first window. NULL means active from creation. @@ -49852,7 +49853,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. oneShot: @@ -49885,18 +49886,18 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded automatically. NULL while the option is off. Read-only — set by the server on the switch. createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ schedules: type: array items: @@ -49912,11 +49913,11 @@ components: category: $ref: "#/components/schemas/MedicationCategory" categoryLabel: - anyOf: - - type: string - - type: "null" description: v1.40 — the decrypted label of a custom category (`category` = `custom:`); null for a built-in category, which a client localises from `category`. Read-only. + type: + - string + - "null" courses: type: array items: @@ -49966,7 +49967,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Latest non-skipped `MedicationIntakeEvent.takenAt` for the medication. Drives the rolling-cadence countdown surface. @@ -49978,12 +49979,12 @@ components: explicit skip. Pending projector-minted rows do not count — the card overdue-pill suppression compares this against the passed-dose count, and a pending mint must not read as covered." stockUnitsRemaining: - anyOf: - - type: number - - type: "null" description: v1.16.10 — usable inventory units left across the medication's containers (sum of `unitsRemaining` over ACTIVE / IN_USE items with units left). NULL = inventory tracking off (no items ever registered); 0 = tracking on, supply ran out. Read-only — aggregated, not stored. + type: + - number + - "null" stockDosesRemaining: anyOf: - type: integer @@ -50095,10 +50096,10 @@ components: maximum: 9007199254740991 description: Doses logged as taken whose slot falls on a day of this course. note: - anyOf: - - type: string - - type: "null" description: The person's note on this course. Encrypted at rest. + type: + - string + - "null" required: - id - startsOn @@ -50174,29 +50175,29 @@ components: description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. + type: + - string + - "null" rxNormCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. + type: + - string + - "null" pausedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" remindersActive: type: boolean @@ -50212,7 +50213,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueOverdue: description: "Present on the READ paths only, alongside `nextDueAt`. v1.16.4 — true when `nextDueAt` is an OPEN overdue @@ -50224,7 +50225,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval countdown's first window. NULL means active from creation. @@ -50232,7 +50233,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. oneShot: @@ -50265,18 +50266,18 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded automatically. NULL while the option is off. Read-only — set by the server on the switch. createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ schedules: type: array items: @@ -50292,11 +50293,11 @@ components: category: $ref: "#/components/schemas/MedicationCategory" categoryLabel: - anyOf: - - type: string - - type: "null" description: v1.40 — the decrypted label of a custom category (`category` = `custom:`); null for a built-in category, which a client localises from `category`. Read-only. + type: + - string + - "null" courses: type: array items: @@ -50334,11 +50335,11 @@ components: tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored." stockUnitsRemaining: - anyOf: - - type: number - - type: "null" description: "v1.37.19 — usable inventory units left (same semantics as the list entry's field): NULL = inventory tracking off; 0 = tracking on, supply ran out." + type: + - number + - "null" stockDosesRemaining: anyOf: - type: integer @@ -50418,15 +50419,15 @@ components: type: string description: The catalogue's canonical unit for the code. enteredAmount: - anyOf: - - type: number - - type: "null" description: The amount as entered when it was entered in another unit. + type: + - number + - "null" enteredUnit: - anyOf: - - type: string - - type: "null" description: The input unit it was entered in (e.g. `IU`). + type: + - string + - "null" labelKey: type: string label: @@ -50460,10 +50461,10 @@ components: label: type: string date: - anyOf: - - type: string - - type: "null" description: "`YYYY-MM-DD` or an ISO instant, as the record carries it." + type: + - string + - "null" href: type: string description: The web path of the related record, the same form as a push `url`. @@ -50787,7 +50788,7 @@ components: `baseUpdatedAt` on the next write. Opaque." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version additionalProperties: false @@ -51035,26 +51036,26 @@ components: window; BLISTER / INHALER / BOTTLE / OTHER hold individually sealed units and expire only on their printed date. unitsTotal: - anyOf: - - type: number - - type: "null" description: Units the container shipped with (tablets / ampoules / puffs; 1–1000). v1.16.12 — fractional, so a split-pill remainder reads cleanly. Dose-derived readouts divide by the medication's `unitsPerDose`. v1.18.3 (iOS#31) — NULL when the unit count is unknown (a corrupt or legacy row); the client renders "unknown", never a fabricated 0. + type: + - number + - "null" unitsRemaining: - anyOf: - - type: number - - type: "null" description: Units left in the container. v1.16.12 — fractional (a ½-tablet dose leaves 29.5 of 30). Decremented by the intake consumption hook (FEFO with spillover across containers); refunded when a taken dose is skipped, edited away, or deleted. v1.18.3 (iOS#31) — NULL when the count is unknown (a corrupt or legacy row); the client renders "unknown", never a fabricated 0 it could decrement into negatives. + type: + - number + - "null" firstUseAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Instant the container was first used. NULL until opened. On a PEN / AMPOULE it starts the 30-day in-use clock; on the other kinds it only records that the pack is open. @@ -51062,7 +51063,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Persisted MIN(firstUseAt + 30 days, printedExpiry) for a clock-bearing container (PEN / AMPOULE), else printedExpiry alone. NULL when no deadline applies — an opened blister with no printed date has none. @@ -51070,38 +51071,38 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" purchasedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" manufacturer: - anyOf: - - type: string - - type: "null" description: Marketing-authorisation holder / maker as printed on the carton. NULL for a container registered before the field existed, and for plain supply rows that never carried one. + type: + - string + - "null" doseStrength: - anyOf: - - type: string - - type: "null" description: Strength as printed on the container, e.g. "5 mg/0.5 ml". Free text, not split into a number + unit — pens state strength per dose, per ml, or per cartridge. NULL when unknown. + type: + - string + - "null" notes: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - userId @@ -51346,7 +51347,7 @@ components: anchorIso: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ next: anyOf: - type: object @@ -51354,11 +51355,11 @@ components: windowStart: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowEnd: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ scheduleIndex: type: integer minimum: 0 @@ -51389,11 +51390,11 @@ components: type: object properties: adherenceRate: - anyOf: - - type: number - - type: "null" description: 0-100, taken / (taken + missed). Skipped doses are excluded from the denominator — a deliberate decision, not a compliance failure. NULL when no dose was expected in the window (brand-new medication, paused). + type: + - number + - "null" currentStreak: type: integer minimum: 0 @@ -51429,15 +51430,15 @@ components: day: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowStart: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ windowEnd: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ scheduleIndex: type: integer minimum: 0 @@ -51686,14 +51687,14 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The open cycle's due instant. Null when `state` is `none`. graceUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: End of the due slot's grace window. Null when `state` is `none`. hasClosedCycles: @@ -51729,7 +51730,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Target instant of the open dose. Null when no dose is open. required: @@ -51943,9 +51944,9 @@ components: - one_shot - no_target startsOn: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" resolution: type: object properties: @@ -51957,9 +51958,9 @@ components: - name - none cls: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - tier - cls @@ -51976,9 +51977,9 @@ components: type: object properties: start: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" startSource: anyOf: - type: string @@ -52007,9 +52008,9 @@ components: from: type: string to: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - from - to @@ -52035,9 +52036,9 @@ components: label: type: string unit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" primary: type: boolean referenceBand: @@ -52130,9 +52131,9 @@ components: mean: type: number pct: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - mean - pct @@ -52290,12 +52291,12 @@ components: from: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "Start of the window recorded doses were read over: the requested `from` after the span clamp." to: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "End of the window recorded doses were read over: the requested `to`, or the slot time of a dose recorded ahead of its slot when that lies later (at most one day after `to`)." family: @@ -52334,13 +52335,13 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: The slot anchor instant, or the ad-hoc take's own time. timeOfDay: - anyOf: - - type: string - - type: "null" description: The slot's `HH:mm` label, or null for an ad-hoc row. + type: + - string + - "null" status: type: string enum: @@ -52361,7 +52362,7 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ timeOfDay: type: string filled: @@ -52376,18 +52377,18 @@ components: - type: object properties: id: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" scheduledFor: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ takenAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipped: type: boolean @@ -52397,10 +52398,10 @@ components: type: boolean description: v1.42.2 — the dose was recorded automatically (`autoMarkTaken`), not by the person. doseTaken: - anyOf: - - type: string - - type: "null" description: Per-intake dose override; null = configured dose. + type: + - string + - "null" source: anyOf: - type: string @@ -52443,7 +52444,7 @@ components: currentSince: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ description: "Instant the LIVE plan took over: the newest revision's `validUntil`, or the medication's `createdAt` when no era has been archived." revisions: @@ -52475,12 +52476,12 @@ components: validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ description: Inclusive start instant of the era. validUntil: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ description: Exclusive end instant of the era — the moment the next plan took over. source: type: string @@ -52511,13 +52512,13 @@ components: type: string description: Daily dose times (HH:mm, user local) the era ran at. label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dose: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" scheduleType: type: string description: Schedule-type discriminator of the archived row (SCHEDULED / PRN / CYCLIC). @@ -52609,19 +52610,19 @@ components: dryRun: type: boolean jobId: - anyOf: - - type: string - - type: "null" description: Null on a dry run (nothing was queued). + type: + - string + - "null" status: description: Present only on a real (non-dry-run) submission. type: string const: queued statusUrl: - anyOf: - - type: string - - type: "null" description: Relative poll path (`/api/medications/intake/dose-history-import/{jobId}/status`). Null on a dry run. + type: + - string + - "null" file: $ref: "#/components/schemas/MedicationDoseHistoryImportFile" required: @@ -52796,26 +52797,26 @@ components: - type: "null" description: Set only once `status` is `done`; null before that. failureReason: - anyOf: - - type: string - - type: "null" description: Set only when `status` is `failed`; null otherwise. + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ startedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the worker picked the job up; null while still queued. completedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the job reached `done` or `failed`; null while active. required: @@ -53043,9 +53044,9 @@ components: providerType: type: string model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" tokensUsed: anyOf: - type: integer @@ -53129,14 +53130,14 @@ components: - type: "null" description: The user's selected provider, or null when none is set. model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" baseUrl: - anyOf: - - type: string - - type: "null" description: Custom base URL (LOCAL provider only); null otherwise. + type: + - string + - "null" aiAvailable: type: boolean description: True when ANY provider can serve this user — including the operator's server-managed key when the user set @@ -53156,31 +53157,31 @@ components: hasAnthropicKey: type: boolean anthropicKeyPreview: - anyOf: - - type: string - - type: "null" description: "`...` + last 4 chars of the stored Anthropic key, or null." + type: + - string + - "null" hasLocalKey: type: boolean hasOpenaiKey: type: boolean openaiKeyPreview: - anyOf: - - type: string - - type: "null" description: "`...` + last 4 chars of the stored OpenAI key, or null." + type: + - string + - "null" compatBaseUrl: - anyOf: - - type: string - - type: "null" description: "v1.33.1 (#470) — base URL of the OpenAI-compatible gateway (LiteLLM / OpenRouter / vLLM). Its own column: the OpenAI provider stays pinned to api.openai.com and never reads it." + type: + - string + - "null" compatModel: - anyOf: - - type: string - - type: "null" description: Model the gateway should route. Null falls back to `model`; with neither set the gateway entry does not resolve. + type: + - string + - "null" hasCompatKey: type: boolean description: Whether a bearer is stored for the gateway. The bearer is optional — a gateway without auth needs none. @@ -53419,7 +53420,7 @@ components: synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - tone - verbosity @@ -53559,7 +53560,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the recipe last changed; null while it never has. updatedAt: @@ -53568,7 +53569,7 @@ components: synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - pillars - excludedPillars @@ -53623,7 +53624,7 @@ components: synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - modules additionalProperties: false @@ -54729,7 +54730,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - avatarUrl - contentType @@ -54770,18 +54771,18 @@ components: username: type: string displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" email: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dateOfBirth: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" gender: anyOf: @@ -54792,13 +54793,13 @@ components: - OTHER - type: "null" heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" locale: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timezone: type: string description: "The IANA zone the server cuts this account's days in, the same value `/api/auth/me` reports: the stored @@ -54823,23 +54824,23 @@ components: moodReminderEnabled: type: boolean fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerIkNumber: - anyOf: - - type: string - - type: "null" description: German insurer institution number (IKNR), 9 digits. + type: + - string + - "null" insuranceNumber: - anyOf: - - type: string - - type: "null" description: German KVNR, decrypted for the form prefill. + type: + - string + - "null" modules: $ref: "#/components/schemas/ModuleMap" required: @@ -54885,18 +54886,18 @@ components: username: type: string displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" email: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dateOfBirth: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" gender: anyOf: @@ -54907,13 +54908,13 @@ components: - OTHER - type: "null" heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" locale: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timezone: type: string description: "The IANA zone the server cuts this account's days in, the same value `/api/auth/me` reports: the stored @@ -54938,18 +54939,18 @@ components: moodReminderEnabled: type: boolean fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" insurerIkNumber: - anyOf: - - type: string - - type: "null" description: German insurer institution number (IKNR), 9 digits. + type: + - string + - "null" hasInsuranceNumber: type: boolean description: Whether a KVNR is on file (the value itself is not echoed). @@ -55105,7 +55106,7 @@ components: synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - medication - mood @@ -55561,13 +55562,13 @@ components: platform: type: string appVersion: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" locale: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" channels: type: array items: @@ -55580,11 +55581,11 @@ components: lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ isCurrent: type: boolean description: True only when the caller sent `X-Device-Id` AND that device still holds an unrevoked refresh token for @@ -55838,11 +55839,11 @@ components: items: $ref: "#/components/schemas/CoachConversation" nextCursor: - anyOf: - - type: string - - type: "null" description: Id of the last conversation on this page; pass it back as `cursor` for the next page. Null at the end of the list. + type: + - string + - "null" required: - conversations - nextCursor @@ -55859,11 +55860,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Bumped on every appended message; the rail orders by this. messageCount: type: integer @@ -55883,9 +55884,9 @@ components: documentTitle: description: v1.29.x (S7) — the FIRST attachment's resolved title, kept for the rail's single-line badge. Null on a health thread or a fenced thread with no live attachment. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - title @@ -55901,11 +55902,11 @@ components: documentId: type: string title: - anyOf: - - type: string - - type: "null" description: The attached document's resolved title (title, falling back to filename). Null when it has neither. Plaintext; no health values. + type: + - string + - "null" required: - documentId - title @@ -57336,9 +57337,9 @@ components: maximum: 9007199254740991 - type: "null" model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - totalTokens additionalProperties: false @@ -57480,10 +57481,10 @@ components: items: type: array items: - anyOf: - - type: string - - type: number - - type: "null" + type: + - string + - number + - "null" description: Row-major cells, at most 400 rows. `null` is a period with no reading. truncated: type: boolean @@ -57903,11 +57904,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ messageCount: type: integer minimum: -9007199254740991 @@ -57919,9 +57920,9 @@ components: description: Every message in the conversation, decrypted, oldest-first. summary: description: Rolling summary of turns elided past the history window; null when none is on file. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fenced: type: boolean description: v1.29.x (S7) — the sticky fence flag; the client routes a fenced thread's turns through the hardened fenced @@ -57938,9 +57939,9 @@ components: description: v1.29.x (S7) — count of live attachment rows (equal to attachments.length). documentTitle: description: v1.29.x (S7) — the first attachment's resolved title for the rail badge. Null on a health thread. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - title @@ -57969,26 +57970,26 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ metricSource: anyOf: - $ref: "#/components/schemas/CoachProvenance" - type: "null" description: Provenance envelope (assistant turns); null for user turns. providerType: - anyOf: - - type: string - - type: "null" description: 'Provider that produced the reply (e.g. anthropic, openai, local, refusal). The sentinel `cancelled` marks an empty assistant row closing a turn the client aborted mid-generation (navigation away); clients should render it as an interrupted turn with a retry affordance. v1.39.4 — the sentinel `reuse` marks a turn answered from a table already stored in the conversation (a follow-up chip such as "as a chart"), with no model call: render it as a normal assistant turn without a token footer. Null for user turns.' + type: + - string + - "null" promptVersion: - anyOf: - - type: string - - type: "null" description: Coach prompt version that produced the reply; null for user turns. + type: + - string + - "null" tokensUsed: anyOf: - type: integer @@ -57998,11 +57999,11 @@ components: description: v1.18.9 — total tokens this assistant turn cost, persisted so the per-message token footer survives a reload. Null on user turns, refusals, and pre-feature rows. model: - anyOf: - - type: string - - type: "null" description: v1.18.9 — the provider model that produced the reply (e.g. gpt-4o). Null when unknown (user turns, refusals, older rows). + type: + - string + - "null" required: - id - role @@ -58262,7 +58263,7 @@ components: `baseUpdatedAt` on the next write. Opaque." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version additionalProperties: false @@ -58344,30 +58345,30 @@ components: `coach` (saved during a turn), `extracted` (the background extraction; every fact older than v1.41), `pattern` (the deterministic matcher). `proposed` is never listed." sourceConversationId: - anyOf: - - type: string - - type: "null" description: The conversation the fact came out of, when known. + type: + - string + - "null" sourceMessageId: - anyOf: - - type: string - - type: "null" description: v1.41 — the message the fact came out of, when known. + type: + - string + - "null" lastUsedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.41 — when the fact last went into a Coach turn. createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: v1.41 — when the fact was last written or edited. required: - id @@ -58583,7 +58584,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - category @@ -58641,18 +58642,18 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Timestamp of the newest Coach assistant message; null when none exists. unread: type: boolean description: True when that message is newer than the last time the caller opened the Coach. conversationId: - anyOf: - - type: string - - type: "null" description: Conversation holding the newest assistant message; null when none exists. The client deep-links into it (`/coach?c=`) on the unread path. + type: + - string + - "null" ai: description: The `coach` capability. While unavailable the three fields above are the quiet empty values. $ref: "#/components/schemas/AiCapabilityState" @@ -58683,7 +58684,7 @@ components: seenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: The server-minted open timestamp. required: - seenAt @@ -58707,21 +58708,21 @@ components: type: object properties: aboutMe: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" conditions: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" allergies: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" coachFocus: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiIncludedSections: type: array items: @@ -58743,7 +58744,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" maxChars: type: integer @@ -58787,21 +58788,21 @@ components: type: object properties: aboutMe: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" conditions: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" allergies: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" coachFocus: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" aiIncludedSections: type: array items: @@ -58822,7 +58823,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ maxChars: type: integer minimum: -9007199254740991 @@ -58880,20 +58881,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -58901,13 +58902,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -58933,20 +58934,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -58954,13 +58955,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -58986,20 +58987,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -59007,13 +59008,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59045,20 +59046,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -59066,13 +59067,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59116,20 +59117,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -59137,13 +59138,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59179,7 +59180,7 @@ components: validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: type: "null" provenance: @@ -59190,7 +59191,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59229,20 +59230,20 @@ components: - ALCOHOL_PATTERN - SHIFT_SCHEDULE value: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unreadable: type: boolean validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" provenance: type: string @@ -59250,13 +59251,13 @@ components: - USER_REPORTED - USER_CORRECTION supersededByRevisionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59292,7 +59293,7 @@ components: validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: type: "null" provenance: @@ -59303,7 +59304,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59349,7 +59350,7 @@ components: removedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -59418,21 +59419,21 @@ components: - UNKNOWN - type: "null" contacts: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" contactsUnreadable: type: boolean implants: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" implantsUnreadable: type: boolean note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" noteUnreadable: type: boolean required: @@ -59589,20 +59590,20 @@ components: type: string description: The metric this plan moves (e.g. WEIGHT, SLEEP). ifCue: - anyOf: - - type: string - - type: "null" description: Decrypted if-cue (the trigger). Null when the row's key id is no longer in the map. + type: + - string + - "null" thenAction: - anyOf: - - type: string - - type: "null" description: Decrypted then-action. Null when the row's key id is no longer in the map. + type: + - string + - "null" target: - anyOf: - - type: string - - type: "null" description: Decrypted optional target; null when none / undecryptable. + type: + - string + - "null" status: type: string enum: @@ -59617,23 +59618,23 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Optional check-in checkpoint; null when none. sourceConversationId: - anyOf: - - type: string - - type: "null" description: Id of the conversation the proposal was extracted from; null for a plan without conversation provenance. Lets a chat surface show only the proposals born in the open thread. + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - metric @@ -59791,14 +59792,14 @@ components: id: type: string note: - anyOf: - - type: string - - type: "null" description: Decrypted note (the user's own framing). Null when the row's key id is no longer in the map. + type: + - string + - "null" metric: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" triggerKind: type: string enum: @@ -59808,12 +59809,12 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" contextCue: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" status: type: string enum: @@ -59833,11 +59834,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - note @@ -59973,22 +59974,22 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ usedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" revokedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" uses: type: integer @@ -60034,7 +60035,7 @@ components: redeemedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ user: anyOf: - type: object @@ -60044,9 +60045,9 @@ components: username: type: string email: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - username @@ -60093,11 +60094,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ uses: type: integer minimum: -9007199254740991 @@ -60168,7 +60169,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - createdAt @@ -60226,18 +60227,18 @@ components: requestedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ startedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ - type: "null" finishedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ - type: "null" outcome: anyOf: @@ -60332,7 +60333,7 @@ components: timestamp: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ missedThresholdMinutes: type: integer minimum: -9007199254740991 @@ -60485,21 +60486,21 @@ components: hasApnsToken: type: boolean apnsTokenPrefix: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" apnsTokenSuffix: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" apnsEnvironment: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - platform @@ -60538,13 +60539,13 @@ components: result: type: string reason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - eventType - channel @@ -60582,13 +60583,13 @@ components: timezone: type: string heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dateOfBirth: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" gender: anyOf: - type: string @@ -60598,9 +60599,9 @@ components: - OTHER - type: "null" glucoseUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" onboardingTourCompleted: type: boolean required: @@ -60927,9 +60928,9 @@ components: source: type: string deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -60983,29 +60984,29 @@ components: - type: object properties: bpInTargetPct: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPct7d: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPct30d: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctAllTime: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctPriorMonth: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctPriorYear: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetCount90: anyOf: - type: integer @@ -61058,19 +61059,19 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 nextDueAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" nextDueOverdue: type: boolean nextDueMedicationName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" nextDueMedicationId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dueCandidates: type: array items: @@ -61123,9 +61124,9 @@ components: - yellow - red delta: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" confidence: $ref: "#/components/schemas/DerivedConfidence" composition: @@ -61188,9 +61189,9 @@ components: active: type: boolean since: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" episodeCount: type: integer minimum: -9007199254740991 @@ -61357,9 +61358,9 @@ components: description: "`disabled` whenever the `briefing` AI capability is unavailable for a reason other than a missing provider (then `briefingAi` names it); `no-provider` when no provider can serve it." briefingUpdatedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" briefingStale: type: boolean description: True when `briefing` carries the last good (expired-TTL) briefing while a refresh is pending (`preparing`). @@ -61404,9 +61405,9 @@ components: stillLearning: type: boolean stillLearningReason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" windowDays: type: integer minimum: -9007199254740991 @@ -61418,9 +61419,9 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 meanMgdl: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" distribution: anyOf: - type: object @@ -61465,13 +61466,13 @@ components: additionalProperties: false - type: "null" gmi: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" estimatedA1c: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" variability: anyOf: - type: object @@ -61493,9 +61494,9 @@ components: - type: object properties: jIndex: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" lbgi: type: number hbgi: @@ -61720,10 +61721,10 @@ components: type: string description: One-line alert message. body: - anyOf: - - type: string - - type: "null" description: Longer narrative; null on the current rule-engine cards. + type: + - string + - "null" severity: type: string enum: @@ -61742,9 +61743,9 @@ components: label: type: string actionURL: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - label @@ -61754,7 +61755,7 @@ components: generatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ provider: type: string description: "Who wrote the card: always `rules`, the threshold engine. It used to echo the account's AI provider @@ -61847,35 +61848,35 @@ components: $ref: "#/components/schemas/MetricDataSummary" description: Keyed by `MeasurementType`. Only types the account has data for are present. bmi: - anyOf: - - type: number - - type: "null" description: Derived from `summaries.WEIGHT.latest` and the profile height; null when either is missing. + type: + - number + - "null" bpInTargetPct: - anyOf: - - type: number - - type: "null" description: Trailing 90 days — the headline the BP tile shows. + type: + - number + - "null" bpInTargetPct7d: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPct30d: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctAllTime: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctPriorMonth: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetPctPriorYear: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" bpInTargetCount90: anyOf: - type: integer @@ -61986,9 +61987,9 @@ components: source: type: string deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -62016,7 +62017,7 @@ components: lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ daysAgo: type: integer minimum: -9007199254740991 @@ -62059,35 +62060,35 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 latest: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" min: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" max: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" median: - anyOf: - - type: number - - type: "null" description: 50th percentile over the trailing 90 days on this route (the SQL summaries path fixes that window). Linear-interpolated midpoint. + type: + - number + - "null" avg7: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg30: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" slope7: anyOf: - $ref: "#/components/schemas/TrendSlope" @@ -62099,15 +62100,15 @@ components: avg30LastMonth: description: Mean over the 30-day window that ended 30 days ago — the prior period the tile's delta callout compares against. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg30LastYear: description: Mean over the 30-day window that ended 365 days ago. Populated only for types whose WEEK/MONTH/YEAR rollup tier reaches that far back; null otherwise, which is not the same as 'the year-ago window was empty'. - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - count - latest @@ -62345,10 +62346,10 @@ components: - result additionalProperties: false delta: - anyOf: - - type: number - - type: "null" description: Move against the previous window, or null when none forms. + type: + - number + - "null" deltaReason: anyOf: - type: string @@ -62441,7 +62442,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" episodeCount: type: integer @@ -62619,7 +62620,7 @@ components: computedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Compute time (for cache-staleness + the 'as of' chip). required: - inputs @@ -62655,7 +62656,7 @@ components: asOf: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ sources: type: array items: @@ -62677,15 +62678,15 @@ components: - population-percentile - guideline-band low: - anyOf: - - type: number - - type: "null" description: Lower band edge, canonical unit. + type: + - number + - "null" high: - anyOf: - - type: number - - type: "null" description: Upper band edge, canonical unit. + type: + - number + - "null" label: type: string description: The band as display text, in the reader's units. @@ -62710,13 +62711,13 @@ components: - population-percentile - guideline-band low: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" high: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" label: type: string source: @@ -62827,14 +62828,14 @@ components: distanceKg: type: number deltaKg: - anyOf: - - type: number - - type: "null" description: Positive means the distance to the personal band narrowed. + type: + - number + - "null" asOf: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: type: string required: @@ -62900,7 +62901,7 @@ components: lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ daysAgo: type: integer minimum: -9007199254740991 @@ -62930,9 +62931,9 @@ components: source: type: string deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -62999,7 +63000,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" legacyPayload: type: boolean @@ -63018,11 +63019,11 @@ components: description: True when the most recent generation attempt failed after the last successful one. The last good text is still served, so this is the only honest signal that what is shown is held rather than fresh. generationFailureClass: - anyOf: - - type: string - - type: "null" description: Coarse class of that failure, so the empty state can point at the right lever (raise the response timeout versus re-check the provider). Null when the last attempt succeeded. + type: + - string + - "null" briefingOmittedReason: anyOf: - type: string @@ -63077,7 +63078,7 @@ components: description: Present on the cached branch only. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ legacyPayload: type: boolean briefingOmittedReason: @@ -63115,18 +63116,18 @@ components: type: object properties: activeProvider: - anyOf: - - type: string - - type: "null" description: The provider type the runner would reach first — the head of the RESOLVED chain, which drops disabled entries. Null when the account has none configured. + type: + - string + - "null" cachedActiveProvider: - anyOf: - - type: string - - type: "null" description: "The last provider that actually worked, from the in-process cache. Present when the runner has rerouted to a fallback, so the settings surface can say `active: codex (cached: openai)` instead of leaving the reroute in the logs. In-process, so it resets on restart and can differ between instances." + type: + - string + - "null" configuredChain: type: array items: @@ -63191,7 +63192,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - createdAt @@ -63355,7 +63356,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - text - provenance @@ -63414,7 +63415,7 @@ components: date: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: When it happened. A `side-effect` entry is a DAY rather than an instant, and is stamped at 12:00 UTC of that day so it sorts — do not read it as a capture time. kind: @@ -63441,13 +63442,13 @@ components: - type: "null" note: description: The titration note, decrypted. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" injectionSite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" inventoryDelta: type: integer minimum: -9007199254740991 @@ -63566,10 +63567,10 @@ components: additionalProperties: false description: The day's shape, ascending. baseline: - anyOf: - - type: number - - type: "null" description: The resting baseline the day was judged against. + type: + - number + - "null" baselineSource: type: string enum: @@ -63664,7 +63665,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When that connection was made; null when never connected. hasAdminKey: @@ -63693,7 +63694,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the cached insight text was last written. required: @@ -63793,13 +63794,13 @@ components: type: object properties: current: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" average30: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" range: anyOf: - type: object @@ -63824,9 +63825,9 @@ components: type: object properties: heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" age: anyOf: - type: integer @@ -63871,13 +63872,13 @@ components: type: string description: Already-resolved display label. current: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" average30: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" trend: anyOf: - type: string @@ -63938,9 +63939,9 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 lastMetGoalAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" streakDays: type: integer minimum: -9007199254740991 @@ -64056,16 +64057,16 @@ components: description: Aggregate over the previous comparable window `[now-2N, now-N)`. $ref: "#/components/schemas/AnalyticsWindowAggregate" delta: - anyOf: - - type: number - - type: "null" description: "`current.mean - previous.mean`; null when either window has no data (never a misleading 0)." + type: + - number + - "null" deltaPct: - anyOf: - - type: number - - type: "null" description: "`delta / previous.mean` as a fraction (0.03 = +3 %); null when the prior window has no / zero mean (no divide-by-zero). The client shows 'no prior-period data' in that case." + type: + - number + - "null" required: - range - windowDays @@ -64089,26 +64090,26 @@ components: description: Reading count composed across buckets. For step-like totals it is the number of days with a total, for SLEEP_DURATION the number of nights. min: - anyOf: - - type: number - - type: "null" description: Window minimum; null when empty. + type: + - number + - "null" max: - anyOf: - - type: number - - type: "null" description: Window maximum; null when empty. + type: + - number + - "null" mean: - anyOf: - - type: number - - type: "null" description: Count-weighted mean across buckets; null when empty. For step-like totals (steps, energy, distance, flights) it is the average daily total, for SLEEP_DURATION the average time asleep per night. + type: + - number + - "null" sum: - anyOf: - - type: number - - type: "null" description: Cumulative total for cumulative metrics (steps, energy, distance); null when no bucket carries a sum. + type: + - number + - "null" required: - count - min @@ -64141,11 +64142,11 @@ components: description: "Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that." text: - anyOf: - - type: string - - type: "null" description: The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, and whenever the `statusText` capability is unavailable (see `ai`). + type: + - string + - "null" cached: type: boolean description: True when `text` is served from cache (incl. last-good). @@ -64153,7 +64154,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the served assessment was generated; null when none. preparing: @@ -64272,11 +64273,11 @@ components: description: "Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that." summary: - anyOf: - - type: string - - type: "null" description: The overall compliance note (plain text). Null while a first generation is preparing, and whenever the `statusText` capability is unavailable (see `ai`). + type: + - string + - "null" medications: type: array items: @@ -64289,7 +64290,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the served assessment was generated; null when none. preparing: @@ -64352,12 +64353,12 @@ components: description: "Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that." text: - anyOf: - - type: string - - type: "null" description: The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, when the metric has insufficient data, and whenever the `statusText` capability is unavailable (see `ai`). + type: + - string + - "null" cached: type: boolean description: True when `text` is served from cache (incl. last-good). @@ -64365,7 +64366,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the served assessment was generated; null when none. preparing: @@ -64420,12 +64421,12 @@ components: description: "Whether a configured AI provider exists for the record: provider presence and nothing else. It is never false because the operator switched notes off or consent is missing; `ai` says that." text: - anyOf: - - type: string - - type: "null" description: The assessment note (plain text, rendered as React text children). Null while a first generation is preparing, when the marker has no numeric readings, and whenever the `statusText` capability is unavailable (see `ai`). + type: + - string + - "null" cached: type: boolean description: True when `text` is served from cache (incl. last-good). @@ -64433,7 +64434,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the served assessment was generated; null when none. preparing: @@ -64539,10 +64540,10 @@ components: provenance: $ref: "#/components/schemas/DerivedProvenance" reason: - anyOf: - - type: string - - type: "null" description: Why the value could not be produced; null when status is 'ok'. + type: + - string + - "null" assessment: anyOf: - $ref: "#/components/schemas/DerivedAssessment" @@ -64581,7 +64582,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: When the text was produced / last warmed. required: - text @@ -64827,13 +64828,13 @@ components: recordedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: On-device recording time (the display timestamp). durationSeconds: - anyOf: - - type: number - - type: "null" description: "`sampleCount / samplingFrequency` in seconds; null when the source omitted the sampling frequency." + type: + - number + - "null" samplingFrequency: type: integer minimum: -9007199254740991 @@ -64852,10 +64853,10 @@ components: - type: "null" description: Source-reported average heart rate (BPM) for the strip, when present. lead: - anyOf: - - type: string - - type: "null" description: Recording lead label when the source reports it; null for the single-lead ScanWatch (Lead I). + type: + - string + - "null" classification: anyOf: - type: string @@ -64926,7 +64927,7 @@ components: recordedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: The `recordedAt` that was submitted, echoed back. sampleCount: type: integer @@ -64934,10 +64935,10 @@ components: maximum: 9007199254740991 description: "Server-derived: the number of samples received." durationSeconds: - anyOf: - - type: number - - type: "null" description: "Server-derived: `sampleCount / samplingFrequency`." + type: + - number + - "null" required: - id - status @@ -64987,13 +64988,13 @@ components: recordedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: On-device recording time (the display timestamp). durationSeconds: - anyOf: - - type: number - - type: "null" description: Strip duration in seconds; null when the sampling frequency was 0. + type: + - number + - "null" samplingFrequency: type: integer minimum: -9007199254740991 @@ -65007,10 +65008,10 @@ components: - type: "null" description: Source-reported average heart rate (BPM) for the strip, when present. lead: - anyOf: - - type: string - - type: "null" description: Recording lead label when reported; null for the single-lead ScanWatch (Lead I). + type: + - string + - "null" classification: anyOf: - type: string @@ -65132,16 +65133,16 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Device-reported event time (`measuredAt`). source: $ref: "#/components/schemas/MeasurementSource" deviceType: - anyOf: - - type: string - - type: "null" description: Device class that reported the event (`watch | band | ring | phone | scale | other | unknown`); null when the source omitted it (treated as `unknown`). + type: + - string + - "null" required: - id - type @@ -65450,17 +65451,17 @@ components: pValue: type: number qValue: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" evidenceHash: type: string lastComputedAt: type: string dismissedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - canonicalKey @@ -65503,9 +65504,9 @@ components: dismissed: type: boolean dismissedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" evidenceHash: type: string required: @@ -65644,10 +65645,10 @@ components: maximum: 9007199254740991 description: Nights with a per-night index reading. recentMeanIndex: - anyOf: - - type: number - - type: "null" description: Mean of the index readings (lower-better); null when none. + type: + - number + - "null" trend: anyOf: - type: string @@ -65705,15 +65706,15 @@ components: present: type: boolean latestDate: - anyOf: - - type: string - - type: "null" description: YYYY-MM-DD of the most-recent panel. + type: + - string + - "null" previousDate: - anyOf: - - type: string - - type: "null" description: YYYY-MM-DD of the prior panel. + type: + - string + - "null" changes: type: array items: @@ -65812,33 +65813,33 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 latest: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" min: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" max: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" median: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg7: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg30: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" slope7: anyOf: - type: object @@ -65884,13 +65885,13 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 avg30LastMonth: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg30LastYear: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - count - latest @@ -65941,30 +65942,30 @@ components: maximum: 9007199254740991 description: Daily points behind the figures. mean: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" min: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" max: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" latest: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" delta: - anyOf: - - type: number - - type: "null" description: Latest daily mean minus the previous daily mean. + type: + - number + - "null" inTargetPct: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" totalEntries: type: integer minimum: -9007199254740991 @@ -66062,9 +66063,9 @@ components: maximum: 9007199254740991 description: 0 = Monday … 6 = Sunday. avgScore: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" count: type: integer minimum: -9007199254740991 @@ -66091,9 +66092,9 @@ components: - evening - night avgScore: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" count: type: integer minimum: -9007199254740991 @@ -66253,13 +66254,13 @@ components: type: string label: description: Decrypted custom-tag label; absent for catalogue tags. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" count: type: integer minimum: -9007199254740991 @@ -66306,21 +66307,21 @@ components: type: string description: Tag key, or the correlation channel key for a metric. labelKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" categoryKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" direction: type: string enum: @@ -66338,15 +66339,15 @@ components: description: Unified 0..1 ranking strength (|r| for a metric, min(1, |delta| / 2) for a tag). Ranking only — surface `delta` or `r`, not this. delta: - anyOf: - - type: number - - type: "null" description: Raw mood-point delta for a tag row; null for a metric row. + type: + - number + - "null" r: - anyOf: - - type: number - - type: "null" description: Raw Pearson r for a metric row; null for a tag row. + type: + - number + - "null" required: - source - key @@ -66373,15 +66374,15 @@ components: type: string description: Structured tags only — this board never carries a flat tag. label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" categoryKey: type: string icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" metricKey: type: string description: Metric channel. Currently `activeEnergy`, `sleepDuration`, `nextDayRecovery`, `nextDayRestingHeartRate` or @@ -66459,9 +66460,9 @@ components: categoryKey: type: string icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" inverse: type: boolean description: True for an inverse-scaled factor (stress, conflict). The split already runs on the flipped series, so a @@ -66686,27 +66687,27 @@ components: maximum: 9007199254740991 description: Entries carrying a value. avg7: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg30: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" avg90: - anyOf: - - type: number - - type: "null" description: Trailing-window means; null when that window is empty. + type: + - number + - "null" latest: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" latestDate: - anyOf: - - type: string - - type: "null" description: "`YYYY-MM-DD` of `latest`." + type: + - string + - "null" newestDaysAgo: anyOf: - type: integer @@ -66754,23 +66755,23 @@ components: type: string description: Stable tag key. For a flat tag this is the free text itself. labelKey: - anyOf: - - type: string - - type: "null" description: i18n label key for a structured tag; null for a flat one. + type: + - string + - "null" label: description: Decrypted custom-tag label. Absent for catalogue and flat tags — the key is absent, not null. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" categoryKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" withDays: type: integer minimum: -9007199254740991 @@ -66789,11 +66790,11 @@ components: type: number description: "`withAvg − withoutAvg`, rounded. Signed, never causal." pooledSd: - anyOf: - - type: number - - type: "null" description: Pooled mood SD across the two groups (the Cohen's-d denominator); null when neither group has testable spread. Ranking input only — not rendered. + type: + - number + - "null" pValue: type: number description: Welch two-sided p-value. @@ -66925,15 +66926,15 @@ components: description: The expected pleasantness (A1) for that day, 0-10, on the account's own past pattern. A counterfactual, never a measurement — and never to be displayed without `n` and the band. ciLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" ciHigh: - anyOf: - - type: number - - type: "null" description: The band, from the validation residuals. It widens when the model fits badly, so a wide band is the signal to present the value cautiously. + type: + - number + - "null" n: type: integer minimum: -9007199254740991 @@ -66945,7 +66946,7 @@ components: computedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ ageDays: anyOf: - type: integer @@ -66992,10 +66993,10 @@ components: description: Where the self-assessment sat against the BAND, not against `predicted`. `within` means the day went as the account's own past days suggest. deviationAmount: - anyOf: - - type: number - - type: "null" description: How far outside the band, in scale points. `0` when inside. + type: + - number + - "null" contributions: type: array items: @@ -67199,17 +67200,17 @@ components: key: type: string labelKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" custom: type: boolean tags: @@ -67232,17 +67233,17 @@ components: key: type: string labelKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" kind: type: string enum: @@ -67294,13 +67295,13 @@ components: labelKey: type: "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" kind: type: string enum: @@ -67353,13 +67354,13 @@ components: labelKey: type: "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" kind: type: string enum: @@ -67467,13 +67468,13 @@ components: labelKey: type: "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" custom: type: boolean const: true @@ -67507,13 +67508,13 @@ components: labelKey: type: "null" label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" custom: type: boolean const: true @@ -67608,7 +67609,7 @@ components: synthesise it." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - groupOrder - placements @@ -67744,10 +67745,10 @@ components: type: string description: Free-text tag list, decoded from storage; `[]` when none. note: - anyOf: - - type: string - - type: "null" description: Free-text note, decrypted for the response. AES-256-GCM at rest; the ciphertext never rides the wire. + type: + - string + - "null" source: type: string enum: @@ -67758,31 +67759,31 @@ components: - DAYLIO - NOTIFICATION externalId: - anyOf: - - type: string - - type: "null" description: The sync client's source-stable dedup id, or null. + type: + - string + - "null" moodLoggedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tz: - anyOf: - - type: string - - type: "null" description: IANA zone the `date` label is anchored to. Null on legacy rows (read as Europe/Berlin). + type: + - string + - "null" syncedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ syncVersion: type: integer minimum: 0 @@ -68092,10 +68093,10 @@ components: type: string description: Symptoms linked to the day-log, by catalogue key. episodeId: - anyOf: - - type: string - - type: "null" description: The episode to open in the illness module, when any. + type: + - string + - "null" required: - available - logged @@ -68228,11 +68229,11 @@ components: type: boolean description: Whether a bot token is stored. The token itself is never returned. chatId: - anyOf: - - type: string - - type: "null" description: The stored chat id, returned in full. It is an addressing value, not a credential — the bot token is what authorises a send. + type: + - string + - "null" required: - enabled - hasBotToken @@ -68745,9 +68746,9 @@ components: signedAt: type: string revokedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string required: @@ -68865,17 +68866,17 @@ components: signedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ revokedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - userId @@ -69061,12 +69062,12 @@ components: startedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ completedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" uploadBytes: type: integer @@ -69091,13 +69092,13 @@ components: count), `warnings` (codes for tables or columns the importer could not read), `totals`. Never a value, a timestamp or text from the file." failureReason: - anyOf: - - type: string - - type: "null" description: Why the job failed. Starts with a code the client can key on when the file was refused — `unsupported_version` (database older than version 9), `not_health_connect` (no `health_connect_export.db`, or not a Health Connect database), `unsafe_schema` (a record table is a view), `too_large`, `staging_missing` — else free text. + type: + - string + - "null" required: - jobId - status @@ -69294,12 +69295,12 @@ components: startedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ completedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" uploadBytes: type: integer @@ -69309,7 +69310,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The instant the Health app stamped on the export itself, once the worker has read it. Null before then. progress: @@ -69330,10 +69331,10 @@ components: (`discovered` / `imported` / `updated` / `skipped` / `failed`) are normalised to bounded non-negative integers before serving. failureReason: - anyOf: - - type: string - - type: "null" description: Free text on `failed`; null otherwise. + type: + - string + - "null" required: - jobId - status @@ -69394,20 +69395,20 @@ components: maximum: 9007199254740991 - type: "null" rrule: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" anchorDate: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" endsOn: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" origin: type: string @@ -69419,32 +69420,32 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" nextDueAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastSatisfiedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastSkippedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" skipCount: type: integer @@ -69455,11 +69456,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - label @@ -69724,7 +69725,7 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ onTime: type: boolean source: @@ -69740,7 +69741,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -69800,67 +69801,67 @@ components: id: type: string biomarkerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" analyte: type: string value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unit: type: string converted: type: boolean sourceValue: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" printedReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceOrigin: $ref: "#/components/schemas/LabReferenceRangeOrigin" referenceDivergesFromCatalog: @@ -70002,9 +70003,9 @@ components: trend: type: array items: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" latest: $ref: "#/components/schemas/LabResult" required: @@ -70049,67 +70050,67 @@ components: id: type: string biomarkerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" analyte: type: string value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unit: type: string converted: type: boolean sourceValue: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" printedReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceOrigin: $ref: "#/components/schemas/LabReferenceRangeOrigin" referenceDivergesFromCatalog: @@ -70125,9 +70126,9 @@ components: updatedAt: type: string note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - biomarkerId @@ -70381,67 +70382,67 @@ components: id: type: string biomarkerId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" analyte: type: string value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unit: type: string converted: type: boolean sourceValue: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceUnit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" catalogReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" sourceReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" printedReferenceText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceOrigin: type: string enum: @@ -70531,10 +70532,10 @@ components: - DOCUMENT_EXTRACT - SUPPLEMENT_LABEL_EXTRACT documentId: - anyOf: - - type: string - - type: "null" description: The document a document run reads; null for a lab scan. + type: + - string + - "null" status: type: string enum: @@ -70583,13 +70584,13 @@ components: createdAt: type: string startedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" finishedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" queuedForMs: anyOf: - type: integer @@ -70644,9 +70645,9 @@ components: type: object properties: reportDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" providerType: type: string rows: @@ -70683,37 +70684,37 @@ components: analyte: type: string value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" valueText: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" unit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" referenceLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" referenceText: - anyOf: - - type: string - - type: "null" description: The reference range EXACTLY as printed on the report, verbatim ("3,5 - 5,0", "< 116", "bis 5,0", "negativ"). `referenceLow` / `referenceHigh` carry the same window reduced to numbers WHEN it reduces to numbers; this field carries it either way, so a window stated in words is not lost between the report and the reading. + type: + - string + - "null" takenAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" confidence: type: object properties: @@ -70737,30 +70738,30 @@ components: - new - existing markerUnit: - anyOf: - - type: string - - type: "null" description: "The unit the matched marker is tracked in, or null when the marker does not exist yet (it will adopt this row's unit). A row whose `unit` differs from it is converted on commit when the marker's analyte is known and the pair is in the unit-conversion registry; otherwise the commit skips it with `reason: unit_mismatch`, and the review screen marks it before Save." + type: + - string + - "null" markerAnalyteKey: - anyOf: - - type: string - - type: "null" description: The analyte the matched marker is (the catalog slug, e.g. `fasting-glucose` or `ldl`, or a registry-only key such as `urea`), or null when the marker does not exist yet or its name matches none. With `markerUnit` it tells a client whether a row in another unit can be converted on commit. + type: + - string + - "null" duplicateOf: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" biomarkerId: description: v1.43 — the marker this row was matched to automatically (by name, by an alias of the account, or by a catalogue abbreviation); null without a match. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" matchKind: description: v1.43 — how `biomarkerId` was found. anyOf: @@ -70835,9 +70836,9 @@ components: type: object properties: title: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" kind: anyOf: - type: string @@ -70854,9 +70855,9 @@ components: - OTHER - type: "null" documentDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - title - kind @@ -70929,10 +70930,10 @@ components: - printedText additionalProperties: false servingNote: - anyOf: - - type: string - - type: "null" description: e.g. "per 2 capsules"; shown, never stored. + type: + - string + - "null" required: - ingredients - unmatched @@ -70977,27 +70978,27 @@ components: unit: type: string lowerBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" upperBound: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" panel: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" analyteKey: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasContext: type: boolean context: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hidden: type: boolean aliases: @@ -71182,10 +71183,10 @@ components: type: string description: Names that become aliases of the target. mergeId: - anyOf: - - type: string - - type: "null" description: "`null` on a dry run." + type: + - string + - "null" required: - dryRun - target @@ -71262,17 +71263,17 @@ components: - $ref: "#/components/schemas/Practitioner" - type: "null" reason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" outcome: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" bodySite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" laterality: anyOf: - type: string @@ -71282,9 +71283,9 @@ components: - BOTH - type: "null" reminderNextDueAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" links: $ref: "#/components/schemas/EncounterLinks" skipped: @@ -71321,25 +71322,25 @@ components: name: type: string specialty: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" practice: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" phone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -71387,13 +71388,13 @@ components: id: type: string label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" date: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" redacted: type: boolean required: @@ -71653,13 +71654,13 @@ components: onsetAt: type: string resolvedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" bodySite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" laterality: anyOf: - type: string @@ -71779,9 +71780,9 @@ components: - OTHER - PROCEDURE practitionerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - occurredAt @@ -72086,11 +72087,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - name @@ -72208,13 +72209,13 @@ components: occurredAt: type: string antigenSlug: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" vaccineName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" doseNumber: anyOf: - type: integer @@ -72228,9 +72229,9 @@ components: maximum: 9007199254740991 - type: "null" lotNumber: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" site: anyOf: - type: string @@ -72248,10 +72249,10 @@ components: - $ref: "#/components/schemas/VaccinationCatalogEntry" - type: "null" customVaccineId: - anyOf: - - type: string - - type: "null" description: The record's own vaccine definition the dose names (v1.42), verbatim. + type: + - string + - "null" customVaccine: anyOf: - $ref: "#/components/schemas/CustomVaccine" @@ -72271,13 +72272,13 @@ components: - $ref: "#/components/schemas/VaccinationEncounterStub" - type: "null" reminderId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" documents: type: array items: @@ -72368,25 +72369,25 @@ components: name: type: string specialty: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" practice: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" location: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" phone: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -72424,13 +72425,13 @@ components: id: type: string label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" date: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" redacted: type: boolean required: @@ -72689,13 +72690,13 @@ components: occurredAt: type: string antigenSlug: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" vaccineName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - occurredAt @@ -72811,21 +72812,21 @@ components: onsetAt: type: string resolvedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" parentConditionId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" bodySite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" laterality: anyOf: - type: string @@ -73010,17 +73011,17 @@ components: maximum: 9007199254740991 - type: "null" feverC: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" symptoms: type: array items: $ref: "#/components/schemas/IllnessSymptom" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" updatedAt: type: string required: @@ -73205,9 +73206,9 @@ components: - computedAt additionalProperties: false reason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - episodeId - status @@ -73238,21 +73239,21 @@ components: items: $ref: "#/components/schemas/IllnessVitalReturn" recoveryGapDays: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" adverseCoverageDays: type: integer minimum: -9007199254740991 maximum: 9007199254740991 feltBetterDay: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" gapDriverType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" redFlags: type: array items: @@ -73314,13 +73315,13 @@ components: type: type: string returnedDay: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" gapDays: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" adverse: type: boolean required: @@ -73413,9 +73414,9 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 typicalRecoveryGapDays: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" gapSampleSize: type: integer minimum: -9007199254740991 @@ -73437,9 +73438,9 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 gapDriverType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - windowDays - episodeCount @@ -73504,9 +73505,9 @@ components: item9Flagged: type: boolean crisisShownAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" takenAt: type: string createdAt: @@ -73636,9 +73637,9 @@ components: item9Flagged: type: boolean crisisShownAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" takenAt: type: string createdAt: @@ -73732,17 +73733,17 @@ components: - INACTIVE - RESOLVED onsetAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reaction: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -73887,9 +73888,9 @@ components: maximum: 9007199254740991 - type: "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -74004,9 +74005,9 @@ components: items: $ref: "#/components/schemas/InboundDocument" nextCursor: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - documents - nextCursor @@ -74032,13 +74033,13 @@ components: - SICK_NOTE - OTHER title: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" filename: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" mimeType: type: string byteSize: @@ -74053,21 +74054,21 @@ components: - CONFIRMED - DISCARDED providerType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reportDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" documentDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" errorReason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" factCount: type: number pendingCount: @@ -74097,9 +74098,9 @@ components: - local-ocr - type: "null" lastIndexAttemptAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastIndexOutcome: anyOf: - type: string @@ -74123,9 +74124,9 @@ components: - OTHER - type: "null" sourceId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -74191,9 +74192,9 @@ components: kind: type: string occurredAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - encounterId - kind @@ -74226,9 +74227,9 @@ components: type: object properties: id: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" duplicate: type: boolean deleted: @@ -74266,9 +74267,9 @@ components: known: type: boolean id: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" deleted: type: boolean required: @@ -74384,15 +74385,15 @@ components: occurredAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ reason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" bodySite: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" laterality: anyOf: - type: string @@ -74402,9 +74403,9 @@ components: - BOTH - type: "null" practitionerName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - encounterId - occurredAt @@ -74531,9 +74532,9 @@ components: ok: type: boolean error: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - ok @@ -74587,13 +74588,13 @@ components: - SICK_NOTE - OTHER title: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" filename: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" mimeType: type: string byteSize: @@ -74608,21 +74609,21 @@ components: - CONFIRMED - DISCARDED providerType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" reportDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" documentDate: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" errorReason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" factCount: type: number pendingCount: @@ -74652,9 +74653,9 @@ components: - local-ocr - type: "null" lastIndexAttemptAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastIndexOutcome: anyOf: - type: string @@ -74678,9 +74679,9 @@ components: - OTHER - type: "null" sourceId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -74699,13 +74700,13 @@ components: under a grant that does not cover the health background the doses live in: the page is shared, the doses are not, and an empty list would claim the page records none." summary: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" summaryGeneratedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" summaryState: type: string enum: @@ -74791,13 +74792,13 @@ components: anchored: type: boolean sourceOffset: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" page: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" confidence: type: number required: @@ -74808,19 +74809,19 @@ components: - confidence additionalProperties: false committedRecordId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" committedRecordType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" biomarkerId: description: v1.43, OBSERVATION only — the marker matched automatically (name, alias or catalogue abbreviation); null without a match. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" matchKind: description: v1.43, OBSERVATION only — how `biomarkerId` was found. anyOf: @@ -74875,17 +74876,17 @@ components: vaccinationId: type: string occurredAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" catalogSlug: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" vaccineName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - vaccinationId - occurredAt @@ -75063,9 +75064,9 @@ components: items: $ref: "#/components/schemas/DocumentChatMessage" summary: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - title @@ -75090,17 +75091,17 @@ components: createdAt: type: string providerType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" tokensUsed: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" model: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - role @@ -75118,9 +75119,9 @@ components: items: $ref: "#/components/schemas/DocumentChatConversation" nextCursor: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - conversations - nextCursor @@ -75292,21 +75293,21 @@ components: unit: type: string targetLow: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" targetHigh: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" decimals: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" description: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" correlationEnabled: type: boolean latest: @@ -75477,9 +75478,9 @@ components: measuredAt: type: string note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string required: @@ -75610,7 +75611,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "When the needs flow was completed. Distinct from the account payload's `onboardingCompletedAt`, which stays the first-run redirect's gate: `POST /api/onboarding/restart` clears this one and leaves that one @@ -75767,16 +75768,16 @@ components: - add-medication - log-reading target: - anyOf: - - type: string - - type: "null" description: What the task is about — a source key for a connection, an area key for a reading. Null for the medication task. + type: + - string + - "null" completedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the offered task produced its result, or null. required: @@ -76098,9 +76099,9 @@ components: - type: "null" latestAboveUpperLimit: description: v1.43 — whether the latest day is above the upper limit; null without one. - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" required: - nutrient - unit @@ -76115,7 +76116,7 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ topReason: type: string required: @@ -76187,9 +76188,9 @@ components: $ref: "#/components/schemas/NutrientAmountBySource" aboveUpperLimit: description: v1.43 — whether the day is above the upper limit; null without one. - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" required: - day - amount @@ -76303,7 +76304,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When HealthKit last synced for this user; null when never (and always null on the PATCH echo). lastSyncTrigger: @@ -76326,10 +76327,11 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" syncHealth: - description: Present on the GET read; omitted from the PATCH echo. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Present on the GET read; omitted from the PATCH echo." $ref: "#/components/schemas/SyncHealth" metricFreshness: description: Per-metric-type freshness for the `APPLE_HEALTH` source, with the per-type arrival facts. Present on the @@ -76401,7 +76403,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The instant that triggered the verdict; null when none did. required: @@ -76421,7 +76423,7 @@ components: lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Newest recorded reading for this type from this source. stale: type: boolean @@ -76436,7 +76438,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastTrigger: description: The `syncTrigger` of that batch. Null when the client reported none. A type whose trigger is only ever @@ -76456,7 +76458,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastBackgroundAt: description: 'When a batch with `syncTrigger: "background"` last carried this type (since v1.42.4). A later @@ -76468,7 +76470,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastBackfillAt: description: 'v1.43 — when a batch with `syncTrigger: "backfill"` last carried this type. A backfill moves only this and @@ -76477,7 +76479,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" required: - type @@ -76502,7 +76504,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Earliest instant reached across those rows (`measuredAt` for measurements, `startedAt` for workouts); null when nothing has arrived. During a first-run backfill this walks backwards as history lands. @@ -76547,7 +76549,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: When the current degraded / action_required condition started. Null for ok and limited. action: @@ -76582,14 +76584,14 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Last successful sync. For Apple Health, the last accepted batch. lastDataAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "Newest data this source delivered: for polled providers when the newest reading or workout was taken, for Apple Health when a sync last brought a new sample. Null when nothing has arrived." @@ -76597,10 +76599,10 @@ components: type: object properties: ledgerState: - anyOf: - - type: string - - type: "null" description: The stored sync-ledger state. Null for Apple Health. + type: + - string + - "null" failingLegs: type: array items: @@ -76609,13 +76611,13 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastAttemptAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" verdict: type: string @@ -76640,12 +76642,12 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ errorCode: - anyOf: - - type: string - - type: "null" description: Short error code (`401`, `invalid_grant`, `scope_missing`); anything not shaped like one is dropped. + type: + - string + - "null" required: - endpoint - status @@ -76979,6 +76981,8 @@ components: instance. False for every public instance. type: boolean state: + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." type: string enum: - connected @@ -76987,30 +76991,28 @@ components: - disconnected - parked - unknown - description: "Last recorded ledger state for this provider. `unknown` (added v1.38.19) is what a user who has never - synced sees: the ledger row is created on the first attempt, and its absence is no claim about the - connection in either direction — it used to read as `connected`, which is why a client must never treat this - field as liveness. Read `syncHealth.verdict` for that, and treat `unknown` as `disconnected` anywhere a - ledger state is painted." lastSuccessAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastAttemptAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastError: - description: Last recorded failure message, decrypted from the ledger. Null when the last attempt succeeded or none has - run. - anyOf: - - type: string - - type: "null" + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Last recorded failure message, decrypted from the ledger. + Null when the last attempt succeeded or none has run." + type: + - string + - "null" syncHealth: + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." $ref: "#/components/schemas/SyncHealth" health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to @@ -77190,30 +77192,32 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" connectedAt: description: When the connection row was created. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tokenExpired: - description: The stored access token's expiry is in the past. Read straight off the row — unlike the Withings status - read, this endpoint does not refresh; the sync path refreshes lazily instead. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. The stored access token's expiry is in the past. Read + straight off the row — unlike the Withings status read, this endpoint does not refresh; the sync path + refreshes lazily instead." type: boolean tokenExpiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ backfillCompleted: description: The self-converging history backfill has walked every collection to completion. type: boolean scope: description: Space-separated OAuth scope string granted by WHOOP. Null on a legacy connection that predates scope tracking. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to none. Absent on the not-connected body. @@ -77280,14 +77284,16 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" connectedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tokenExpired: - description: True after a refresh attempt that did not produce a live token. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. True after a refresh attempt that did not produce a live + token." type: boolean tokenRefreshFailed: description: The read tried to refresh an expired token and could not — either the per-user credentials are gone or @@ -77298,15 +77304,17 @@ components: description: Post-refresh expiry when the read refreshed, the stored expiry otherwise. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ scope: description: Comma-separated OAuth scope string from the authorisation flow. Null on a legacy connection that has never re-authed. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasActivityScope: - description: Pre-computed convenience flag the reconnect banner reads. A null `scope` counts as missing activity. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Pre-computed convenience flag the reconnect banner reads. A + null `scope` counts as missing activity." type: boolean health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to @@ -77607,27 +77615,29 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" connectedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tokenExpired: - description: Read straight off the row — this endpoint does not refresh; the sync path refreshes lazily. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Read straight off the row — this endpoint does not refresh; + the sync path refreshes lazily." type: boolean tokenExpiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ backfillCompleted: description: The history backfill has walked every collection. type: boolean scope: description: OAuth scope string granted at authorisation. Null on a legacy connection that predates scope tracking. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to none. Absent on the not-connected body. @@ -77776,27 +77786,29 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" connectedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ tokenExpired: - description: Read straight off the row — this endpoint does not refresh; the sync path refreshes lazily. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Read straight off the row — this endpoint does not refresh; + the sync path refreshes lazily." type: boolean tokenExpiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ backfillCompleted: description: The history backfill has walked every collection. type: boolean scope: description: OAuth scope string granted at authorisation. Null on a legacy connection that predates scope tracking. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to none. Absent on the not-connected body. @@ -77804,10 +77816,11 @@ components: - $ref: "#/components/schemas/ConnectionHealth" - type: "null" needsReauth: - description: "The stored refresh token was rejected with `invalid_grant` — Google expires it after 7 days while the - OAuth client is in Testing publishing mode, and a user-revoked grant lands here too. Distinct from `parked` - and from `disconnected`: the connection still exists, it just cannot mint a token until the user reconnects. - Only Google Health has this field." + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. The stored refresh token was rejected with `invalid_grant` — + Google expires it after 7 days while the OAuth client is in Testing publishing mode, and a user-revoked + grant lands here too. Distinct from `parked` and from `disconnected`: the connection still exists, it just + cannot mint a token until the user reconnects. Only Google Health has this field." type: boolean required: - connected @@ -78007,6 +78020,8 @@ components: type: boolean description: The user has stored their own BYO client id and secret. state: + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." type: string enum: - connected @@ -78015,28 +78030,27 @@ components: - disconnected - parked - unknown - description: "Last recorded ledger state for this provider. `unknown` (added v1.38.19) is what a user who has never - synced sees: the ledger row is created on the first attempt, and its absence is no claim about the - connection in either direction — it used to read as `connected`, which is why a client must never treat this - field as liveness. Read `syncHealth.verdict` for that, and treat `unknown` as `disconnected` anywhere a - ledger state is painted." lastSuccessAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastAttemptAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastError: - anyOf: - - type: string - - type: "null" + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." + type: + - string + - "null" syncHealth: + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." $ref: "#/components/schemas/SyncHealth" health: description: The unified connection state. Present whenever the connection exists; null in the rare case it resolved to @@ -78396,8 +78410,8 @@ components: $ref: "#/components/schemas/IntegrationStatusEntry" description: "All nine providers, always, in the fixed order withings, whoop, fitbit, google-health, polar, oura, strava, nightscout, dawarich. A provider the user has never touched still appears, with `connected: false` - and a synthesised `connected` ledger state — absence from this list never means anything, because nothing is - ever absent from it." + and the ledger state `unknown` — absence from this list never means anything, because nothing is ever absent + from it." required: - threshold - metricFreshnessDegraded @@ -78431,27 +78445,26 @@ components: - disconnected - parked - unknown - description: "Last recorded ledger state for this provider. `unknown` (added v1.38.19) is what a user who has never - synced sees: the ledger row is created on the first attempt, and its absence is no claim about the - connection in either direction — it used to read as `connected`, which is why a client must never treat this - field as liveness. Read `syncHealth.verdict` for that, and treat `unknown` as `disconnected` anywhere a - ledger state is painted." + description: "Deprecated: removed in v1.44, read `health`." + deprecated: true lastSuccessAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastAttemptAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastError: - anyOf: - - type: string - - type: "null" + deprecated: true + description: "Deprecated: removed in v1.44, read `health`." + type: + - string + - "null" consecutiveFailuresByKind: anyOf: - type: object @@ -78474,9 +78487,13 @@ components: - persistent additionalProperties: false - type: "null" - description: Per-kind consecutive-failure counters, or null when the row has never written a bucket payload. Compare - against the top-level `threshold` to know how close the provider is to the operator alert. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Per-kind consecutive-failure counters, or null when the row + has never written a bucket payload. Compare against the top-level `threshold` to know how close the provider + is to the operator alert." syncHealth: + description: "Deprecated: removed in v1.44, read `health`." + deprecated: true $ref: "#/components/schemas/SyncHealth" metricFreshness: type: array @@ -78494,7 +78511,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" legacyLastSyncedAt: description: The connection row's own last-sync instant, for legacy connections established before the ledger existed. @@ -78502,35 +78519,39 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" tokenExpiresAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" tokenExpired: - description: Null — not false — when there is no connection to judge. - anyOf: - - type: boolean - - type: "null" + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Null — not false — when there is no connection to judge." + type: + - boolean + - "null" backfillCompleted: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" scope: description: Withings only. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasActivityScope: - description: Withings only. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Withings only." type: boolean planUnavailable: - description: Withings only. The readings this connection's Withings API plan withholds (status 277 since the 2026-10-12 - plan split), deduplicated and in display order. Empty when the plan refuses nothing. The sync skips these - and keeps importing everything else; the list refreshes when the weekly re-probe asks again. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Withings only. The readings this connection's Withings API + plan withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when + the plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when + the weekly re-probe asks again." type: array items: type: string @@ -78558,7 +78579,8 @@ components: - bloodPressure - weight needsReauth: - description: Google Health only. + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Google Health only." type: boolean available: description: Oura / Polar / Strava only. @@ -78567,9 +78589,11 @@ components: description: Oura / Polar / Strava only. type: boolean scopeReconnect: - description: "Oura only. Collections the stored grant does not cover: they answered 401 while sibling collections - succeeded with the same token. Not a failure and not counted toward the reconnect threshold; one reconnect - asks for the missing scopes. Empty when the grant covers everything HealthLog reads." + deprecated: true + description: "Deprecated: removed in v1.44, read `health`. Oura only. Collections the stored grant does not cover: they + answered 401 while sibling collections succeeded with the same token. Not a failure and not counted toward + the reconnect threshold; one reconnect asks for the missing scopes. Empty when the grant covers everything + HealthLog reads." type: array items: type: string @@ -78590,8 +78614,7 @@ components: - type: "null" description: The unified connection state, null when the provider is not connected. It supersedes `state`, `lastError`, `consecutiveFailuresByKind`, `syncHealth`, `tokenExpired`, `needsReauth`, `hasActivityScope`, - `planUnavailable` and `scopeReconnect` on this entry, which go once the settings surface renders `health` - (no other client reads this envelope). + `planUnavailable` and `scopeReconnect` on this entry; those nine are deprecated and removed in v1.44. required: - integration - state @@ -78620,7 +78643,7 @@ components: lastSeenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Newest recorded reading for this type from this source. stale: type: boolean @@ -78693,7 +78716,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The connection row's last successful sync; null when it has never synced. Present here and absent from the `/api//test` family — the one shape difference between the two probe families. @@ -78752,7 +78775,7 @@ components: probedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ types: type: object propertyNames: @@ -78878,9 +78901,9 @@ components: band: type: string delta: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" configured: description: True when the account's own recipe narrows the score's composition below what its defaults would resolve to today. Resolved on the server and carried straight from the dashboard snapshot; the configuration @@ -78888,9 +78911,9 @@ components: type: boolean deltaReason: description: Why a delta was suppressed; null when it was not. - anyOf: - - type: string - - type: "null" + type: + - string + - "null" scoreVersion: description: Algorithm version that produced the value. type: integer @@ -78945,9 +78968,9 @@ components: nudge: type: string delta: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - sourceMetric - tone @@ -78959,12 +78982,12 @@ components: description: Clinical-priority top signal, lifted from the cached briefing. Null while the `briefing` capability is unavailable. briefingLead: - anyOf: - - type: string - - type: "null" description: "The first sentence of the cached briefing paragraph that says something: a greeting or a sentence that only repeats the score is skipped. Null while the `briefing` capability is unavailable or when no sentence qualifies; `line` then falls to its deterministic floor." + type: + - string + - "null" lead: anyOf: - type: object @@ -78993,15 +79016,15 @@ components: - type: object properties: headline: - anyOf: - - type: string - - type: "null" description: The top signal's headline, or null when the lead already talks about its metric. + type: + - string + - "null" delta: - anyOf: - - type: string - - type: "null" description: The top signal's delta, when it has one. + type: + - string + - "null" required: - headline - delta @@ -79069,13 +79092,13 @@ components: description: The day's newest data arrival while it is still news (under three hours old), else null. Additive since v1.31.0. reactionLine: - anyOf: - - type: string - - type: "null" description: One-sentence generated reaction to that arrival, standing for the rest of the local day. Null whenever no line was generated (no provider, no consent, budget exhausted, or generation failed), and whenever the `reactionLines` capability is unavailable, however recent the stored line — consumers fall back to `briefingLead` / `line`. Additive since v1.31.0. + type: + - string + - "null" ai: type: object properties: @@ -79286,7 +79309,7 @@ components: date: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Build instant. required: - salutation @@ -79354,7 +79377,7 @@ components: lastUpdated: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: Build instant of the cached body. required: - greeting @@ -79382,26 +79405,26 @@ components: description: i18n key for the metric's title (e.g. `dashboard.metric.title.weight`). The wire is language-neutral — resolve the key against your own bundle. latestValue: - anyOf: - - type: number - - type: "null" description: Most recent reading regardless of age, systolic for `bloodPressure`, last night's time asleep in HOURS for `sleep`, the latest daily mean for `mood`, and the derived figure for `bmi`. Null when the card is alive on history alone. + type: + - number + - "null" secondaryValue: - anyOf: - - type: number - - type: "null" description: Diastolic value on the `bloodPressure` card; null on every other kind. + type: + - number + - "null" unitKey: type: string description: i18n key for the display unit, resolved the same way. unit: - anyOf: - - type: string - - type: "null" description: Explicit unit token, set only on the `sleep` card (`h`, because its value is a per-night total in hours rather than the canonical SLEEP_DURATION minutes). Null on every other kind. + type: + - string + - "null" sleepStages: anyOf: - type: object @@ -79428,9 +79451,9 @@ components: source: type: string deviceType: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" asleepMinutes: type: integer minimum: 0 @@ -79467,7 +79490,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Instant behind `latestValue`, falling back to the metric's newest reading. For `mood` this is the first instant of the latest daily bucket's day in the account's time zone, not a reading time (the series pins a @@ -79483,7 +79506,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The metric's newest reading, for the 'last reading N days ago' caption when it is older than the sparkline window. @@ -79777,7 +79800,7 @@ components: `baseUpdatedAt` on the next write. Opaque." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ unavailableWidgetIds: description: "Widget ids whose toggle cannot do anything for this account, and which a settings UI should therefore not offer — a switch over a tile that can never paint. Resolved per request from the caller's enabled modules @@ -80039,7 +80062,7 @@ components: `baseUpdatedAt` on the next write. Opaque." type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - version additionalProperties: false @@ -80103,11 +80126,11 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ createdByYou: type: boolean required: @@ -80153,7 +80176,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - token @@ -80210,7 +80233,7 @@ components: claimedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ guardians: type: array items: @@ -80420,13 +80443,13 @@ components: type: string minLength: 1 displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" access: type: string enum: @@ -80510,13 +80533,13 @@ components: type: string minLength: 1 displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" fullName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" access: type: string enum: @@ -81074,11 +81097,11 @@ components: this session. Send it back as the `X-HealthLog-Record-Epoch` header on every subsequent same-origin request, and as `expectedEpoch` when switching. scope: - anyOf: - - type: string - - type: "null" description: The account this session is pointed at, or null for its own record. Send it back as the `X-HealthLog-Record-Scope` header, with `self` standing for null. + type: + - string + - "null" required: - epoch - scope @@ -81168,9 +81191,9 @@ components: username: type: string displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - username @@ -81250,21 +81273,21 @@ components: invitedAt: type: string acceptedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" expiresAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastUsedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" revokedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" revokedBy: anyOf: - type: string @@ -81395,21 +81418,21 @@ components: invitedAt: type: string acceptedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" expiresAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" lastUsedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" revokedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" revokedBy: anyOf: - type: string @@ -81481,15 +81504,15 @@ components: description: The profile's account id — the value `POST /api/account/switch` and the `X-HealthLog-Account` selector take, and the `{id}` in this family's paths. displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dateOfBirth: - anyOf: - - type: string - - type: "null" description: "`YYYY-MM-DD`, or null. The same date-only shape the request takes — this is a calendar date, not an instant, and publishing it with a time would invite a client to apply a zone to it." + type: + - string + - "null" gender: anyOf: - type: string @@ -81500,12 +81523,12 @@ components: - type: "null" description: "`User.gender` for this record. Null means not recorded, which is a real answer and not a missing one." locale: - anyOf: - - type: string - - type: "null" description: The record's own language. Every profile this family creates carries one, because creation requires it; the column is nullable, so null means a row written before that and a client renders its own language rather than the word. + type: + - string + - "null" timezone: type: string recordKind: @@ -81590,9 +81613,9 @@ components: invitedAt: type: string acceptedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - grantId - account @@ -81688,9 +81711,9 @@ components: username: type: string displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" access: type: string enum: @@ -82104,11 +82127,11 @@ components: (`disabledReason` says why). `manually_disabled` — the user turned it off. `sending_paused` — still enabled but inside a retry cooldown until `nextRetryAt`. `active` — enabled and sending. disabledReason: - anyOf: - - type: string - - type: "null" description: Why the dispatcher auto-disabled the channel; null when it did not. This is the field that separates `auto_disabled` from `manually_disabled`. + type: + - string + - "null" consecutiveFailures: type: integer minimum: -9007199254740991 @@ -82117,23 +82140,23 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastFailureAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastFailureReason: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" nextRetryAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: End of the current cooldown; null when not paused. required: @@ -82157,7 +82180,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Null when this category has never been delivered. required: @@ -82221,12 +82244,12 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ lastUsedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: Null when the connection has never been exercised. required: @@ -82299,18 +82322,18 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" expiresAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ revoked: type: boolean description: Revoked tokens stay in this list rather than disappearing from it, so a client must filter if it wants live @@ -82356,7 +82379,7 @@ components: expiresAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - token - name @@ -82416,9 +82439,9 @@ components: type: string description: Empty string when the record has no display name set. locale: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timezone: type: string kind: @@ -82487,13 +82510,13 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" lastAttemptAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" health: anyOf: @@ -82575,13 +82598,13 @@ components: type: object properties: displayName: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" heightCm: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dateOfBirth: anyOf: - type: string @@ -82976,7 +82999,7 @@ components: timestamp: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: When the probe ran. UTC (`Z`) — no user context is loaded. database: type: string @@ -82995,7 +83018,7 @@ components: description: Last worker heartbeat. Absent when the worker has never beaten in this process. type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - status - timestamp @@ -83064,11 +83087,11 @@ components: metricType: $ref: "#/components/schemas/MeasurementType" metricSlot: - anyOf: - - type: string - - type: "null" description: Per-sport bucket for workout-driven bests (e.g. "running_5km_time"). Null for a plain measurement-driven best, where the metric type alone is the dimension. + type: + - string + - "null" direction: type: string enum: @@ -83083,23 +83106,23 @@ components: achievedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ sourceMeasurementId: - anyOf: - - type: string - - type: "null" description: The measurement that achieved the record. Nulled rather than cascaded when that measurement is later deleted, so the historical fact survives the reading behind it. + type: + - string + - "null" source: $ref: "#/components/schemas/MeasurementSource" externalId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ benchmark: description: v1.43 — an age- and sex-aware comparison band where a cited norm exists, in the record's unit; null otherwise. @@ -83270,7 +83293,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" isHidden: type: boolean @@ -83315,7 +83338,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" progress: type: number @@ -83418,15 +83441,15 @@ components: maximum: 9007199254740991 description: Stored daily observations for this account. latestDate: - anyOf: - - type: string - - type: "null" description: Day key of the newest stored observation. + type: + - string + - "null" latestFetchedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" required: - days @@ -83440,7 +83463,7 @@ components: lastFailedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: When the newest exhausted-retry failure gave up. failures: type: integer @@ -83473,16 +83496,16 @@ components: maximum: 9007199254740991 description: Stored days the air-quality feed returned hourly values for. latestDate: - anyOf: - - type: string - - type: "null" description: Newest such day (YYYY-MM-DD). + type: + - string + - "null" domain: - anyOf: - - type: string - - type: "null" description: "The model domain of that day: `cams_europe` (about 11 km, with pollen) or `cams_global` (about 45 km, no pollen)." + type: + - string + - "null" history: anyOf: - type: object @@ -83530,83 +83553,83 @@ components: type: string description: YYYY-MM-DD of the newest stored day. tempMin: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" tempMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" apparentMax: - anyOf: - - type: number - - type: "null" description: Daily feels-like maximum, °C. + type: + - number + - "null" airQuality: anyOf: - type: object properties: pm25Mean: - anyOf: - - type: number - - type: "null" description: PM2.5 daily mean, µg/m³. + type: + - number + - "null" pm10Mean: - anyOf: - - type: number - - type: "null" description: PM10 daily mean, µg/m³. + type: + - number + - "null" no2Mean: - anyOf: - - type: number - - type: "null" description: NO2 daily mean, µg/m³. + type: + - number + - "null" o3Max8h: - anyOf: - - type: number - - type: "null" description: Ozone, highest 8-hour running mean, µg/m³. + type: + - number + - "null" eaqiMax: - anyOf: - - type: number - - type: "null" description: European air-quality index, daily maximum. + type: + - number + - "null" uvIndexMax: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" dustMax: - anyOf: - - type: number - - type: "null" description: Dust, µg/m³. + type: + - number + - "null" pollen: type: object properties: alder: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" birch: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" grass: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" mugwort: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" olive: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" ragweed: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - alder - birch @@ -83667,9 +83690,9 @@ components: type: number description: Coarse longitude, rounded to 1 decimal. label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" timezone: type: string description: The home's IANA zone. Falls back to the account's display timezone when the home was stored without one. @@ -83677,7 +83700,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: "Effective-from instant. Every set/update re-stamps it: a home resolves days from the moment it was set, and days before it stay un-attributed unless a travel override covers them." @@ -83968,9 +83991,9 @@ components: userId: type: string username: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" backupId: type: string restoreInstanceSettings: @@ -84039,13 +84062,13 @@ components: createdAt: type: string startedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" completedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - id - userId @@ -84227,16 +84250,16 @@ components: baseUrl: type: string organizationId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" hasToken: type: boolean const: true lastVerifiedAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" originAllowed: type: boolean required: @@ -84404,9 +84427,9 @@ components: title: type: string date: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" tags: type: array items: @@ -84424,9 +84447,9 @@ components: - imported - deleted documentId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - sourceId - title @@ -84463,9 +84486,9 @@ components: - duplicate - deleted documentId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" linked: type: boolean required: @@ -84529,13 +84552,13 @@ components: id: type: string label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" icon: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" sortOrder: type: integer minimum: -9007199254740991 @@ -84556,9 +84579,9 @@ components: maximum: 9007199254740991 - type: "null" lastOccurredAt: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - count30d - maxIntensity30d @@ -84696,13 +84719,13 @@ components: minimum: -9007199254740991 maximum: 9007199254740991 note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" episodeId: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string updatedAt: @@ -84908,17 +84931,17 @@ components: - firstValue - gap type: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" params: type: object propertyNames: type: string additionalProperties: - anyOf: - - type: string - - type: number + type: + - string + - number description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers in the unit the entry names. required: @@ -84992,9 +85015,9 @@ components: maximum: 9007199254740991 description: Entries folded into this one (lab results of one day). href: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - date - kind @@ -85164,10 +85187,10 @@ components: type: string description: Decrypted server-side for the caller's own session. sub: - anyOf: - - type: string - - type: "null" description: A second line, already formatted (a dose, a destination). + type: + - string + - "null" since: anyOf: - type: string @@ -85196,10 +85219,10 @@ components: - type: "null" description: Length of a closed period in days; null while open. href: - anyOf: - - type: string - - type: "null" description: In-app path to the record this came from. + type: + - string + - "null" required: - kind - section @@ -85225,7 +85248,7 @@ components: at: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ source: type: string description: The `MeasurementSource` of the canonical row. @@ -85264,7 +85287,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: The instant, or null for something that has a date only. kind: @@ -85322,16 +85345,16 @@ components: description: "The record's own name, or a code the client words: a mood, a screener's instrument, a workout's sport, `cycle` for a cycle day." meta: - anyOf: - - type: string - - type: "null" description: "The second line: text (a dose, a lab value, a duration) or a code or number the client words (a category, a kind, a severity, a screener's score and band, an intensity, a dose number)." + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" description: The person's own note, decrypted for the caller's session. + type: + - string + - "null" docs: type: array items: @@ -85347,9 +85370,9 @@ components: additionalProperties: false description: Documents filed against the entry. href: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - at - kind @@ -85372,18 +85395,18 @@ components: - firstValue - gap type: - anyOf: - - type: string - - type: "null" description: The `MeasurementType` it is about. + type: + - string + - "null" params: type: object propertyNames: type: string additionalProperties: - anyOf: - - type: string - - type: number + type: + - string + - number description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers in the unit the entry names. required: @@ -85456,17 +85479,17 @@ components: - mi description: Resolved from the account's unit preference. total: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" driving: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" train: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" required: - unit - total @@ -85493,9 +85516,9 @@ components: isTravelDay: type: boolean countryCode: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - isTravelDay - countryCode @@ -85504,7 +85527,7 @@ components: fetchedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - source - distance @@ -85689,22 +85712,22 @@ components: type: string description: The item's name. A trip is labelled `travel` and a cycle `cycle`; the client words both. sub: - anyOf: - - type: string - - type: "null" description: "The second line, by kind: a code for a life event (its category), a visit or procedure (its encounter kind) and a document (its kind); the analyte count for a lab day; the dose as text for a medication or a dose change. The client words every code." + type: + - string + - "null" href: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" group: - anyOf: - - type: string - - type: "null" description: "Items that belong to one thing share a group and are drawn on one row: a medication's own span, its courses, its dose changes and its pauses all carry the medication's id. Null for an item that stands alone." + type: + - string + - "null" required: - id - kind @@ -85743,9 +85766,9 @@ components: description: A local calendar date, `YYYY-MM-DD`. - type: "null" href: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - lane - id @@ -85760,9 +85783,9 @@ components: type: string description: A `MeasurementType` or `MOOD`. unit: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" points: type: array items: @@ -85863,9 +85886,9 @@ components: propertyNames: type: string additionalProperties: - anyOf: - - type: string - - type: number + type: + - string + - number description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers in the unit the entry names. required: @@ -85959,22 +85982,22 @@ components: - MONTH - YEAR title: - anyOf: - - type: string - - type: "null" description: Decrypted; null only when the stored title cannot be read. + type: + - string + - "null" note: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - category @@ -86124,9 +86147,9 @@ components: - $ref: "#/components/schemas/SearchMarkedText" - type: "null" day: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" path: type: string description: In-app path that opens the record, never an API path. @@ -86164,6 +86187,9 @@ components: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 + items: false + minItems: 2 + maxItems: 2 description: Half-open [start, end) character ranges of `text` a query word matched. required: - text @@ -86269,7 +86295,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" reviewDue: type: boolean @@ -86302,9 +86328,9 @@ components: type: object properties: value: - anyOf: - - type: number - - type: "null" + type: + - number + - "null" status: type: string enum: @@ -86325,9 +86351,9 @@ components: maximum: 9007199254740991 - type: "null" meetsWho150: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" measuredStepsAvailable: type: boolean required: @@ -86413,7 +86439,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ totalScore: type: integer minimum: -9007199254740991 @@ -86536,7 +86562,7 @@ components: validFrom: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ validUntil: type: "null" provenance: @@ -86547,7 +86573,7 @@ components: createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - kind @@ -86567,7 +86593,7 @@ components: takenAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ items: type: array prefixItems: @@ -86580,6 +86606,9 @@ components: - type: integer minimum: -9007199254740991 maximum: 9007199254740991 + items: false + minItems: 3 + maxItems: 3 totalScore: type: integer minimum: 0 @@ -86620,9 +86649,9 @@ components: description: The screening cut-off for the entry's language and the profile's sex (German S3 guideline, Korean KNHANES, else Bradley 2007). `null` when the profile has no sex; show the sum only. aboveCutoff: - anyOf: - - type: boolean - - type: "null" + type: + - boolean + - "null" validatedLocale: type: boolean description: Whether the item wording shown was a validated translation for `locale`. @@ -86660,7 +86689,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - aiSections - updatedAt @@ -86695,7 +86724,7 @@ components: lastConfirmedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - section - lastConfirmedAt @@ -86769,7 +86798,7 @@ components: removedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ required: - id - removedAt @@ -86837,21 +86866,21 @@ components: homePlaceSet: type: boolean homeCountry: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" daysStored: type: integer minimum: -9007199254740991 maximum: 9007199254740991 oldestDay: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" latestDay: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" health: anyOf: - $ref: "#/components/schemas/ConnectionHealth" @@ -86916,9 +86945,9 @@ components: maximum: 9007199254740991 - type: "null" homeCountry: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" required: - homePlaceId - homeCountry @@ -86948,7 +86977,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" latencyMs: type: integer @@ -87363,7 +87392,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" note: description: One free-text note for the whole context. AES-256-GCM at rest; the ciphertext never rides the wire. @@ -87391,32 +87420,32 @@ components: description: Legacy reminder-window upper bound (HH:mm). Used to derive the late-classification grace span when `reminderGraceMinutes` is null. label: - anyOf: - - type: string - - type: "null" + type: + - string + - "null" dose: - anyOf: - - type: string - - type: "null" description: Per-schedule dose override. NULL means the schedule inherits `Medication.dose`. + type: + - string + - "null" unitsPerDose: - anyOf: - - type: number - - type: "null" description: v1.37.10 (#219) — per-slot inventory-units override (may be a split-pill fraction). NULL means the schedule inherits the medication-level `unitsPerDose`. Kept raw so an edit surface can distinguish an explicit value from inheritance; consumers wanting the effective figure read `resolvedUnitsPerDose`. + type: + - number + - "null" resolvedUnitsPerDose: type: number description: v1.37.19 — the EFFECTIVE units one dose of this slot consumes, resolved server-side (`schedule.unitsPerDose ?? medication.unitsPerDose`, matching the intake consumption resolver). Always present; clients never re-derive the inheritance rule. daysOfWeek: - anyOf: - - type: string - - type: "null" description: Legacy persisted recurrence encoding (`null` | `1,3,5` | `i2;1,3,5`). v1.5 readers consult `rrule` first; the field is kept for pre-v1.5 rows. v1.6.0 drops the column. + type: + - string + - "null" timesOfDay: type: array items: @@ -87431,12 +87460,12 @@ components: - type: "null" description: Reminder grace window in minutes. NULL falls back to the legacy `windowEnd - windowStart` span. rrule: - anyOf: - - type: string - - type: "null" description: RFC 5545 RRULE string (subset). Used for calendar-anchored cadences. **Mutually exclusive with `rollingIntervalDays`** — exactly one of the two is non-null on any v1.5+ schedule (or both are null on legacy rows that haven't been touched since the migration). + type: + - string + - "null" rollingIntervalDays: anyOf: - type: integer @@ -87537,7 +87566,7 @@ components: updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z))$ required: - lastStopId - completedStopIds @@ -87593,29 +87622,29 @@ components: description: v1.7.0 iOS 26 AlarmKit critical-reminder opt-in. Default false. Critical alarms bypass the device mute switch / Focus; the server stores the preference only. atcCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional WHO ATC classification code (active-substance class, e.g. `A10BX10`). User/clinician-asserted; never machine-guessed. Emitted on the FHIR `medicationCodeableConcept` under `http://www.whocc.no/atc`. NULL = no code captured. + type: + - string + - "null" rxNormCode: - anyOf: - - type: string - - type: "null" description: v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured. + type: + - string + - "null" pausedAt: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" snoozedUntil: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" remindersActive: description: "Present on the READ paths only (list and single GET). v1.42.2 — whether the server sends dose reminders @@ -87636,7 +87665,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" nextDueOverdue: description: "Present on the READ paths only, alongside `nextDueAt`. v1.16.4 — true when `nextDueAt` is an OPEN overdue @@ -87648,7 +87677,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval countdown's first window. NULL means active from creation. @@ -87656,7 +87685,7 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true. oneShot: @@ -87689,18 +87718,18 @@ components: anyOf: - type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ - type: "null" description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded automatically. NULL while the option is off. Read-only — set by the server on the switch. createdAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ updatedAt: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ schedules: type: array items: @@ -87931,7 +87960,7 @@ components: asOf: type: string format: date-time - pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ description: "`measuredAt` of the latest score." series: type: array @@ -87961,10 +87990,10 @@ components: type: string description: Contributor id, e.g. rhr / hrv / sleep. value: - anyOf: - - type: number - - type: "null" description: 0–100 sub-score, null when the input was missing. + type: + - number + - "null" weight: type: number description: Effective weight after redistributing missing inputs. diff --git a/docs/ops/migration-0409-plaintext-location.md b/docs/ops/migration-0409-plaintext-location.md new file mode 100644 index 000000000..93f5231e6 --- /dev/null +++ b/docs/ops/migration-0409-plaintext-location.md @@ -0,0 +1,98 @@ +# Upgrade stopped at migration 0409 + +v1.43 removes the readable location columns that v1.42 replaced with sealed +copies (`users.home_lat/home_lon/home_label`, +`environment_contexts.lat/lon/location_label`, +`environment_travel_locations.lat/lon/label`). Before it drops anything, +migration `0409_drop_plaintext_location` checks that the v1.42 encryption +backfill has sealed every one of those values. If any row still holds a +readable location, the migration stops and the app container does not start. + +## What you see + +The container exits during the migration step with: + +```text +A migration failed to apply. New migrations cannot be applied before the error is recovered from. ... +Migration name: 0409_drop_plaintext_location +Database error code: P0001 +Database error: +ERROR: HealthLog migration 0409 stopped: 3 home location(s), 0 location period(s) and 12 daily location(s) are still stored in plaintext. ... +``` + +On every later start, Prisma refuses with `P3009` ("migrate found failed +migrations in the target database") until the failed attempt is cleared. + +## What it means + +Nothing was lost and nothing was dropped. The check runs before any other +statement in the migration, so the location columns and their values are all +still there. The migrations before 0409 in the same image did apply; they only +add tables, columns and enum values, so the previous release runs on top of +them unchanged. + +The cause is a v1.42 instance that was not running long enough for its +encryption backfill to reach every account, for example a host that went from +v1.41 straight to v1.43, or one that was stopped soon after updating to v1.42. + +## How to recover + +The order matters: clear the failed attempt first, because the previous +release runs `prisma migrate deploy` on start as well and refuses to boot +while a failed migration is on record. + +```bash +# 1. Mark the failed 0409 attempt as rolled back. The entrypoint is replaced +# so the container does not try to migrate first. +docker compose run --rm --entrypoint sh app -c \ + 'NODE_PATH=/opt/prisma-cli/node_modules node /opt/prisma-cli/node_modules/prisma/build/index.js migrate resolve --rolled-back 0409_drop_plaintext_location' + +# 2. Start the previous release. Set this in the compose `.env` (or your +# platform's environment) and recreate the container. +# HEALTHLOG_IMAGE_REF=:v1.42.4 +docker compose up -d app +``` + +The previous release starts the encryption backfill about three minutes after +boot and again once a day. It works through the accounts one at a time. Watch +it with the three counts the migration checks; all three must reach 0: + +```bash +docker compose exec -T db psql -U healthlog healthlog -c " + SELECT + (SELECT count(*) FROM users WHERE home_lat IS NOT NULL) AS home, + (SELECT count(*) FROM environment_travel_locations WHERE lat IS NOT NULL) AS periods, + (SELECT count(*) FROM environment_contexts WHERE lat IS NOT NULL) AS days;" +``` + +The app log shows each pass as +`[free-text-encryption-backfill] user=... ` lines. If the counts stop going +down, look for `[free-text-encryption-backfill] user=... failed` in the log; +the error on the line after it names the cause. + +Once all three counts are 0: + +```bash +# 3. Dump the database. This is the copy you go back to if anything else +# goes wrong; keep it with your encryption key. +docker compose exec -T db pg_dump -U healthlog healthlog \ + | gzip > healthlog-before-0409-$(date +%F).sql.gz + +# 4. Upgrade again: remove the pin (or set it to the new version) and +# recreate the container. Migration 0409 now runs through. +docker compose pull app +docker compose up -d app +curl -s http://localhost:3000/api/version +``` + +## What not to do + +- Do not clear the readable columns by hand to get past the check. The + check exists because those values have no sealed copy yet; setting them to + `NULL` deletes the location for good. +- Do not run `migrate resolve --applied 0409_drop_plaintext_location`. That + records the migration as done while the columns are still there, and the + v1.43 schema no longer matches the database. + +Background: [`deploy.md`](deploy.md) for the general update and rollback +order, [`backup-restore.md`](backup-restore.md) for restoring a dump. diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index aa6ed6ba4..58e7f5856 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -403,5 +403,9 @@ The line is a notice and does not pause the start: the migrations run right after it. So the dump belongs before step 2, not after you see the line; by then the database is already being migrated. +If an update to v1.43 stops at migration `0409_drop_plaintext_location`, +nothing was changed; the recovery is in +[`docs/ops/migration-0409-plaintext-location.md`](../ops/migration-0409-plaintext-location.md). + For the ordering on a host where you want the migration result before the new container takes traffic, see [`docs/ops/deploy.md`](../ops/deploy.md). diff --git a/package.json b/package.json index b27b8a46b..7bd1ae075 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "undici": "^8.11.2", "web-push": "^3.6.7", "yaml": "^2.9.1", - "zod": "^4.4.3", + "zod": "^4.6.7", "zod-openapi": "^6.0.2", "zxcvbn-typescript": "^5.0.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 979bf89d3..1714c6142 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,10 +58,10 @@ importers: version: 3.2.2(react@19.3.0) '@hookform/resolvers': specifier: ^5.9.1 - version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3) + version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.6.7) '@modelcontextprotocol/sdk': specifier: ^1.32.1 - version: 1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3) + version: 1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.6.7) '@napi-rs/canvas': specifier: 1.0.10 version: 1.0.10 @@ -192,11 +192,11 @@ importers: specifier: ^2.9.1 version: 2.9.1 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: ^4.6.7 + version: 4.6.7 zod-openapi: specifier: ^6.0.2 - version: 6.0.2(zod@4.4.3) + version: 6.0.2(zod@4.6.7) zxcvbn-typescript: specifier: ^5.0.1 version: 5.0.1 @@ -6918,11 +6918,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - - zod@4.6.5: - resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} zxcvbn-typescript@5.0.1: resolution: {integrity: sha512-If2a6jdm278dfWmn6NL7oC1lNYRHOthBw0C78LsKvtAHHFNsMudvFhDn2fF/b74txWtSosJJiuJ7jBPn/xP6HQ==} @@ -7507,7 +7504,7 @@ snapshots: dependencies: hono: 4.13.7 - '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)': + '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.6.7)': dependencies: '@standard-schema/utils': 0.3.0 react-hook-form: 7.89.0(@types/react@19.3.0)(react@19.3.0) @@ -7517,7 +7514,7 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) effect: 3.20.0 valibot: 1.4.2(typescript@6.0.3) - zod: 4.4.3 + zod: 4.6.7 '@humanfs/core@0.19.2': dependencies: @@ -7738,7 +7735,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.6.7)': dependencies: '@hono/node-server': 1.19.17(hono@4.13.7) ajv: 8.20.0 @@ -7755,8 +7752,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) + zod: 4.6.7 + zod-to-json-schema: 3.25.2(zod@4.6.7) optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: @@ -10899,8 +10896,8 @@ snapshots: '@babel/parser': 7.29.9 eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0) hermes-parser: 0.25.1 - zod: 4.6.5 - zod-validation-error: 4.0.2(zod@4.6.5) + zod: 4.6.7 + zod-validation-error: 4.0.2(zod@4.6.7) transitivePeerDependencies: - supports-color @@ -11760,7 +11757,7 @@ snapshots: tinyglobby: 0.2.17 unbash: 4.0.12 yaml: 2.9.1 - zod: 4.6.5 + zod: 4.6.7 language-subtag-registry@0.3.23: {} @@ -13895,26 +13892,24 @@ snapshots: zlibjs@0.3.1: {} - zod-openapi@6.0.2(zod@4.4.3): + zod-openapi@6.0.2(zod@4.6.7): dependencies: - zod: 4.4.3 + zod: 4.6.7 zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.2(zod@4.4.3): + zod-to-json-schema@3.25.2(zod@4.6.7): dependencies: - zod: 4.4.3 + zod: 4.6.7 - zod-validation-error@4.0.2(zod@4.6.5): + zod-validation-error@4.0.2(zod@4.6.7): dependencies: - zod: 4.6.5 + zod: 4.6.7 zod@3.25.76: {} - zod@4.4.3: {} - - zod@4.6.5: {} + zod@4.6.7: {} zxcvbn-typescript@5.0.1: {} diff --git a/prisma/migrations/0409_drop_plaintext_location/migration.sql b/prisma/migrations/0409_drop_plaintext_location/migration.sql index af7f5f4fd..cbce4e700 100644 --- a/prisma/migrations/0409_drop_plaintext_location/migration.sql +++ b/prisma/migrations/0409_drop_plaintext_location/migration.sql @@ -10,7 +10,8 @@ -- It refuses to run while any row still holds a plaintext location: such a -- row has not been sealed yet, and dropping the column would lose it. Start -- the previous release, let the backfill finish (the three counts below must --- all be 0), then upgrade again. +-- all be 0), then upgrade again. The operator steps, including clearing the +-- failed attempt, are in docs/ops/migration-0409-plaintext-location.md. DO $$ DECLARE home_rows BIGINT; @@ -21,7 +22,7 @@ BEGIN SELECT count(*) INTO travel_rows FROM "environment_travel_locations" WHERE "lat" IS NOT NULL; SELECT count(*) INTO day_rows FROM "environment_contexts" WHERE "lat" IS NOT NULL; IF home_rows > 0 OR travel_rows > 0 OR day_rows > 0 THEN - RAISE EXCEPTION 'HealthLog migration 0409 stopped: % home location(s), % location period(s) and % daily location(s) are still stored in plaintext. Run the previous release until its free-text encryption backfill reports zero remaining rows, then upgrade again. Nothing was changed.', home_rows, travel_rows, day_rows; + RAISE EXCEPTION 'HealthLog migration 0409 stopped: % home location(s), % location period(s) and % daily location(s) are still stored in plaintext. Run the previous release until its free-text encryption backfill reports zero remaining rows, then upgrade again. Nothing was changed. Recovery steps: docs/ops/migration-0409-plaintext-location.md', home_rows, travel_rows, day_rows; END IF; END $$; diff --git a/src/__tests__/datetime-helper-guard.test.ts b/src/__tests__/datetime-helper-guard.test.ts new file mode 100644 index 000000000..42012eb8c --- /dev/null +++ b/src/__tests__/datetime-helper-guard.test.ts @@ -0,0 +1,93 @@ +/** + * Every ISO instant under `src/` goes through `isoDateTime()`. + * + * Since v1.43 a request instant must carry seconds, and the refusal is counted + * (`validation.datetime.no_seconds`) so a client on a minute-precision encoder + * shows up in the logs. Both live in `src/lib/validations/datetime.ts`. A + * schema that calls `z.iso.datetime(` or `z.string().datetime(` directly gets + * zod's default message and is invisible to the counter, so a direct call + * anywhere else is refused here. + * + * The matcher tolerates whitespace and line breaks between `z`, `.iso` / + * `.string()` and `.datetime(`, because prettier splits long chains, and it + * also refuses a bare `.datetime(` chained off anything, so an alias such as + * `const s = z.string(); s.datetime()` cannot route around it. Comments are + * stripped first so prose about the old form does not trip it. + */ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "./helpers/source-files"; + +const SRC = join(process.cwd(), "src"); +const HELPER = "lib/validations/datetime.ts"; + +function sourceFiles(): string[] { + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) + .filter((p) => !p.includes("__tests__")) + .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")); +} + +function stripComments(text: string): string { + return text + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/(^|[^:"'`])\/\/.*$/gm, "$1"); +} + +const DIRECT_DATETIME = /\.\s*datetime\s*\(/g; +const HELPER_CALL = /\bisoDateTime\s*\(/g; + +function directDatetimeCalls(text: string): number { + return (stripComments(text).match(DIRECT_DATETIME) ?? []).length; +} + +describe("datetime helper guard", () => { + const files = sourceFiles(); + + it("finds no direct zod datetime call outside the helper", () => { + const offenders = files + .filter((rel) => rel !== HELPER) + .flatMap((rel) => { + const n = directDatetimeCalls(readFileSync(join(SRC, rel), "utf8")); + return n > 0 ? [`${rel} (${n})`] : []; + }); + expect( + offenders, + "use isoDateTime() from @/lib/validations/datetime instead", + ).toEqual([]); + }); + + it("the helper itself is the one place that calls zod", () => { + expect(directDatetimeCalls(readFileSync(join(SRC, HELPER), "utf8"))).toBe( + 1, + ); + }); + + it("actually sees the call sites it guards", () => { + // An empty sweep agrees with an empty offender list. The helper has + // hundreds of callers; a count far below that means the walk or the + // matcher went blind, not that the tree is clean. + const calls = files + .filter((rel) => rel !== HELPER) + .reduce( + (sum, rel) => + sum + + (readFileSync(join(SRC, rel), "utf8").match(HELPER_CALL) ?? []) + .length, + 0, + ); + expect(calls).toBeGreaterThan(350); + }); + + it("matches the forms prettier produces", () => { + expect(directDatetimeCalls("z.iso.datetime({ offset: true })")).toBe(1); + expect(directDatetimeCalls("z.iso\n .datetime({ offset: true })")).toBe(1); + expect(directDatetimeCalls("z\n .string()\n .datetime()")).toBe(1); + expect(directDatetimeCalls("s . datetime ()")).toBe(1); + expect(directDatetimeCalls("// z.iso.datetime() in prose")).toBe(0); + expect(directDatetimeCalls("/* z.string().datetime() */")).toBe(0); + expect(directDatetimeCalls("isoDateTime()")).toBe(0); + }); +}); diff --git a/src/__tests__/derived-response-consumer-guard.test.ts b/src/__tests__/derived-response-consumer-guard.test.ts index 8b58f5780..355827706 100644 --- a/src/__tests__/derived-response-consumer-guard.test.ts +++ b/src/__tests__/derived-response-consumer-guard.test.ts @@ -160,6 +160,10 @@ function zodObjectShape(path: string, schemaName: string): ZodObjectShape { let current = expression; while (ts.isCallExpression(current)) { const callee = current.expression; + // The shared instant helper (`@/lib/validations/datetime`) is a leaf. + if (ts.isIdentifier(callee) && callee.text === "isoDateTime") { + return [prefix]; + } expect( ts.isPropertyAccessExpression(callee), `${prefix || schemaName} contains an unsupported Zod call: ${current.getText(file)}`, diff --git a/src/__tests__/written-outcome-response-consumer-guard.test.ts b/src/__tests__/written-outcome-response-consumer-guard.test.ts index b74ebaea5..aa62a99d9 100644 --- a/src/__tests__/written-outcome-response-consumer-guard.test.ts +++ b/src/__tests__/written-outcome-response-consumer-guard.test.ts @@ -535,6 +535,10 @@ function zodObjectShape(path: string, schemaName: string): ZodObjectShape { let current = expression; while (ts.isCallExpression(current)) { const callee = current.expression; + // The shared instant helper (`@/lib/validations/datetime`) is a leaf. + if (ts.isIdentifier(callee) && callee.text === "isoDateTime") { + return [prefix]; + } expect( ts.isPropertyAccessExpression(callee), `${prefix || schemaName} contains an unsupported Zod call: ${current.getText(file)}`, diff --git a/src/app/api/admin/app-logs/route.ts b/src/app/api/admin/app-logs/route.ts index 2482a1600..a653f488e 100644 --- a/src/app/api/admin/app-logs/route.ts +++ b/src/app/api/admin/app-logs/route.ts @@ -10,6 +10,7 @@ import { redactSecrets } from "@/lib/logging/redact"; import type { WideEvent } from "@/lib/logging/types"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; export const dynamic = "force-dynamic"; @@ -26,8 +27,8 @@ const querySchema = z.object({ traceId: z.string().trim().min(1).max(200).optional(), level: z.enum(["debug", "info", "warn", "error"]).optional(), action: z.string().trim().min(1).max(200).optional(), - since: z.string().datetime().optional(), - until: z.string().datetime().optional(), + since: isoDateTime({ offset: false }).optional(), + until: isoDateTime({ offset: false }).optional(), limit: z.coerce.number().int().min(1).max(PROBLEMS_BUFFER_MAX).default(200), }); diff --git a/src/app/api/admin/audit-log/route.ts b/src/app/api/admin/audit-log/route.ts index 26a0e970f..8f073f194 100644 --- a/src/app/api/admin/audit-log/route.ts +++ b/src/app/api/admin/audit-log/route.ts @@ -6,6 +6,7 @@ import { redactSecrets } from "@/lib/logging/redact"; import { getAuditLogRetentionDays } from "@/lib/jobs/audit-log-cleanup"; import { NextRequest } from "next/server"; import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.4.16 phase B4: parse the union of legacy + new query params. Legacy @@ -27,8 +28,8 @@ const querySchema = z.object({ actor: z.string().trim().min(1).max(200).optional(), action: z.string().trim().min(1).max(200).optional(), target: z.string().trim().min(1).max(200).optional(), - since: z.string().datetime().optional(), - until: z.string().datetime().optional(), + since: isoDateTime({ offset: false }).optional(), + until: isoDateTime({ offset: false }).optional(), }); export const dynamic = "force-dynamic"; diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts index b64ff48ce..606759215 100644 --- a/src/app/api/import/route.ts +++ b/src/app/api/import/route.ts @@ -41,6 +41,7 @@ import { moodDateKey } from "@/lib/mood/date-key"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { deriveA1 } from "@/lib/mood/level-a"; import { zonedWallClockToUtc } from "@/lib/tz/wall-clock"; +import { isoDateTime } from "@/lib/validations/datetime"; // Derived from canonical enum so round-trip export → import covers every // type. Previous hardcoded subset silently dropped 4 of 11 types @@ -51,12 +52,12 @@ const measurementSchema = z value: z.number(), unit: z.string(), // v1.17.1 — share the canonical entry-instant bound used by the single - // POST + edit paths (v1.17 W1b). Previously a bare `z.string().datetime()` + // POST + edit paths (v1.17 W1b). Previously a bare datetime check // with no ceiling, so a crafted JSON import could forward-date a reading. // `validateEntryInstant` rejects any future instant beyond a 5-min skew // tolerance and any instant before 1900; the field transforms to a `Date`. measuredAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ), glucoseContext: glucoseContextEnum.optional(), source: z.string().optional(), @@ -96,7 +97,7 @@ const moodEntrySchema = z // v1.17.1 — same entry-instant bound as measurements; an imported mood // log cannot be forward-dated. Optional + transforms to a `Date` when set. loggedAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ).optional(), // v1.12.1 — optional source-stable id (e.g. a Daylio row id). When // present, the import upserts on `(userId, source, externalId)` so a diff --git a/src/app/api/insights/ecg/route.ts b/src/app/api/insights/ecg/route.ts index c6d17269c..95d9d306e 100644 --- a/src/app/api/insights/ecg/route.ts +++ b/src/app/api/insights/ecg/route.ts @@ -46,6 +46,7 @@ import { annotate } from "@/lib/logging/context"; import { checkRateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/db"; import { persistEcgRecording } from "@/lib/ecg/persist-recording"; +import { isoDateTime } from "@/lib/validations/datetime"; export const dynamic = "force-dynamic"; @@ -96,7 +97,7 @@ const ingestSourceEnum = z.enum(["APPLE_HEALTH"]); const ecgIngestSchema = z .object({ externalRecordingId: z.string().min(1).max(120), - recordedAt: z.iso.datetime({ offset: true }), + recordedAt: isoDateTime(), // HealthKit hands both of these over as doubles. A watch reports its // average heart rate with a fraction (64.7) often enough that requiring // an integer refused most new recordings while the old ones, synced when diff --git a/src/app/api/measurements/batch/route.ts b/src/app/api/measurements/batch/route.ts index 1a61187f6..ea7c78dd1 100644 --- a/src/app/api/measurements/batch/route.ts +++ b/src/app/api/measurements/batch/route.ts @@ -112,6 +112,7 @@ import { type MeasurementSource, type MeasurementType, } from "@/generated/prisma/client"; +import { isoDateTime } from "@/lib/validations/datetime"; // v1.4.25 W16c — historical-backfill threshold for PR push // suppression. A batch larger than this fires the detection job with @@ -156,8 +157,8 @@ const batchEntrySchema = z.object({ hkIdentifier: z.string().min(1).max(120), value: z.number().finite(), unit: z.string().min(1).max(60), - startDate: z.iso.datetime({ offset: true }), - endDate: z.iso.datetime({ offset: true }), + startDate: isoDateTime(), + endDate: isoDateTime(), sleepStage: z.number().int().min(0).max(20).optional(), // v1.10.0 — categorical events (WX-B). The integer `HKCategoryValue` // codepoint for an EVENT-class category sample (irregular-rhythm, diff --git a/src/app/api/medications/[id]/dose-history/route.ts b/src/app/api/medications/[id]/dose-history/route.ts index 2b9bb1370..acb1e411b 100644 --- a/src/app/api/medications/[id]/dose-history/route.ts +++ b/src/app/api/medications/[id]/dose-history/route.ts @@ -47,6 +47,7 @@ import { import { assertMedicationOwnership } from "@/lib/medications/route-guards"; import { dueSchedules } from "@/lib/medications/intake-tracking"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import { isoDateTime } from "@/lib/validations/datetime"; type RouteParams = { params: Promise<{ id: string }> }; @@ -60,12 +61,10 @@ const RATE_WINDOW_MS = 60_000; const querySchema = z .object({ - from: z.iso - .datetime({ offset: true }) + from: isoDateTime() .transform((s) => new Date(s)) .optional(), - to: z.iso - .datetime({ offset: true }) + to: isoDateTime() .transform((s) => new Date(s)) .optional(), }) diff --git a/src/app/api/medications/intake/bulk/route.ts b/src/app/api/medications/intake/bulk/route.ts index 085322060..9d0916ff1 100644 --- a/src/app/api/medications/intake/bulk/route.ts +++ b/src/app/api/medications/intake/bulk/route.ts @@ -101,6 +101,7 @@ import { INTAKE_NOT_TRACKED_REASON, isRecordOnly, } from "@/lib/medications/intake-tracking"; +import { isoDateTime } from "@/lib/validations/datetime"; const MAX_ENTRIES_PER_BATCH = 500; const BATCH_RATE_LIMIT_MAX = 60; @@ -109,8 +110,7 @@ const BATCH_RATE_LIMIT_WINDOW_MS = 60 * 1000; const bulkEntrySchema = z .object({ medicationId: z.string().min(1), - scheduledFor: z.iso - .datetime({ offset: true }) + scheduledFor: isoDateTime() .transform((s) => new Date(s)) .optional(), // v1.16.9 — shared plausibility bounds (no future beyond clock skew, @@ -135,8 +135,7 @@ const bulkEntrySchema = z // entry skipped (`force_slot_invalid`) per the bulk per-entry contract. // Ignored on non-taken entries (a pending echo / skip carries no take // to attribute). - forceSlotInstant: z.iso - .datetime({ offset: true }) + forceSlotInstant: isoDateTime() .transform((s) => new Date(s)) .optional(), // v1.16.4 — per-entry dose override, mirroring the single intake diff --git a/src/app/api/mood-entries/bulk/route.ts b/src/app/api/mood-entries/bulk/route.ts index d22415e71..dbea11e57 100644 --- a/src/app/api/mood-entries/bulk/route.ts +++ b/src/app/api/mood-entries/bulk/route.ts @@ -63,6 +63,7 @@ import { droppedLinkKeysForWire, NO_DROPPED_LINK_KEYS, } from "@/lib/mood/tag-links"; +import { isoDateTime } from "@/lib/validations/datetime"; const MAX_ENTRIES_PER_BATCH = 500; const BATCH_RATE_LIMIT_MAX = 60; @@ -135,7 +136,7 @@ const bulkEntrySchema = z.object({ * @per-entry-parse: moodContextSchema, in the loop below */ context: z.unknown().optional(), - moodLoggedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + moodLoggedAt: isoDateTime().transform((s) => new Date(s)), source: moodSourceEnum.optional().default("MANUAL"), /** * Optional iOS-side identifier (e.g. SwiftData row UUID) that lets diff --git a/src/lib/day/wire-schemas.ts b/src/lib/day/wire-schemas.ts index 4649265ae..150c5ad7e 100644 --- a/src/lib/day/wire-schemas.ts +++ b/src/lib/day/wire-schemas.ts @@ -32,6 +32,7 @@ import { TIMELINE_RANGE_MAX_DAYS, TIMELINE_ZOOMS, } from "./contract"; +import { isoDateTime } from "@/lib/validations/datetime"; /* ─── Shared pieces ───────────────────────────────────────────────────────── */ @@ -40,7 +41,7 @@ export const dateKeySchema = z .refine(isCalendarDateKey, { message: "Expected a YYYY-MM-DD calendar date" }) .describe("A local calendar date, `YYYY-MM-DD`."); -const instant = z.iso.datetime({ offset: true }); +const instant = isoDateTime(); const messageParams = z .record(z.string(), z.union([z.string(), z.number()])) @@ -212,7 +213,7 @@ export const dayMovementSchema = z countryCode: z.string().nullable(), }) .nullable(), - fetchedAt: z.iso.datetime({ offset: true }), + fetchedAt: isoDateTime(), }) .meta({ id: "DayMovement", diff --git a/src/lib/logging/__tests__/datetime-no-seconds-counter.test.ts b/src/lib/logging/__tests__/datetime-no-seconds-counter.test.ts new file mode 100644 index 000000000..a58146541 --- /dev/null +++ b/src/lib/logging/__tests__/datetime-no-seconds-counter.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { eventStorage } from "@/lib/logging/context"; +import { WideEventBuilder } from "@/lib/logging/event-builder"; +import { + DATETIME_NO_SECONDS_COUNTER, + isoDateTime, +} from "@/lib/validations/datetime"; + +describe("validation.datetime.no_seconds", () => { + it("counts each minute-precision refusal on the request's wide event", () => { + const builder = new WideEventBuilder("http"); + eventStorage.run(builder, () => { + const s = isoDateTime(); + s.safeParse("2026-10-15T06:15Z"); + s.safeParse("2026-10-15T07:15+02:00"); + s.safeParse("2026-10-15T07:15:00Z"); + s.safeParse("garbage"); + }); + expect(builder.toJSON().meta?.[DATETIME_NO_SECONDS_COUNTER]).toBe(2); + }); + + it("leaves a request without such a refusal untouched", () => { + const builder = new WideEventBuilder("http"); + eventStorage.run(builder, () => { + isoDateTime().safeParse("2026-10-15T07:15:00Z"); + }); + expect( + builder.toJSON().meta?.[DATETIME_NO_SECONDS_COUNTER], + ).toBeUndefined(); + }); + + it("is a no-op outside a request", () => { + expect(() => isoDateTime().safeParse("2026-10-15T06:15Z")).not.toThrow(); + }); +}); diff --git a/src/lib/logging/context.ts b/src/lib/logging/context.ts index 5ff165dff..13e550918 100644 --- a/src/lib/logging/context.ts +++ b/src/lib/logging/context.ts @@ -2,6 +2,10 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { WideEvent } from "./types"; import { WideEventBuilder } from "./event-builder"; import { registerSourcePriorityParseObserver } from "@/lib/validations/source-priority"; +import { + DATETIME_NO_SECONDS_COUNTER, + registerDatetimeNoSecondsObserver, +} from "@/lib/validations/datetime"; /** AsyncLocalStorage fuer den aktuellen Request-Kontext */ export const eventStorage = new AsyncLocalStorage(); @@ -37,3 +41,16 @@ export function annotate(fields: { registerSourcePriorityParseObserver((fields) => { annotate(fields); }); + +// v1.43 — count every request instant refused for lacking seconds, per wide +// event, so a client still on a minute-precision encoder is visible in the +// logs (`meta["validation.datetime.no_seconds"]`) before its users report +// the 422. Same bundle-safe registration as the breadcrumb above. +const noSecondsCounts = new WeakMap(); +registerDatetimeNoSecondsObserver(() => { + const evt = getEvent(); + if (!evt) return; + const next = (noSecondsCounts.get(evt) ?? 0) + 1; + noSecondsCounts.set(evt, next); + evt.addMeta(DATETIME_NO_SECONDS_COUNTER, next); +}); diff --git a/src/lib/mcp/tools.ts b/src/lib/mcp/tools.ts index fa3d851d8..9d9defb92 100644 --- a/src/lib/mcp/tools.ts +++ b/src/lib/mcp/tools.ts @@ -101,6 +101,7 @@ import { import type { McpAuthContext } from "./auth"; import { dueSchedules } from "@/lib/medications/intake-tracking"; import { liveEraStartsByMedication } from "@/lib/medications/scheduling/live-era"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Tool annotations (MCP 2025-11-25). The cloud connectors REQUIRE these on every @@ -881,8 +882,8 @@ const coachReadOutput: z.ZodRawShape = { */ const dateRangeShape = z .object({ - from: z.iso.datetime({ offset: true }), - to: z.iso.datetime({ offset: true }), + from: isoDateTime(), + to: isoDateTime(), }) .describe( "An explicit inclusive date range. Use INSTEAD OF `window` for an arbitrary span (e.g. before vs after a date). Both bounds are ISO-8601 instants.", diff --git a/src/lib/mcp/write-tools.ts b/src/lib/mcp/write-tools.ts index f6ae24c4e..160742de2 100644 --- a/src/lib/mcp/write-tools.ts +++ b/src/lib/mcp/write-tools.ts @@ -60,6 +60,7 @@ import { checkMcpBloodPressure, resolveMcpMeasurementUnit, } from "./writes"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Write-tool annotations (MCP 2025-11-25). NOT read-only and NOT destructive @@ -187,8 +188,7 @@ const WRITE_TOOL_DEFINITIONS: McpToolDefinition[] = [ .describe( "Optional unit hint. Omit to use the type's canonical unit. A recognised alias (lb/lbs/pound(s), in/inch(es), °F, mmol/L) is converted to canonical; any other unit is refused with unsupported_unit — the value is never stored in a non-canonical unit.", ), - measuredAt: z.iso - .datetime({ offset: true }) + measuredAt: isoDateTime() .optional() .describe( "Optional ISO-8601 instant the reading was taken; defaults to now.", @@ -379,8 +379,7 @@ const WRITE_TOOL_DEFINITIONS: McpToolDefinition[] = [ inputShape: { systolic: z.number().describe("Systolic (the higher number), in mmHg."), diastolic: z.number().describe("Diastolic (the lower number), in mmHg."), - measuredAt: z.iso - .datetime({ offset: true }) + measuredAt: isoDateTime() .optional() .describe( "Optional ISO-8601 instant the reading was taken; defaults to now.", diff --git a/src/lib/medications/side-effects/validators.ts b/src/lib/medications/side-effects/validators.ts index dcb398bf3..c64769f44 100644 --- a/src/lib/medications/side-effects/validators.ts +++ b/src/lib/medications/side-effects/validators.ts @@ -19,6 +19,7 @@ import { MedicationSideEffectCategory, MedicationSideEffectEntry, } from "@/generated/prisma/client"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.4.25 W21 Fix-N (simp-M1) — these arrays used to restate the @@ -54,20 +55,17 @@ const SIDE_EFFECT_NOTES_MAX = 280; export const createSideEffectSchema = z.object({ entry: z.nativeEnum(MedicationSideEffectEntry), severity: z.number().int().min(1).max(5), - occurredAt: z.iso - .datetime({ offset: true }) + occurredAt: isoDateTime() .transform((s) => new Date(s)) .optional(), notes: z.string().max(SIDE_EFFECT_NOTES_MAX).nullable().optional(), }); export const listSideEffectsSchema = z.object({ - from: z.iso - .datetime({ offset: true }) + from: isoDateTime() .transform((s) => new Date(s)) .optional(), - to: z.iso - .datetime({ offset: true }) + to: isoDateTime() .transform((s) => new Date(s)) .optional(), limit: z.coerce.number().int().min(1).max(200).optional().default(50), diff --git a/src/lib/onboarding/tour-progress.ts b/src/lib/onboarding/tour-progress.ts index c14c3a7f1..700859ed1 100644 --- a/src/lib/onboarding/tour-progress.ts +++ b/src/lib/onboarding/tour-progress.ts @@ -12,6 +12,7 @@ */ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** Terminal / running status of the module tour. */ export const TOUR_PROGRESS_STATUSES = [ @@ -33,7 +34,7 @@ export const tourProgressSchema = z.object({ lastStopId: z.string().min(1).max(64).nullable(), completedStopIds: z.array(z.string().min(1).max(64)).max(64).default([]), status: z.enum(TOUR_PROGRESS_STATUSES), - updatedAt: z.iso.datetime(), + updatedAt: isoDateTime({ offset: false }), }); export type TourProgress = z.infer; diff --git a/src/lib/openapi/routes/account-sharing.ts b/src/lib/openapi/routes/account-sharing.ts index eeea2cb37..a62587e9e 100644 --- a/src/lib/openapi/routes/account-sharing.ts +++ b/src/lib/openapi/routes/account-sharing.ts @@ -45,6 +45,7 @@ import { import { onboardingStateResource } from "./onboarding"; import { aiAccountBlock, moduleAccessMap } from "./profile"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * The four request bodies this table publishes, and the one thing to know about @@ -619,7 +620,7 @@ const managedProfileHandover = z "The one-time `hlp_` token. Returned only in this response and never again; only its hash is stored.", ), url: z.string().describe("The link to hand over: `/claim/`."), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), }) .meta({ id: "ManagedProfileHandover" }); @@ -639,8 +640,8 @@ const managedProfileHandoverStatus = z open: z .object({ id: z.string(), - createdAt: z.iso.datetime({ offset: true }), - expiresAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + expiresAt: isoDateTime(), createdByYou: z.boolean(), }) .nullable() @@ -660,7 +661,7 @@ const pendingHandoverDecision = z .object({ pending: z .object({ - claimedAt: z.iso.datetime({ offset: true }), + claimedAt: isoDateTime(), guardians: z.array( z.object({ grantId: z diff --git a/src/lib/openapi/routes/admin.ts b/src/lib/openapi/routes/admin.ts index df06d2d96..38b028cc5 100644 --- a/src/lib/openapi/routes/admin.ts +++ b/src/lib/openapi/routes/admin.ts @@ -10,6 +10,7 @@ import type { ZodOpenApiObject } from "zod-openapi"; import { inviteCreateSchema } from "@/lib/validations/invite"; import { adminReminderCheckSchema } from "@/lib/validations/notifications"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // v1.4.48 H-APNs-1 — admin diagnostic endpoint for the notification // subsystem. Mirrors the runtime types in @@ -27,7 +28,7 @@ const adminDiagnosticDevice = z apnsTokenPrefix: z.string().nullable(), apnsTokenSuffix: z.string().nullable(), apnsEnvironment: z.string().nullable(), - lastSeenAt: z.iso.datetime({ offset: true }), + lastSeenAt: isoDateTime(), }) .meta({ id: "AdminDiagnosticDevice", @@ -53,7 +54,7 @@ const adminDiagnosticPushAttempt = z channel: z.string(), result: z.string(), reason: z.string().nullable(), - at: z.iso.datetime({ offset: true }), + at: isoDateTime(), }) .meta({ id: "AdminDiagnosticPushAttempt", @@ -89,10 +90,10 @@ export const adminInvitePaths: NonNullable = { z.array( z.object({ id: z.string(), - createdAt: z.iso.datetime({ offset: true }), - expiresAt: z.iso.datetime({ offset: true }), - usedAt: z.iso.datetime({ offset: true }).nullable(), - revokedAt: z.iso.datetime({ offset: true }).nullable(), + createdAt: isoDateTime(), + expiresAt: isoDateTime(), + usedAt: isoDateTime().nullable(), + revokedAt: isoDateTime().nullable(), uses: z.number().int(), maxUses: z.number().int(), creator: z @@ -104,7 +105,7 @@ export const adminInvitePaths: NonNullable = { redemptions: z.array( z.object({ id: z.string(), - redeemedAt: z.iso.datetime({ offset: true }), + redeemedAt: isoDateTime(), user: z .object({ id: z.string(), @@ -141,8 +142,8 @@ export const adminInvitePaths: NonNullable = { schema: dataEnvelope( z.object({ id: z.string(), - createdAt: z.iso.datetime({ offset: true }), - expiresAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + expiresAt: isoDateTime(), uses: z.number().int(), maxUses: z.number().int(), token: z @@ -226,7 +227,7 @@ const adminReminderMedication = z const adminReminderCheckResult = z .object({ - timestamp: z.iso.datetime({ offset: true }), + timestamp: isoDateTime(), missedThresholdMinutes: z.number().int(), scoped: z .boolean() @@ -285,9 +286,9 @@ const adminMeasurementMaintenanceStatus = z run: z .object({ state: z.enum(["queued", "running", "completed", "failed"]), - requestedAt: z.iso.datetime(), - startedAt: z.iso.datetime().nullable(), - finishedAt: z.iso.datetime().nullable(), + requestedAt: isoDateTime({ offset: false }), + startedAt: isoDateTime({ offset: false }).nullable(), + finishedAt: isoDateTime({ offset: false }).nullable(), outcome: z .enum(["completed", "refused_purge_running", "already_running"]) .nullable() diff --git a/src/lib/openapi/routes/analytics.ts b/src/lib/openapi/routes/analytics.ts index 41dda0037..bbdda4373 100644 --- a/src/lib/openapi/routes/analytics.ts +++ b/src/lib/openapi/routes/analytics.ts @@ -29,6 +29,7 @@ import { healthScoreBasis, } from "./insights/schemas"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── The per-type summary the tile strip renders ────────────────────── @@ -97,7 +98,7 @@ const lastSeenByType = z z.string(), z .object({ - lastSeenAt: z.iso.datetime({ offset: true }), + lastSeenAt: isoDateTime(), daysAgo: z .number() .int() @@ -250,7 +251,7 @@ const pillarValue = z.object({ .describe( "The complete display value, paired or panel values included, written in the reader's units (fasting glucose in the account's glucose unit).", ), - asOf: z.iso.datetime({ offset: true }), + asOf: isoDateTime(), sources: z.array(z.string()), }), reference: z.object({ @@ -343,7 +344,7 @@ const weightGoalValue = z.object({ .number() .nullable() .describe("Positive means the distance to the personal band narrowed."), - asOf: z.iso.datetime({ offset: true }), + asOf: isoDateTime(), source: z.string(), }); @@ -428,7 +429,7 @@ const healthScoreReport = z restMode: z .object({ active: z.literal(true), - since: z.iso.datetime({ offset: true }).nullable(), + since: isoDateTime().nullable(), episodeCount: z.number().int(), }) .nullable() diff --git a/src/lib/openapi/routes/auth.ts b/src/lib/openapi/routes/auth.ts index 2556bb59d..82b07e7f2 100644 --- a/src/lib/openapi/routes/auth.ts +++ b/src/lib/openapi/routes/auth.ts @@ -55,6 +55,7 @@ import { claimSchema, } from "@/lib/validations/managed-profile-handover"; import { HANDOVER_ACCESS_LEVELS } from "@/lib/managed-profiles/handover-access"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Sub-schemas owned here (route-specific shapes) ─────────────────── @@ -109,14 +110,14 @@ const accessRefreshBundle = z .describe( "Access token (`hlk_<64hex>`); only present for native-policy callers. A browser caller gets the session cookie instead.", ), - tokenExpiresAt: z.iso.datetime({ offset: true }).optional(), + tokenExpiresAt: isoDateTime().optional(), refreshToken: z .string() .optional() .describe( "Refresh token (`hlr_<64hex>`); only present for native-policy callers.", ), - refreshTokenExpiresAt: z.iso.datetime({ offset: true }).optional(), + refreshTokenExpiresAt: isoDateTime().optional(), }) .meta({ id: "AccessRefreshBundle", @@ -195,8 +196,8 @@ const sessionListResponse = z .string() .nullable() .describe("Resolved coarse location, when available."), - lastActiveAt: z.iso.datetime({ offset: true }).nullable(), - createdAt: z.iso.datetime({ offset: true }), + lastActiveAt: isoDateTime().nullable(), + createdAt: isoDateTime(), isCurrent: z .boolean() .describe("True for the session making this request."), @@ -214,9 +215,9 @@ const trustedDeviceListResponse = z .string() .nullable() .describe("Coarse, IP-free device label (e.g. 'Firefox on macOS')."), - createdAt: z.iso.datetime({ offset: true }), - lastUsedAt: z.iso.datetime({ offset: true }), - expiresAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + lastUsedAt: isoDateTime(), + expiresAt: isoDateTime(), isCurrent: z .boolean() .describe("True for the device making this request."), @@ -280,7 +281,7 @@ const securityActivityResponse = z action: z .string() .describe("Audit action name, e.g. auth.login.password."), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), location: z.string().nullable(), ipMasked: z.string().nullable().describe("Host-masked IP."), carrier: z.string().nullable(), @@ -295,8 +296,8 @@ const webauthnCredentialInfo = z .object({ id: z.string(), name: z.string(), - createdAt: z.iso.datetime({ offset: true }), - lastUsedAt: z.iso.datetime({ offset: true }).nullable(), + createdAt: isoDateTime(), + lastUsedAt: isoDateTime().nullable(), }) .meta({ id: "WebauthnMfaCredentialInfo" }); @@ -320,9 +321,8 @@ const passkeyInfo = z .describe( "Whether the authenticator reported the credential as backed up (synced to the platform keychain) at registration time. Not re-read afterwards.", ), - createdAt: z.iso.datetime({ offset: true }).describe("Registered at."), - lastUsedAt: z.iso - .datetime({ offset: true }) + createdAt: isoDateTime().describe("Registered at."), + lastUsedAt: isoDateTime() .nullable() .describe( "When this passkey last completed a verified assertion; null when it has never been used to sign in. Stamped on every successful login, so a client can render a real 'last used' rather than assuming never.", @@ -392,7 +392,7 @@ const stepUpMintResponse = z .describe( "Opaque single-use elevation (`hle_<64hex>`). Present it in the `X-Step-Up` header on ONE second-factor-management call. Returned exactly once; store it in memory only.", ), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), expiresInSeconds: z.number().int(), method: z .enum(["password", "totp", "webauthn", "passkey"]) @@ -473,12 +473,11 @@ const apiTokenInfo = z .describe( 'The token\'s scope list. `["*"]` is cookie-equivalent (what a native login mints); anything else is narrow and reaches only the routes that name it. Admin endpoints are cookie-only and are reachable by no token at any scope.', ), - lastUsedAt: z.iso - .datetime({ offset: true }) + lastUsedAt: isoDateTime() .nullable() .describe("Stamped fire-and-forget on use; null for an unused token."), - expiresAt: z.iso.datetime({ offset: true }).nullable(), - createdAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime().nullable(), + createdAt: isoDateTime(), revoked: z .boolean() .describe( @@ -505,7 +504,7 @@ const createMeasurementTokenResponse = z "The raw `hlk_` Bearer, RETURNED EXACTLY ONCE. It is stored only as an HMAC-SHA256 hash, so there is no way to retrieve it later; a client that does not capture it here has to mint a replacement. Treat it as a secret in logs, crash reports and analytics.", ), name: z.string(), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), }) .meta({ id: "CreateMeasurementTokenResponse", @@ -527,7 +526,7 @@ const createWorkoutTokenResponse = z "The raw `hlk_` Bearer, RETURNED EXACTLY ONCE. It is stored only as an HMAC-SHA256 hash and cannot be retrieved later. Treat it as a secret.", ), name: z.string(), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), }) .meta({ id: "CreateWorkoutTokenResponse", @@ -549,7 +548,7 @@ const createDocumentTokenResponse = z "The raw `hlk_` Bearer, RETURNED EXACTLY ONCE. It is stored only as an HMAC-SHA256 hash and cannot be retrieved later. Treat it as a secret.", ), name: z.string(), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), }) .meta({ id: "CreateDocumentTokenResponse", @@ -715,7 +714,7 @@ const claimPreview = z .string() .nullable() .describe("The profile's name, as its guardians set it."), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), guardians: z .array( z.object({ @@ -1116,7 +1115,7 @@ export const authPaths: NonNullable = { z .object({ method: z.enum(["password", "totp", "webauthn", "passkey"]), - verifiedAt: z.string().datetime(), + verifiedAt: isoDateTime({ offset: false }), }) .meta({ id: "ReproofResponse" }), "ReproofEnvelope", diff --git a/src/lib/openapi/routes/awards.ts b/src/lib/openapi/routes/awards.ts index d61b80c41..96a6031da 100644 --- a/src/lib/openapi/routes/awards.ts +++ b/src/lib/openapi/routes/awards.ts @@ -22,6 +22,7 @@ import { recordRefusal, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const personalRecord = z .object({ @@ -45,7 +46,7 @@ const personalRecord = z ), value: z.number(), unit: z.string(), - achievedAt: z.iso.datetime({ offset: true }), + achievedAt: isoDateTime(), sourceMeasurementId: z .string() .nullable() @@ -54,7 +55,7 @@ const personalRecord = z ), source: measurementSourceEnum, externalId: z.string().nullable(), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), benchmark: recordBenchmark .nullable() .optional() @@ -93,7 +94,7 @@ const achievement = z points: z.number().int(), unlocked: z.boolean(), progressPercent: z.number(), - completedAt: z.iso.datetime({ offset: true }).nullable(), + completedAt: isoDateTime().nullable(), isHidden: z .boolean() .describe( @@ -144,7 +145,7 @@ const achievementIosEntry = z description: z.string(), iconName: z.string(), unlocked: z.boolean(), - unlockedAt: z.iso.datetime({ offset: true }).nullable(), + unlockedAt: isoDateTime().nullable(), progress: z.number().describe("0–1, clamped. Not the 0–100 percent."), category: z.string(), points: z.number().int(), diff --git a/src/lib/openapi/routes/coach.ts b/src/lib/openapi/routes/coach.ts index 0b2d67cdd..3f9aec5b6 100644 --- a/src/lib/openapi/routes/coach.ts +++ b/src/lib/openapi/routes/coach.ts @@ -79,6 +79,7 @@ import { aiExtractionRefusalDescription, aiExtractionRefusals, } from "./ai-extraction-refusals"; +import { isoDateTime } from "@/lib/validations/datetime"; // The assigning form, deliberately: `schema.meta({...})` as a bare statement // returns a clone and registers nothing, so the component id would never @@ -269,8 +270,7 @@ const insightsLayoutPutBody = insightsLayoutSchema }); const insightsLayoutResult = insightsLayoutSchema .extend({ - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .optional() .describe( "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque.", @@ -315,14 +315,13 @@ const coachFactItem = z.object({ .string() .nullable() .describe("v1.41 — the message the fact came out of, when known."), - lastUsedAt: z.iso - .datetime({ offset: true }) + lastUsedAt: isoDateTime() .nullable() .describe("v1.41 — when the fact last went into a Coach turn."), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso - .datetime({ offset: true }) - .describe("v1.41 — when the fact was last written or edited."), + createdAt: isoDateTime(), + updatedAt: isoDateTime().describe( + "v1.41 — when the fact was last written or edited.", + ), }); const coachFactCreateRequest = coachFactCreateSchema.meta({ @@ -355,7 +354,7 @@ const coachFactCreatedResponse = z.object({ const coachFactUpdatedResponse = z.object({ fact: coachFactWrittenItem.extend({ - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }), }); @@ -414,8 +413,7 @@ const coachPlanItem = z status: z .enum(COACH_PLAN_STATUSES) .describe("App-side lifecycle: proposed | active | met | abandoned."), - reviewDate: z.iso - .datetime({ offset: true }) + reviewDate: isoDateTime() .nullable() .describe("Optional check-in checkpoint; null when none."), sourceConversationId: z @@ -424,8 +422,8 @@ const coachPlanItem = z .describe( "Id of the conversation the proposal was extracted from; null for a plan without conversation provenance. Lets a chat surface show only the proposals born in the open thread.", ), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "CoachPlan", @@ -481,12 +479,12 @@ const coachReminderItem = z ), metric: z.string().nullable(), triggerKind: z.enum(["date", "context"]), - dueAt: z.iso.datetime({ offset: true }).nullable(), + dueAt: isoDateTime().nullable(), contextCue: z.string().nullable(), status: z.enum(COACH_REMINDER_STATUSES), source: z.enum(["sentinel", "extractor", "manual", "action"]), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "CoachReminder", @@ -549,7 +547,7 @@ const coachMessageSchema = z content: z .string() .describe("Decrypted message body — the server decrypts on read."), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), metricSource: coachProvenanceSchema .nullable() .describe("Provenance envelope (assistant turns); null for user turns."), @@ -601,10 +599,10 @@ const coachConversationSchema = z .object({ id: z.string(), title: z.string().describe("Title summarised from the first user message."), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso - .datetime({ offset: true }) - .describe("Bumped on every appended message; the rail orders by this."), + createdAt: isoDateTime(), + updatedAt: isoDateTime().describe( + "Bumped on every appended message; the rail orders by this.", + ), messageCount: z.number().int(), fenced: z .boolean() @@ -650,8 +648,8 @@ const coachConversationDetailSchema = z .object({ id: z.string(), title: z.string(), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), messageCount: z.number().int(), messages: z .array(coachMessageSchema) @@ -1386,8 +1384,7 @@ export const coachPaths: NonNullable = { "application/json": { schema: dataEnvelope( z.object({ - nudgedAt: z.iso - .datetime({ offset: true }) + nudgedAt: isoDateTime() .nullable() .describe( "Timestamp of the newest Coach assistant message; null when none exists.", @@ -1429,9 +1426,9 @@ export const coachPaths: NonNullable = { "application/json": { schema: dataEnvelope( z.object({ - seenAt: z.iso - .datetime({ offset: true }) - .describe("The server-minted open timestamp."), + seenAt: isoDateTime().describe( + "The server-minted open timestamp.", + ), }), "CoachSeenResponse", ), @@ -1462,7 +1459,7 @@ export const coachPaths: NonNullable = { coachFocus: z.string().nullable(), aiIncludedSections: z.array(healthProfileAiSectionSchema), pendingQuestions: z.array(z.string()), - updatedAt: z.iso.datetime({ offset: true }).nullable(), + updatedAt: isoDateTime().nullable(), maxChars: z.number().int(), fieldMaxChars: z.number().int(), ai: aiCapabilityState.describe( @@ -1506,7 +1503,7 @@ export const coachPaths: NonNullable = { coachFocus: z.string().nullable(), aiIncludedSections: z.array(healthProfileAiSectionSchema), pendingQuestions: z.array(z.string()), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), maxChars: z.number().int(), fieldMaxChars: z.number().int(), ai: aiCapabilityState.describe( @@ -2130,7 +2127,7 @@ export const coachFeedbackPaths: NonNullable = { schema: dataEnvelope( z.object({ id: z.string(), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }), "CoachMessageFeedbackResponse", ), diff --git a/src/lib/openapi/routes/consent.ts b/src/lib/openapi/routes/consent.ts index e042f14ce..a50c0b645 100644 --- a/src/lib/openapi/routes/consent.ts +++ b/src/lib/openapi/routes/consent.ts @@ -32,6 +32,7 @@ import { webConsentGrantBody as webConsentGrantBodyBase, } from "@/lib/validations/consent"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // `.meta()` CLONES in Zod 4 rather than annotating in place, so the returned // schema has to be captured and referenced. A bare `schema.meta({...})` @@ -88,9 +89,9 @@ const serialisedReceipt = z id: z.string(), userId: z.string(), kind: consentKindEnum, - signedAt: z.iso.datetime({ offset: true }), - revokedAt: z.iso.datetime({ offset: true }).nullable(), - createdAt: z.iso.datetime({ offset: true }), + signedAt: isoDateTime(), + revokedAt: isoDateTime().nullable(), + createdAt: isoDateTime(), }) .meta({ id: "SerialisedConsentReceipt", diff --git a/src/lib/openapi/routes/cycle.ts b/src/lib/openapi/routes/cycle.ts index 27b0d342c..c659d49b5 100644 --- a/src/lib/openapi/routes/cycle.ts +++ b/src/lib/openapi/routes/cycle.ts @@ -39,6 +39,7 @@ import { recordWriteRateLimitResponse, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Cycle tracking (v1.15.0) ───────────────────────────────────────── // The `/api/cycle/*` capture / calendar / history / settings surface + @@ -139,8 +140,8 @@ export const cycleDayLogDto = z source: z.string(), externalId: z.string().nullable(), syncVersion: z.number().int(), - updatedAt: z.iso.datetime({ offset: true }), - deletedAt: z.iso.datetime({ offset: true }).nullable(), + updatedAt: isoDateTime(), + deletedAt: isoDateTime().nullable(), }) .meta({ id: "CycleDayLogDTO", @@ -159,7 +160,7 @@ export const menstrualCycleDto = z ovulationConfirmed: z.boolean(), isPredicted: z.boolean(), syncVersion: z.number().int(), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .meta({ id: "MenstrualCycleDTO", @@ -289,7 +290,7 @@ const cycleProfileDto = z typicalCycleLength: z.number().int().nullable(), typicalPeriodLength: z.number().int().nullable(), lutealPhaseLength: z.number().int().nullable(), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .meta({ id: "CycleProfileDTO", @@ -315,7 +316,7 @@ const cycleCalendarResponse = z.object({ // Additive + back-compatible. stillLearning: z.boolean(), days: z.array(cycleCalendarDayDto), - meta: z.object({ generatedAt: z.iso.datetime({ offset: true }) }), + meta: z.object({ generatedAt: isoDateTime() }), }); const cycleHistoryResponse = z.object({ diff --git a/src/lib/openapi/routes/dashboard.ts b/src/lib/openapi/routes/dashboard.ts index 09ae45169..0a33fecbc 100644 --- a/src/lib/openapi/routes/dashboard.ts +++ b/src/lib/openapi/routes/dashboard.ts @@ -36,6 +36,7 @@ import { recordRefusal, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const dashboardWidgetConfig = z.object({ id: z.enum(DASHBOARD_WIDGET_CATALOGUE_IDS), @@ -102,8 +103,7 @@ const dashboardLayoutPutBody = dashboardLayoutSchema const dashboardLayoutResult = dashboardLayoutSchema .extend({ - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .optional() .describe( "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque.", @@ -226,8 +226,7 @@ const dashboardMetricCard = z .describe( "Up to seven points: the last seven DAY rollup buckets for a measured metric regardless of age (daily sum for cumulative metrics such as steps, daily mean otherwise), the trailing nights' asleep hours for `sleep`, the last seven daily means for `mood`, and the trailing derived series for `bmi`. May be empty.", ), - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .nullable() .describe( "Instant behind `latestValue`, falling back to the metric's newest reading. For `mood` this is the first instant of the latest daily bucket's day in the account's time zone, not a reading time (the series pins a local day, not an instant); formatted in that zone it always lands on the day the entry was logged. For `bmi` it is the WEIGHT metadata, since BMI moves exactly when weight does.", @@ -238,8 +237,7 @@ const dashboardMetricCard = z .describe( "Total readings ever logged for this metric — the gate that keeps a tile visible through a logging gap. Summed across both sides for `bloodPressure`; the mood entry count for `mood`; the WEIGHT count for `bmi`.", ), - lastSeenAt: z.iso - .datetime({ offset: true }) + lastSeenAt: isoDateTime() .nullable() .describe( "The metric's newest reading, for the 'last reading N days ago' caption when it is older than the sparkline window.", @@ -267,7 +265,7 @@ const dashboardSummaryResponse = z .describe( "Already-localised greeting including the display name (or username). One of the few strings this payload still translates server-side, in the account's own locale.", ), - date: z.iso.datetime({ offset: true }).describe("Build instant."), + date: isoDateTime().describe("Build instant."), }) .describe("Header block."), streak: z @@ -308,9 +306,7 @@ const dashboardSummaryResponse = z sleepRhythm: sleepRhythmResponse.describe( "The same server-authoritative sleep-debt + chronotype + average-per-night DTO `GET /api/sleep/rhythm` returns, off the same canonical night reconstruction. Always present, with its own calm `partial` / `learning` states — it is NOT gated by the sleep module here, so it can carry figures while the sleep metric CARD is filtered out.", ), - lastUpdated: z.iso - .datetime({ offset: true }) - .describe("Build instant of the cached body."), + lastUpdated: isoDateTime().describe("Build instant of the cached body."), }) .meta({ id: "DashboardSummaryResponse", diff --git a/src/lib/openapi/routes/dawarich.ts b/src/lib/openapi/routes/dawarich.ts index d3afa4825..4d320f65a 100644 --- a/src/lib/openapi/routes/dawarich.ts +++ b/src/lib/openapi/routes/dawarich.ts @@ -24,6 +24,7 @@ import { moduleDisabledResponse, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const dawarichConnectRequest = dawarichConnectSchema.meta({ id: "DawarichConnectRequest", @@ -207,7 +208,7 @@ export const dawarichPaths: NonNullable = { schema: dataEnvelope( z.object({ ok: z.literal(true), - lastSyncedAt: z.iso.datetime({ offset: true }).nullable(), + lastSyncedAt: isoDateTime().nullable(), latencyMs: z.number().int(), }), "DawarichProbeEnvelope", diff --git a/src/lib/openapi/routes/documents.ts b/src/lib/openapi/routes/documents.ts index 9a7167387..35fb5495d 100644 --- a/src/lib/openapi/routes/documents.ts +++ b/src/lib/openapi/routes/documents.ts @@ -47,6 +47,7 @@ import { recordRefusal, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const kindEnum = z.enum(INBOUND_DOCUMENT_KINDS); @@ -755,7 +756,7 @@ export const inboundDocumentPaths: NonNullable = { linkedProcedures: z.array( z.object({ encounterId: z.string(), - occurredAt: z.iso.datetime({ offset: true }), + occurredAt: isoDateTime(), reason: z.string().nullable(), bodySite: z.string().nullable(), laterality: z diff --git a/src/lib/openapi/routes/environment.ts b/src/lib/openapi/routes/environment.ts index ca4ffcc6b..73074980b 100644 --- a/src/lib/openapi/routes/environment.ts +++ b/src/lib/openapi/routes/environment.ts @@ -32,6 +32,7 @@ import { moduleDisabledResponse, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const homeLocationRequest = homeLocationSchema.meta({ id: "EnvironmentHomeRequest", @@ -68,8 +69,7 @@ const environmentHome = z .describe( "The home's IANA zone. Falls back to the account's display timezone when the home was stored without one.", ), - since: z.iso - .datetime({ offset: true }) + since: isoDateTime() .nullable() .describe( "Effective-from instant. Every set/update re-stamps it: a home resolves days from the moment it was set, and days before it stay un-attributed unless a travel override covers them.", @@ -112,13 +112,13 @@ const environmentOverview = z .string() .nullable() .describe("Day key of the newest stored observation."), - latestFetchedAt: z.iso.datetime({ offset: true }).nullable(), + latestFetchedAt: isoDateTime().nullable(), }), lastFetchFailure: z .object({ - lastFailedAt: z.iso - .datetime({ offset: true }) - .describe("When the newest exhausted-retry failure gave up."), + lastFailedAt: isoDateTime().describe( + "When the newest exhausted-retry failure gave up.", + ), failures: z .number() .int() diff --git a/src/lib/openapi/routes/export.ts b/src/lib/openapi/routes/export.ts index a19a5d09c..6dcd5c9c4 100644 --- a/src/lib/openapi/routes/export.ts +++ b/src/lib/openapi/routes/export.ts @@ -25,6 +25,7 @@ import { SHARING_NOT_PERMITTED_DESCRIPTION, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const exportQuery = z .object({ @@ -43,15 +44,13 @@ const exportQuery = z }) .meta({ id: "ExportQuery" }); -const forgivingSince = z.iso - .datetime({ offset: true }) +const forgivingSince = isoDateTime() .optional() .describe( "Inclusive lower bound. Parsed with the plain Date constructor, so an ISO date (`2026-07-14`) works as well as a full instant — and an UNPARSEABLE value is silently DROPPED from the filter rather than refused, which widens the export instead of erroring. Validate on your side.", ); -const forgivingUntil = z.iso - .datetime({ offset: true }) +const forgivingUntil = isoDateTime() .optional() .describe("Inclusive upper bound, with the same forgiving parse as `since`."); diff --git a/src/lib/openapi/routes/health.ts b/src/lib/openapi/routes/health.ts index 1f9ffa54b..2540e8fe6 100644 --- a/src/lib/openapi/routes/health.ts +++ b/src/lib/openapi/routes/health.ts @@ -18,6 +18,7 @@ */ import { z } from "zod/v4"; import type { ZodOpenApiObject } from "zod-openapi"; +import { isoDateTime } from "@/lib/validations/datetime"; const healthStatusEnum = z.enum(["ok", "degraded"]).meta({ id: "HealthStatus", @@ -43,9 +44,9 @@ const healthAdminResponse = z .object({ status: healthStatusEnum, reason: healthReason, - timestamp: z.iso - .datetime({ offset: true }) - .describe("When the probe ran. UTC (`Z`) — no user context is loaded."), + timestamp: isoDateTime().describe( + "When the probe ran. UTC (`Z`) — no user context is loaded.", + ), database: z .enum(["connected", "disconnected"]) .describe("Result of a `SELECT 1` round-trip."), @@ -54,8 +55,7 @@ const healthAdminResponse = z .describe( "The in-process pg-boss worker loop. Reported for every process, including one that does not run the worker role — a web-only process reads `stopped` here and can still be `ok`.", ), - workerLastHeartbeat: z.iso - .datetime({ offset: true }) + workerLastHeartbeat: isoDateTime() .optional() .describe( "Last worker heartbeat. Absent when the worker has never beaten in this process.", diff --git a/src/lib/openapi/routes/import.ts b/src/lib/openapi/routes/import.ts index 5283047c0..2611958c4 100644 --- a/src/lib/openapi/routes/import.ts +++ b/src/lib/openapi/routes/import.ts @@ -22,6 +22,7 @@ import { measurementTypeEnum, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const csvImportRowResult = z .object({ @@ -71,11 +72,9 @@ const jsonImportMeasurement = z "Checked against the type's server-side plausibility range; an out-of-range value fails the whole request with 422.", ), unit: z.string(), - measuredAt: z.iso - .datetime({ offset: true }) - .describe( - "Bounded: no future instants beyond a 5-min clock-skew tolerance, nothing before 1900.", - ), + measuredAt: isoDateTime().describe( + "Bounded: no future instants beyond a 5-min clock-skew tolerance, nothing before 1900.", + ), glucoseContext: glucoseContextEnum .optional() .describe( @@ -121,8 +120,7 @@ const jsonImportMoodEntry = z .string() .optional() .describe("JSON-encoded array of tag strings, stored verbatim."), - loggedAt: z.iso - .datetime({ offset: true }) + loggedAt: isoDateTime() .optional() .describe( "Bounded like `measuredAt`. Absent → the server derives a stable per-row instant within the `date` day, so a re-import of the same file dedups instead of duplicating.", @@ -186,8 +184,8 @@ const healthConnectImportJob = z .describe( "`queued`, `unpacking`, `parsing`, `upserting`, `done` or `failed`. A plain string, like the Apple Health status.", ), - startedAt: z.iso.datetime({ offset: true }), - completedAt: z.iso.datetime({ offset: true }).nullable(), + startedAt: isoDateTime(), + completedAt: isoDateTime().nullable(), uploadBytes: z.number().int(), progress: z .record(z.string(), z.unknown()) @@ -493,11 +491,10 @@ export const importPaths: NonNullable = { .describe( "`queued`, `unpacking`, `parsing`, `upserting`, `done` or `failed`. A plain string on the wire, not a closed enum — the column is untyped and a future phase name would arrive here without a schema change.", ), - startedAt: z.iso.datetime({ offset: true }), - completedAt: z.iso.datetime({ offset: true }).nullable(), + startedAt: isoDateTime(), + completedAt: isoDateTime().nullable(), uploadBytes: z.number().int(), - exportedAt: z.iso - .datetime({ offset: true }) + exportedAt: isoDateTime() .nullable() .describe( "The instant the Health app stamped on the export itself, once the worker has read it. Null before then.", diff --git a/src/lib/openapi/routes/insights/paths.ts b/src/lib/openapi/routes/insights/paths.ts index 534a96116..769d78891 100644 --- a/src/lib/openapi/routes/insights/paths.ts +++ b/src/lib/openapi/routes/insights/paths.ts @@ -70,6 +70,7 @@ import { scoreHistoryResponse, } from "./schemas"; import { recommendationFeedbackRequestSchema } from "@/lib/validations/recommendation-feedback"; +import { isoDateTime } from "@/lib/validations/datetime"; const insightsFeedbackRequest = recommendationFeedbackRequestSchema.meta({ id: "InsightsFeedbackRequest", @@ -355,7 +356,7 @@ export const insightsPaths: NonNullable = { schema: dataEnvelope( z.object({ id: z.string(), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }), "InsightsFeedbackCreatedEnvelope", ), diff --git a/src/lib/openapi/routes/insights/schemas.ts b/src/lib/openapi/routes/insights/schemas.ts index 325af4af1..a07dc42c8 100644 --- a/src/lib/openapi/routes/insights/schemas.ts +++ b/src/lib/openapi/routes/insights/schemas.ts @@ -26,6 +26,7 @@ import { SCORE_HISTORY_IDS, SCORE_HISTORY_MAX_DAYS, } from "@/lib/insights/score-history-ids"; +import { isoDateTime } from "@/lib/validations/datetime"; /** The retrospective periods the narrative route accepts, read off the engine. */ const NARRATIVE_PERIOD_VALUES = Object.keys(PERIOD_DAYS); @@ -104,8 +105,7 @@ export const metricStatusResponse = z cached: z .boolean() .describe("True when `text` is served from cache (incl. last-good)."), - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .nullable() .describe("When the served assessment was generated; null when none."), preparing: z @@ -226,9 +226,7 @@ export const wellnessScoreValue = z .describe( "Days that actually carried a score inside the window. Compare against `provenance.windowDays` — a shorter record answers a wide request with fewer days, and this is where that shows.", ), - asOf: z.iso - .datetime({ offset: true }) - .describe("`measuredAt` of the latest score."), + asOf: isoDateTime().describe("`measuredAt` of the latest score."), series: z .array(z.number()) .describe( @@ -327,9 +325,9 @@ export const derivedProvenance = z .number() .int() .describe("Trailing window the value summarises, in days."), - computedAt: z.iso - .datetime({ offset: true }) - .describe("Compute time (for cache-staleness + the 'as of' chip)."), + computedAt: isoDateTime().describe( + "Compute time (for cache-staleness + the 'as of' chip).", + ), }) .meta({ id: "DerivedProvenance" }); @@ -392,9 +390,9 @@ export const derivedAssessment = z .describe( "'deterministic' for the always-on template text, or 'ai' when warmer provider prose has been cached.", ), - updatedAt: z.iso - .datetime({ offset: true }) - .describe("When the text was produced / last warmed."), + updatedAt: isoDateTime().describe( + "When the text was produced / last warmed.", + ), }) .meta({ id: "DerivedAssessment" }); @@ -782,8 +780,7 @@ export const insightStatusResponse = z cached: z .boolean() .describe("True when `text` is served from cache (incl. last-good)."), - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .nullable() .describe("When the served assessment was generated; null when none."), preparing: z @@ -843,8 +840,7 @@ export const biomarkerAssessmentResponse = z cached: z .boolean() .describe("True when `text` is served from cache (incl. last-good)."), - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .nullable() .describe("When the served assessment was generated; null when none."), preparing: z @@ -912,8 +908,7 @@ export const medicationComplianceStatusResponse = z .describe( "True when the envelope is served from cache (incl. last-good).", ), - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .nullable() .describe("When the served assessment was generated; null when none."), preparing: z @@ -1556,7 +1551,7 @@ const insightCard = z .describe( "Suggested follow-ups; empty on the current rule-engine cards.", ), - generatedAt: z.iso.datetime({ offset: true }), + generatedAt: isoDateTime(), provider: z .string() .describe( @@ -1586,9 +1581,9 @@ const ecgClassification = z const ecgRecordingListItem = z .object({ id: z.string().describe("Recording id (cuid)."), - recordedAt: z.iso - .datetime({ offset: true }) - .describe("On-device recording time (the display timestamp)."), + recordedAt: isoDateTime().describe( + "On-device recording time (the display timestamp).", + ), durationSeconds: z .number() .nullable() @@ -1660,9 +1655,9 @@ export const ecgDetailQuery = z export const ecgDetailResponse = z .object({ - recordedAt: z.iso - .datetime({ offset: true }) - .describe("On-device recording time (the display timestamp)."), + recordedAt: isoDateTime().describe( + "On-device recording time (the display timestamp).", + ), durationSeconds: z .number() .nullable() @@ -1717,11 +1712,9 @@ export const ecgIngestRequest = z .describe( "The recording's stable id AS THE CLIENT KNOWS IT — for an Apple Health client, the `HKSample.uuid`. It is the source's own identity, not a content hash: re-posting the same id overwrites that recording in place. It does not have to agree with the id the `export.zip` importer derives for the same strip; the server reconciles the two doors itself (see `status: duplicate`).", ), - recordedAt: z.iso - .datetime({ offset: true }) - .describe( - "When the strip was recorded on-device, with offset. Part of the recording's identity — see `status: duplicate`.", - ), + recordedAt: isoDateTime().describe( + "When the strip was recorded on-device, with offset. Part of the recording's identity — see `status: duplicate`.", + ), samplingFrequency: z .number() .min(1) @@ -1777,9 +1770,9 @@ export const ecgIngestResponse = z .describe( "What the write did. `inserted` — a new recording landed (HTTP 201). `updated` — this `externalRecordingId` was already stored and the row was overwritten in place (HTTP 200); a retry is safe and creates nothing. `duplicate` — this exact recording is already stored under a DIFFERENT id, i.e. it arrived earlier through the `export.zip` importer (HTTP 200); nothing was written and `id` names the row that already holds it. A recording is the same recording when `(source, recordedAt, samplingFrequency)` match, which is enforced by a unique index rather than by convention. All three statuses mean the recording is stored and the client may advance its cursor.", ), - recordedAt: z.iso - .datetime({ offset: true }) - .describe("The `recordedAt` that was submitted, echoed back."), + recordedAt: isoDateTime().describe( + "The `recordedAt` that was submitted, echoed back.", + ), sampleCount: z .number() .int() @@ -1833,9 +1826,9 @@ const rhythmEventListItem = z id: z.string().describe("Measurement row id (cuid)."), type: rhythmEventType, classification: rhythmEventClassification, - occurredAt: z.iso - .datetime({ offset: true }) - .describe("Device-reported event time (`measuredAt`)."), + occurredAt: isoDateTime().describe( + "Device-reported event time (`measuredAt`).", + ), source: measurementSourceEnum, deviceType: z .string() @@ -1931,7 +1924,7 @@ export const dataSummary = z const lastSeenSlot = z .object({ - lastSeenAt: z.iso.datetime({ offset: true }), + lastSeenAt: isoDateTime(), daysAgo: z .number() .int() @@ -2368,8 +2361,7 @@ export const insightsSettingsResponse = z .describe( "The account's own ChatGPT-OAuth connection state; `disconnected` when the account row carries none.", ), - codexConnectedAt: z.iso - .datetime({ offset: true }) + codexConnectedAt: isoDateTime() .nullable() .describe("When that connection was made; null when never connected."), hasAdminKey: z @@ -2397,8 +2389,7 @@ export const insightsSettingsResponse = z .describe( "What leaves for the provider: aggregated figures or raw readings. `aggregated` when the account has never chosen.", ), - lastInsightAt: z.iso - .datetime({ offset: true }) + lastInsightAt: isoDateTime() .nullable() .describe("When the cached insight text was last written."), }) @@ -2604,8 +2595,7 @@ export const insightsGenerateReadResponse = z cached: z .boolean() .describe("True when a payload was served from the cache."), - cachedAt: z.iso - .datetime({ offset: true }) + cachedAt: isoDateTime() .nullable() .optional() .describe( @@ -2673,8 +2663,7 @@ export const insightsGeneratePostResponse = z "The insight payload, fresh or cached depending on `cached`, with its `dailyBriefing` resolved for today exactly as on the read GET. A cached briefing generated on an earlier calendar day never short-circuits an account with a provider: like a briefingless cache, it regenerates, and degrades to the cached payload on an exhausted quota.", ), cached: z.boolean(), - cachedAt: z.iso - .datetime({ offset: true }) + cachedAt: isoDateTime() .optional() .describe("Present on the cached branch only."), legacyPayload: z.boolean(), @@ -2794,7 +2783,7 @@ export const narrativeResponse = z .describe( "Where the text came from — provider prose or the deterministic non-causal fallback a provider-less account gets.", ), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .nullable() .describe( @@ -2842,11 +2831,9 @@ export const glp1TimelineResponse = z entries: z .array( z.object({ - date: z.iso - .datetime({ offset: true }) - .describe( - "When it happened. A `side-effect` entry is a DAY rather than an instant, and is stamped at 12:00 UTC of that day so it sorts — do not read it as a capture time.", - ), + date: isoDateTime().describe( + "When it happened. A `side-effect` entry is a DAY rather than an instant, and is stamped at 12:00 UTC of that day so it sorts — do not read it as a capture time.", + ), kind: z.enum([ "dose-change", "injection", diff --git a/src/lib/openapi/routes/integrations.ts b/src/lib/openapi/routes/integrations.ts index d1fe61eb6..f7a6cc166 100644 --- a/src/lib/openapi/routes/integrations.ts +++ b/src/lib/openapi/routes/integrations.ts @@ -49,6 +49,7 @@ import { whoopCredentialsSchema } from "@/lib/validations/whoop"; import { nightscoutConnectSchema } from "@/lib/validations/nightscout"; import { withingsCredentialsSchema } from "@/lib/validations/withings"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // Mirror the route's `directionEnum` — per-metric sync direction. const healthKitDirectionEnum = z @@ -74,6 +75,18 @@ const healthKitEntry = z "One resolved HealthKit metric mapping (defaults merged with the user's stored overrides).", }); +// The health-shaped fields that predate `health` (v1.42.4). They stay on the +// wire through v1.43 so App 1.2.0 keeps decoding, are marked deprecated in the +// document, and go in v1.44. +const LEGACY_HEALTH_FIELD = "Deprecated: removed in v1.44, read `health`."; + +function legacy(text?: string): { deprecated: true; description: string } { + return { + deprecated: true, + description: text ? `${LEGACY_HEALTH_FIELD} ${text}` : LEGACY_HEALTH_FIELD, + }; +} + // Mirrors `src/lib/integrations/sync-verdict.ts` — the server-resolved liveness // verdict. Apple Health is push-based, so only the data-age arms apply. const syncHealth = z @@ -92,8 +105,7 @@ const syncHealth = z .describe( "Liveness verdict. For Apple Health: `fresh` when data arrived within the last 7 days, `stale` when it did not, `pending_first_sync` when none ever arrived.", ), - since: z.iso - .datetime({ offset: true }) + since: isoDateTime() .nullable() .describe("The instant that triggered the verdict; null when none did."), }) @@ -117,8 +129,7 @@ export const connectionHealth = z .describe( "Why the state is what it is. ok: `data_arriving`, `first_sync_pending`. limited: `plan_limited`, `permission_limited`. degraded: `provider_errors`, `sync_stalled`, `no_new_data`, `unexpected_silence` (the integration watchdog saw a source that normally delivers daily go quiet). action_required: `reauth_required`, `paused_after_errors`. Pick the sentence from this code alone.", ), - since: z.iso - .datetime({ offset: true }) + since: isoDateTime() .nullable() .describe( "When the current degraded / action_required condition started. Null for ok and limited.", @@ -143,14 +154,12 @@ export const connectionHealth = z .describe( "Data types this connection cannot deliver. Present in every state, empty when nothing is withheld.", ), - lastSuccessAt: z.iso - .datetime({ offset: true }) + lastSuccessAt: isoDateTime() .nullable() .describe( "Last successful sync. For Apple Health, the last accepted batch.", ), - lastDataAt: z.iso - .datetime({ offset: true }) + lastDataAt: isoDateTime() .nullable() .describe( "Newest data this source delivered: for polled providers when the newest reading or workout was taken, for Apple Health when a sync last brought a new sample. Null when nothing has arrived.", @@ -162,8 +171,8 @@ export const connectionHealth = z .nullable() .describe("The stored sync-ledger state. Null for Apple Health."), failingLegs: z.array(z.string()), - failingSince: z.iso.datetime({ offset: true }).nullable(), - lastAttemptAt: z.iso.datetime({ offset: true }).nullable(), + failingSince: isoDateTime().nullable(), + lastAttemptAt: isoDateTime().nullable(), verdict: z .string() .describe("The `SyncHealth` verdict the state was derived from."), @@ -176,7 +185,7 @@ export const connectionHealth = z "Sync leg name (`sleep`, `measures`, `sync`, `token_refresh`), or `batch:` for Apple Health. Never a URL.", ), status: z.enum(RECENT_CALL_STATUSES), - at: z.iso.datetime({ offset: true }), + at: isoDateTime(), errorCode: z .string() .nullable() @@ -205,9 +214,9 @@ const metricFreshnessEntry = z .describe( "The measurement type (e.g. `RESPIRATORY_RATE`), `WORKOUTS` for the workout leg, or (Apple Health only, since v1.42.4) `MEDICATION_DOSE` for medication doses imported from Apple Health. Clients should show or skip a type they do not know rather than fail.", ), - lastSeenAt: z.iso - .datetime({ offset: true }) - .describe("Newest recorded reading for this type from this source."), + lastSeenAt: isoDateTime().describe( + "Newest recorded reading for this type from this source.", + ), stale: z .boolean() .describe( @@ -225,8 +234,7 @@ const metricFreshnessEntry = z // `HealthKitFreshnessEntry` in `src/lib/integrations/healthkit-type-sync.ts`. const appleHealthMetricFreshnessEntry = metricFreshnessEntry .extend({ - lastReceivedAt: z.iso - .datetime({ offset: true }) + lastReceivedAt: isoDateTime() .nullable() .optional() .describe( @@ -238,22 +246,19 @@ const appleHealthMetricFreshnessEntry = metricFreshnessEntry .describe( "The `syncTrigger` of that batch. Null when the client reported none. A type whose trigger is only ever `manual` reaches the server only through a user-started full sync. Same omission rules as `lastReceivedAt`.", ), - lastNewSampleAt: z.iso - .datetime({ offset: true }) + lastNewSampleAt: isoDateTime() .nullable() .optional() .describe( "When a live sync batch last brought a new or updated sample of this type, as opposed to duplicates only. Null when none has. Same omission rules as `lastReceivedAt`.", ), - lastBackgroundAt: z.iso - .datetime({ offset: true }) + lastBackgroundAt: isoDateTime() .nullable() .optional() .describe( 'When a batch with `syncTrigger: "background"` last carried this type (since v1.42.4). A later `foreground`, `push` or `manual` batch moves `lastReceivedAt` and `lastTrigger` but leaves this standing, so opening the app does not hide an earlier background arrival. Null when the type has never arrived in the background as far as the server recorded: before v1.42.4 only the latest arrival was kept, so a type whose latest arrival then was a background one starts with that date and every other type starts null. Same omission rules as `lastReceivedAt`.', ), - lastBackfillAt: z.iso - .datetime({ offset: true }) + lastBackfillAt: isoDateTime() .nullable() .optional() .describe( @@ -277,8 +282,7 @@ const appleHealthSyncProgress = z .describe( "Measurement + workout rows carrying the `APPLE_HEALTH` source (live batch ingest and the one-shot export import both write it). Soft-deleted measurement rows are excluded.", ), - oldestMeasuredAt: z.iso - .datetime({ offset: true }) + oldestMeasuredAt: isoDateTime() .nullable() .describe( "Earliest instant reached across those rows (`measuredAt` for measurements, `startedAt` for workouts); null when nothing has arrived. During a first-run backfill this walks backwards as history lands.", @@ -293,8 +297,7 @@ const appleHealthSyncProgress = z const healthKitConfigResponse = z .object({ entries: z.array(healthKitEntry), - lastSyncedAt: z.iso - .datetime({ offset: true }) + lastSyncedAt: isoDateTime() .nullable() .describe( "When HealthKit last synced for this user; null when never (and always null on the PATCH echo).", @@ -305,8 +308,7 @@ const healthKitConfigResponse = z .describe( "What the client declared triggered the most recent accepted batch. Null when the client sent no trigger — an older build, or a batch that predates the field. Present on the GET read; omitted from the PATCH echo.", ), - lastBackgroundSyncAt: z.iso - .datetime({ offset: true }) + lastBackgroundSyncAt: isoDateTime() .nullable() .optional() .describe( @@ -314,7 +316,7 @@ const healthKitConfigResponse = z ), syncHealth: syncHealth .optional() - .describe("Present on the GET read; omitted from the PATCH echo."), + .meta(legacy("Present on the GET read; omitted from the PATCH echo.")), metricFreshness: z .array(appleHealthMetricFreshnessEntry) .optional() @@ -515,17 +517,19 @@ const nightscoutStatusResponse = z .describe( "The operator has approved this user's private-origin (RFC 1918 / loopback / link-local) Nightscout instance. False for every public instance.", ), - state: integrationLedgerState.optional(), - lastSuccessAt: z.iso.datetime({ offset: true }).nullable().optional(), - lastAttemptAt: z.iso.datetime({ offset: true }).nullable().optional(), + state: integrationLedgerState.optional().meta(legacy()), + lastSuccessAt: isoDateTime().nullable().optional(), + lastAttemptAt: isoDateTime().nullable().optional(), lastError: z .string() .nullable() .optional() - .describe( - "Last recorded failure message, decrypted from the ledger. Null when the last attempt succeeded or none has run.", + .meta( + legacy( + "Last recorded failure message, decrypted from the ledger. Null when the last attempt succeeded or none has run.", + ), ), - syncHealth: syncHealth.optional(), + syncHealth: syncHealth.optional().meta(legacy()), health: connectionHealth .nullable() .optional() @@ -581,22 +585,22 @@ const whoopStatusResponse = z configured: z .boolean() .describe("Per-user BYO-key client id AND secret are both stored."), - lastSyncedAt: z.iso - .datetime({ offset: true }) + lastSyncedAt: isoDateTime() .nullable() .optional() .describe("Null when no sync has completed yet."), - connectedAt: z.iso - .datetime({ offset: true }) + connectedAt: isoDateTime() .optional() .describe("When the connection row was created."), tokenExpired: z .boolean() .optional() - .describe( - "The stored access token's expiry is in the past. Read straight off the row — unlike the Withings status read, this endpoint does not refresh; the sync path refreshes lazily instead.", + .meta( + legacy( + "The stored access token's expiry is in the past. Read straight off the row — unlike the Withings status read, this endpoint does not refresh; the sync path refreshes lazily instead.", + ), ), - tokenExpiresAt: z.iso.datetime({ offset: true }).optional(), + tokenExpiresAt: isoDateTime().optional(), backfillCompleted: z .boolean() .optional() @@ -629,13 +633,15 @@ const withingsStatusResponse = z configured: z .boolean() .describe("Per-user client id AND secret are both stored."), - lastSyncedAt: z.iso.datetime({ offset: true }).nullable().optional(), - connectedAt: z.iso.datetime({ offset: true }).optional(), + lastSyncedAt: isoDateTime().nullable().optional(), + connectedAt: isoDateTime().optional(), tokenExpired: z .boolean() .optional() - .describe( - "True after a refresh attempt that did not produce a live token.", + .meta( + legacy( + "True after a refresh attempt that did not produce a live token.", + ), ), tokenRefreshFailed: z .boolean() @@ -643,8 +649,7 @@ const withingsStatusResponse = z .describe( "The read tried to refresh an expired token and could not — either the per-user credentials are gone or Withings refused. A permanently revoked grant additionally parks the ledger at `error_reauth` as a side effect of this read.", ), - tokenExpiresAt: z.iso - .datetime({ offset: true }) + tokenExpiresAt: isoDateTime() .optional() .describe( "Post-refresh expiry when the read refreshed, the stored expiry otherwise.", @@ -659,8 +664,10 @@ const withingsStatusResponse = z hasActivityScope: z .boolean() .optional() - .describe( - "Pre-computed convenience flag the reconnect banner reads. A null `scope` counts as missing activity.", + .meta( + legacy( + "Pre-computed convenience flag the reconnect banner reads. A null `scope` counts as missing activity.", + ), ), health: connectionHealth .nullable() @@ -785,8 +792,7 @@ const providerProbeResponse = z const integrationProbeResponse = z .object({ ok: z.literal(true), - lastSyncedAt: z.iso - .datetime({ offset: true }) + lastSyncedAt: isoDateTime() .nullable() .describe( "The connection row's last successful sync; null when it has never synced. Present here and absent from the `/api//test` family — the one shape difference between the two probe families.", @@ -929,15 +935,17 @@ function connectionStatusShape() { configured: z .boolean() .describe("Per-user BYO-key client id AND secret are both stored."), - lastSyncedAt: z.iso.datetime({ offset: true }).nullable().optional(), - connectedAt: z.iso.datetime({ offset: true }).optional(), + lastSyncedAt: isoDateTime().nullable().optional(), + connectedAt: isoDateTime().optional(), tokenExpired: z .boolean() .optional() - .describe( - "Read straight off the row — this endpoint does not refresh; the sync path refreshes lazily.", + .meta( + legacy( + "Read straight off the row — this endpoint does not refresh; the sync path refreshes lazily.", + ), ), - tokenExpiresAt: z.iso.datetime({ offset: true }).optional(), + tokenExpiresAt: isoDateTime().optional(), backfillCompleted: z .boolean() .optional() @@ -970,8 +978,10 @@ const googleHealthStatusResponse = z needsReauth: z .boolean() .optional() - .describe( - "The stored refresh token was rejected with `invalid_grant` — Google expires it after 7 days while the OAuth client is in Testing publishing mode, and a user-revoked grant lands here too. Distinct from `parked` and from `disconnected`: the connection still exists, it just cannot mint a token until the user reconnects. Only Google Health has this field.", + .meta( + legacy( + "The stored refresh token was rejected with `invalid_grant` — Google expires it after 7 days while the OAuth client is in Testing publishing mode, and a user-revoked grant lands here too. Distinct from `parked` and from `disconnected`: the connection still exists, it just cannot mint a token until the user reconnects. Only Google Health has this field.", + ), ), }) .meta({ @@ -1003,11 +1013,11 @@ const oauthProviderStatusResponse = z hasOwnCredentials: z .boolean() .describe("The user has stored their own BYO client id and secret."), - state: integrationLedgerState.optional(), - lastSuccessAt: z.iso.datetime({ offset: true }).nullable().optional(), - lastAttemptAt: z.iso.datetime({ offset: true }).nullable().optional(), - lastError: z.string().nullable().optional(), - syncHealth: syncHealth.optional(), + state: integrationLedgerState.optional().meta(legacy()), + lastSuccessAt: isoDateTime().nullable().optional(), + lastAttemptAt: isoDateTime().nullable().optional(), + lastError: z.string().nullable().optional().meta(legacy()), + syncHealth: syncHealth.optional().meta(legacy()), health: connectionHealth .nullable() .optional() @@ -1030,8 +1040,10 @@ const consecutiveFailuresByKind = z persistent: z.number().int(), }) .nullable() - .describe( - "Per-kind consecutive-failure counters, or null when the row has never written a bucket payload. Compare against the top-level `threshold` to know how close the provider is to the operator alert.", + .meta( + legacy( + "Per-kind consecutive-failure counters, or null when the row has never written a bucket payload. Compare against the top-level `threshold` to know how close the provider is to the operator alert.", + ), ); const integrationEnvelopeEntry = z @@ -1049,12 +1061,12 @@ const integrationEnvelopeEntry = z "dawarich", ]) .describe("Which provider this entry describes."), - state: integrationLedgerState, - lastSuccessAt: z.iso.datetime({ offset: true }).nullable(), - lastAttemptAt: z.iso.datetime({ offset: true }).nullable(), - lastError: z.string().nullable(), + state: integrationLedgerState.meta(legacy()), + lastSuccessAt: isoDateTime().nullable(), + lastAttemptAt: isoDateTime().nullable(), + lastError: z.string().nullable().meta(legacy()), consecutiveFailuresByKind, - syncHealth, + syncHealth: syncHealth.meta(legacy()), metricFreshness: z .array(metricFreshnessEntry) .describe( @@ -1062,30 +1074,31 @@ const integrationEnvelopeEntry = z ), connected: z.boolean(), configured: z.boolean(), - connectedAt: z.iso.datetime({ offset: true }).nullable().optional(), - legacyLastSyncedAt: z.iso - .datetime({ offset: true }) + connectedAt: isoDateTime().nullable().optional(), + legacyLastSyncedAt: isoDateTime() .nullable() .optional() .describe( "The connection row's own last-sync instant, for legacy connections established before the ledger existed. Not present on the three user-row OAuth providers or on Nightscout.", ), - tokenExpiresAt: z.iso.datetime({ offset: true }).nullable().optional(), + tokenExpiresAt: isoDateTime().nullable().optional(), tokenExpired: z .boolean() .nullable() .optional() - .describe("Null — not false — when there is no connection to judge."), + .meta(legacy("Null — not false — when there is no connection to judge.")), backfillCompleted: z.boolean().nullable().optional(), scope: z.string().nullable().optional().describe("Withings only."), - hasActivityScope: z.boolean().optional().describe("Withings only."), + hasActivityScope: z.boolean().optional().meta(legacy("Withings only.")), planUnavailable: z .array(z.enum(WITHINGS_PLAN_FEATURES)) .optional() - .describe( - "Withings only. The readings this connection's Withings API plan withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when the plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when the weekly re-probe asks again.", + .meta( + legacy( + "Withings only. The readings this connection's Withings API plan withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when the plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when the weekly re-probe asks again.", + ), ), - needsReauth: z.boolean().optional().describe("Google Health only."), + needsReauth: z.boolean().optional().meta(legacy("Google Health only.")), available: z.boolean().optional().describe("Oura / Polar / Strava only."), hasOwnCredentials: z .boolean() @@ -1094,15 +1107,17 @@ const integrationEnvelopeEntry = z scopeReconnect: z .array(z.enum(OURA_SCOPE_GATED_COLLECTIONS)) .optional() - .describe( - "Oura only. Collections the stored grant does not cover: they answered 401 while sibling collections succeeded with the same token. Not a failure and not counted toward the reconnect threshold; one reconnect asks for the missing scopes. Empty when the grant covers everything HealthLog reads.", + .meta( + legacy( + "Oura only. Collections the stored grant does not cover: they answered 401 while sibling collections succeeded with the same token. Not a failure and not counted toward the reconnect threshold; one reconnect asks for the missing scopes. Empty when the grant covers everything HealthLog reads.", + ), ), hasToken: z.boolean().optional().describe("Nightscout only."), allowPrivateHost: z.boolean().optional().describe("Nightscout only."), health: connectionHealth .nullable() .describe( - "The unified connection state, null when the provider is not connected. It supersedes `state`, `lastError`, `consecutiveFailuresByKind`, `syncHealth`, `tokenExpired`, `needsReauth`, `hasActivityScope`, `planUnavailable` and `scopeReconnect` on this entry, which go once the settings surface renders `health` (no other client reads this envelope).", + "The unified connection state, null when the provider is not connected. It supersedes `state`, `lastError`, `consecutiveFailuresByKind`, `syncHealth`, `tokenExpired`, `needsReauth`, `hasActivityScope`, `planUnavailable` and `scopeReconnect` on this entry; those nine are deprecated and removed in v1.44.", ), }) .meta({ @@ -1127,7 +1142,7 @@ const integrationStatusEnvelope = z integrations: z .array(integrationEnvelopeEntry) .describe( - "All nine providers, always, in the fixed order withings, whoop, fitbit, google-health, polar, oura, strava, nightscout, dawarich. A provider the user has never touched still appears, with `connected: false` and a synthesised `connected` ledger state — absence from this list never means anything, because nothing is ever absent from it.", + "All nine providers, always, in the fixed order withings, whoop, fitbit, google-health, polar, oura, strava, nightscout, dawarich. A provider the user has never touched still appears, with `connected: false` and the ledger state `unknown` — absence from this list never means anything, because nothing is ever absent from it.", ), }) .meta({ @@ -2672,7 +2687,7 @@ export const integrationPaths: NonNullable = { z .object({ probe: z.literal("structure"), - probedAt: z.iso.datetime({ offset: true }), + probedAt: isoDateTime(), types: z .record(z.string(), z.unknown()) .describe( diff --git a/src/lib/openapi/routes/life-circumstances.ts b/src/lib/openapi/routes/life-circumstances.ts index 706107f1b..590dcabd3 100644 --- a/src/lib/openapi/routes/life-circumstances.ts +++ b/src/lib/openapi/routes/life-circumstances.ts @@ -29,6 +29,7 @@ import { recordRefusal, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const lifeFactRevision = lifeFactRevisionSchema.meta({ id: "LifeFactRevision", @@ -46,7 +47,7 @@ const auditCCutoff = z.object({ const auditCResult = z .object({ id: z.string(), - takenAt: z.iso.datetime({ offset: true }), + takenAt: isoDateTime(), items: z.tuple([z.number().int(), z.number().int(), z.number().int()]), totalScore: z.number().int().min(0).max(12), locale: z.string(), @@ -92,7 +93,7 @@ const lifeCircumstances = z "GOALS", ]), aiIncluded: z.boolean(), - lastConfirmedAt: z.iso.datetime({ offset: true }).nullable(), + lastConfirmedAt: isoDateTime().nullable(), reviewDue: z .boolean() .describe("Older than 365 days; show the quiet yearly check."), @@ -123,7 +124,7 @@ const lifeCircumstances = z .array( z.object({ id: z.string(), - takenAt: z.iso.datetime({ offset: true }), + takenAt: isoDateTime(), totalScore: z.number().int(), }), ) @@ -218,7 +219,7 @@ export const lifeCircumstancesPaths: NonNullable = { z.enum(LIFE_AI_SECTIONS), z.boolean(), ), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }), "LifeAiSectionsEnvelope", ), @@ -251,7 +252,7 @@ export const lifeCircumstancesPaths: NonNullable = { schema: dataEnvelope( z.object({ section: z.enum(LIFE_SECTION_KEYS), - lastConfirmedAt: z.iso.datetime({ offset: true }), + lastConfirmedAt: isoDateTime(), }), "LifeConfirmEnvelope", ), @@ -317,7 +318,7 @@ export const lifeCircumstancesPaths: NonNullable = { schema: dataEnvelope( z.object({ id: z.string(), - removedAt: z.iso.datetime({ offset: true }), + removedAt: isoDateTime(), }), "AuditCRemovedEnvelope", ), diff --git a/src/lib/openapi/routes/mcp.ts b/src/lib/openapi/routes/mcp.ts index ca585bd6e..009502db3 100644 --- a/src/lib/openapi/routes/mcp.ts +++ b/src/lib/openapi/routes/mcp.ts @@ -26,6 +26,7 @@ import type { ZodOpenApiObject } from "zod-openapi"; import { createMcpTokenSchema } from "@/lib/validations/mcp"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * The operator kill switch. `isApiGloballyEnabled()` gates every route in this @@ -51,9 +52,8 @@ const mcpConnection = z scope: z .string() .describe("Space-separated scope string granted to this connection."), - createdAt: z.iso.datetime({ offset: true }), - lastUsedAt: z.iso - .datetime({ offset: true }) + createdAt: isoDateTime(), + lastUsedAt: isoDateTime() .nullable() .describe("Null when the connection has never been exercised."), }) @@ -72,9 +72,9 @@ const mcpConnectorToken = z .describe( 'The granted scopes — `["health:read"]` or `["health:read", "health:write"]`. A `health:write` token is audience-bound to `/mcp`: the resource-server guard refuses it on every REST write, so it only admits the confirmed in-process write tools.', ), - lastUsedAt: z.iso.datetime({ offset: true }).nullable(), - expiresAt: z.iso.datetime({ offset: true }).nullable(), - createdAt: z.iso.datetime({ offset: true }), + lastUsedAt: isoDateTime().nullable(), + expiresAt: isoDateTime().nullable(), + createdAt: isoDateTime(), revoked: z .boolean() .describe( @@ -101,7 +101,7 @@ const createMcpTokenResponse = z "The raw `hlk_` Bearer, RETURNED EXACTLY ONCE. It is stored only as an HMAC-SHA256 hash, so there is no way to retrieve it later; a client that does not capture it here has to mint a replacement. Treat it as a secret in logs, crash reports and analytics.", ), name: z.string(), - expiresAt: z.iso.datetime({ offset: true }), + expiresAt: isoDateTime(), }) .meta({ id: "CreateMcpTokenResponse", diff --git a/src/lib/openapi/routes/measurements.ts b/src/lib/openapi/routes/measurements.ts index ee7b945b2..0138c5937 100644 --- a/src/lib/openapi/routes/measurements.ts +++ b/src/lib/openapi/routes/measurements.ts @@ -30,6 +30,7 @@ import { recordWriteRateLimitResponse, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const batchEntrySchema = z .object({ @@ -67,8 +68,8 @@ const batchEntrySchema = z .describe( "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 }), + startDate: isoDateTime(), + endDate: isoDateTime(), sleepStage: z .number() .int() @@ -260,7 +261,7 @@ export const measurementResource = z type: measurementTypeEnum, value: z.number(), unit: z.string(), - measuredAt: z.iso.datetime({ offset: true }), + measuredAt: isoDateTime(), source: measurementSourceEnum, notes: z.string().nullable().optional(), // The list / detail reads hand back the whole stored row minus the @@ -318,13 +319,12 @@ export const measurementResource = z .describe( "Bumped on every server-side mutation; the last-writer-wins input for cross-device reconciliation.", ), - deletedAt: z.iso - .datetime({ offset: true }) + deletedAt: isoDateTime() .nullable() .optional() .describe("Soft-delete tombstone; null on a live row."), - createdAt: z.iso.datetime({ offset: true }).optional(), - updatedAt: z.iso.datetime({ offset: true }).optional(), + createdAt: isoDateTime().optional(), + updatedAt: isoDateTime().optional(), // Synthesised-row extras — present only on the collapsed day-sum mode. dayKey: z.string().optional(), sampleCount: z.number().int().optional(), @@ -364,7 +364,7 @@ const aggregatedMeasurementBucket = z .object({ type: measurementTypeEnum, value: z.number().describe("The bucket's aggregated value."), - measuredAt: z.iso.datetime({ offset: true }).describe("Bucket start."), + measuredAt: isoDateTime().describe("Bucket start."), count: z.number().int().nonnegative().optional(), unit: z.string().optional(), }) @@ -400,8 +400,7 @@ const listMeasurementsMeta = z .describe( "Rows discarded by the cross-source canonical picker on the day-collapse mode.", ), - windowFrom: z.iso - .datetime({ offset: true }) + windowFrom: isoDateTime() .optional() .describe( "Set on the per-night SLEEP_DURATION mode when the requested window was longer than 365 days: the instant the read started from. The window is a year ending at `to` (or now); page further back by moving `to`.", @@ -422,16 +421,16 @@ const sleepStageEnum = z.enum([ const sleepSegmentResource = z.object({ stage: sleepStageEnum.nullable(), - start: z.iso.datetime({ offset: true }), - end: z.iso.datetime({ offset: true }), + start: isoDateTime(), + end: isoDateTime(), minutes: z.number().int().nonnegative(), }); const sleepSessionResource = z.object({ night: z.string().describe("Wake-day key (YYYY-MM-DD)."), source: measurementSourceEnum.nullable(), - start: z.iso.datetime({ offset: true }), - end: z.iso.datetime({ offset: true }), + start: isoDateTime(), + end: isoDateTime(), asleepMinutes: z.number().int().nonnegative(), inBedMinutes: z.number().int().nonnegative().nullable(), awakeMinutes: z.number().int().nonnegative().nullable(), @@ -643,7 +642,7 @@ const seriesPointSchema = z.object({ .describe( "Measurement row id; `sleep:` for a sleep night; `day:` for a day bucket (`pulse`, `glucose`, `heartRateVariability`, `oxygenSaturation` beyond 90 days); `hour:` for an hour bucket (the same kinds within 90 days when the window holds more than 10 000 readings).", ), - at: z.iso.datetime({ offset: true }).describe("Point timestamp (ISO-8601)."), + at: isoDateTime().describe("Point timestamp (ISO-8601)."), value: z.number().describe("Primary value in the top-level `unit`."), secondary: z .number() @@ -724,11 +723,9 @@ const seriesBatchRowSchema = z.object({ .describe( "The day's mean (spot metrics) or total (cumulative). A row of a coarser tier (a window longer than the daily cap) carries the average day of its bucket, the same quantity: a spot metric's mean of its daily means, a cumulative metric's total over the days of the bucket that hold a reading.", ), - measuredAt: z.iso - .datetime({ offset: true }) - .describe( - "Day-bucket start (ISO-8601): the user's local midnight. A window longer than the daily cap is served from a coarser tier (a UTC day, week or month); its rows are stamped at noon UTC of the bucket's first day, which falls inside the bucket's own week or month in every zone.", - ), + measuredAt: isoDateTime().describe( + "Day-bucket start (ISO-8601): the user's local midnight. A window longer than the daily cap is served from a coarser tier (a UTC day, week or month); its rows are stamped at noon UTC of the bucket's first day, which falls inside the bucket's own week or month in every zone.", + ), count: z .number() .int() diff --git a/src/lib/openapi/routes/medications/paths.ts b/src/lib/openapi/routes/medications/paths.ts index 7ac5aa6dd..9edc5ea07 100644 --- a/src/lib/openapi/routes/medications/paths.ts +++ b/src/lib/openapi/routes/medications/paths.ts @@ -44,6 +44,7 @@ import { recordWriteRateLimitResponse, stdResponses, } from "../shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // Zod 4's `.meta()` returns a NEW instance carrying the id rather than // annotating in place, so each annotated schema is bound to a const and the @@ -145,12 +146,10 @@ const bulkDeleteIntakeRequest = bulkDeleteIntakeEventsSchema.meta({ const bulkIntakeEntry = z .object({ medicationId: z.string().min(1), - scheduledFor: z.iso - .datetime({ offset: true }) + scheduledFor: isoDateTime() .optional() .describe("ISO instant; defaults to `takenAt` then now() when omitted."), - takenAt: z.iso - .datetime({ offset: true }) + takenAt: isoDateTime() .optional() .describe("ISO instant of the take; omit + `skipped:false` = pending."), skipped: z.boolean().optional().describe("Default false."), @@ -160,8 +159,7 @@ const bulkIntakeEntry = z .describe( "Per-entry injection site; a disallowed site marks THIS entry skipped without failing the batch.", ), - forceSlotInstant: z.iso - .datetime({ offset: true }) + forceSlotInstant: isoDateTime() .optional() .describe( "Pin a taken entry onto a named real scheduled slot; ignored on non-taken entries.", diff --git a/src/lib/openapi/routes/medications/schemas.ts b/src/lib/openapi/routes/medications/schemas.ts index abedc3702..50d1c89f9 100644 --- a/src/lib/openapi/routes/medications/schemas.ts +++ b/src/lib/openapi/routes/medications/schemas.ts @@ -21,6 +21,7 @@ import { MEDICATION_ORDER_MAX_ENTRIES, } from "@/lib/medication-list-layout"; import { MEDICATION_IMPORT_SKIP_REASONS } from "@/lib/jobs/medication-intake-import"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Medications (v1.5 scheduling) ──────────────────────────────────── // @@ -278,16 +279,15 @@ export const medicationResource = z .describe( "v1.9.0 optional RxNorm RxCUI (numeric, US identifier, e.g. `2601723`). Secondary FHIR coding under `http://www.nlm.nih.gov/research/umls/rxnorm`, alongside any ATC code. NULL = no code captured.", ), - pausedAt: z.iso.datetime({ offset: true }).nullable(), - snoozedUntil: z.iso.datetime({ offset: true }).nullable(), + pausedAt: isoDateTime().nullable(), + snoozedUntil: isoDateTime().nullable(), remindersActive: z .boolean() .optional() .describe( "Present on the READ paths only (list and single GET). v1.42.2 — whether the server sends dose reminders for this medication: true only when `notificationsEnabled` is on, the medication is `active`, its course has not ended (`endsOn` is today or later on the person's clock, or unset), its intake is tracked (`trackIntake`), it is not `asNeeded`, and `autoMarkTaken` is off. The same decision gates every server-side reminder send. A client that arms local reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, not stored.", ), - nextDueAt: z.iso - .datetime({ offset: true }) + nextDueAt: isoDateTime() .nullable() .optional() .describe( @@ -299,14 +299,12 @@ export const medicationResource = z .describe( "Present on the READ paths only, alongside `nextDueAt`. v1.16.4 — true when `nextDueAt` is an OPEN overdue slot: its anchor has passed, `now` is still inside the slot's catch-up band, and no taken / skipped / auto-missed row resolves it. False for a regular future next-due (and when `nextDueAt` is null). Read-only — computed, not stored.", ), - startsOn: z.iso - .datetime({ offset: true }) + startsOn: isoDateTime() .nullable() .describe( "v1.5 course start (ISO date). Anchors RRULE BYDAY / BYMONTHDAY patterns and the rolling-interval countdown's first window. NULL means active from creation.", ), - endsOn: z.iso - .datetime({ offset: true }) + endsOn: isoDateTime() .nullable() .describe( "v1.5 course end (ISO date). NULL means chronic. Equals `startsOn` when `oneShot` is true.", @@ -331,14 +329,13 @@ export const medicationResource = z .describe( "v1.42.2 — doses are recorded automatically. False (the default) for every medication that existed before the field. When true the server records each scheduled dose as taken (an intake with `autoRecorded: true`, `takenAt` = the slot time) shortly after its time passes, from `autoMarkTakenSince` on. A slot the person took, skipped or removed is never touched, and they can still change or skip any dose. No medication reminder is sent for it on any channel while it is on; a client that arms local reminders should not arm them for it either. `notificationsEnabled` keeps the person's own setting and applies again once this is switched off.", ), - autoMarkTakenSince: z.iso - .datetime({ offset: true }) + autoMarkTakenSince: isoDateTime() .nullable() .describe( "v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded automatically. NULL while the option is off. Read-only — set by the server on the switch.", ), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), schedules: z .array(medicationScheduleResource) .describe( @@ -372,7 +369,7 @@ export const medicationListEntry = medicationResource ), // The list read always computes these two, so they are required here // even though the shared base leaves them optional for the write paths. - nextDueAt: z.iso.datetime({ offset: true }).nullable(), + nextDueAt: isoDateTime().nullable(), nextDueOverdue: z.boolean(), remindersActive: z .boolean() @@ -389,8 +386,7 @@ export const medicationListEntry = medicationResource .describe( "v1.39.4 — whether the medication offers taken / skip today: true only when it is `active`, its intake is tracked (`trackIntake`) and `courseStatus` is CURRENT. Clients render the dose actions from this and do not re-derive it. Logging a past dose from the history is not gated by it. Read-only — computed, not stored.", ), - lastTakenAt: z.iso - .datetime({ offset: true }) + lastTakenAt: isoDateTime() .nullable() .describe( "Latest non-skipped `MedicationIntakeEvent.takenAt` for the medication. Drives the rolling-cadence countdown surface.", @@ -521,20 +517,18 @@ export const medicationInventoryItemResource = z .describe( 'Units left in the container. v1.16.12 — fractional (a ½-tablet dose leaves 29.5 of 30). Decremented by the intake consumption hook (FEFO with spillover across containers); refunded when a taken dose is skipped, edited away, or deleted. v1.18.3 (iOS#31) — NULL when the count is unknown (a corrupt or legacy row); the client renders "unknown", never a fabricated 0 it could decrement into negatives.', ), - firstUseAt: z.iso - .datetime({ offset: true }) + firstUseAt: isoDateTime() .nullable() .describe( "Instant the container was first used. NULL until opened. On a PEN / AMPOULE it starts the 30-day in-use clock; on the other kinds it only records that the pack is open.", ), - expiresAt: z.iso - .datetime({ offset: true }) + expiresAt: isoDateTime() .nullable() .describe( "Persisted MIN(firstUseAt + 30 days, printedExpiry) for a clock-bearing container (PEN / AMPOULE), else printedExpiry alone. NULL when no deadline applies — an opened blister with no printed date has none.", ), - printedExpiry: z.iso.datetime({ offset: true }).nullable(), - purchasedAt: z.iso.datetime({ offset: true }).nullable(), + printedExpiry: isoDateTime().nullable(), + purchasedAt: isoDateTime().nullable(), manufacturer: z .string() .nullable() @@ -548,8 +542,8 @@ export const medicationInventoryItemResource = z 'Strength as printed on the container, e.g. "5 mg/0.5 ml". Free text, not split into a number + unit — pens state strength per dose, per ml, or per cartridge. NULL when unknown.', ), notes: z.string().nullable(), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "MedicationInventoryItem", @@ -597,8 +591,8 @@ export const medicationIntakeEventResource = z id: z.string(), userId: z.string(), medicationId: z.string(), - scheduledFor: z.iso.datetime({ offset: true }), - takenAt: z.iso.datetime({ offset: true }).nullable(), + scheduledFor: isoDateTime(), + takenAt: isoDateTime().nullable(), skipped: z.boolean(), autoMissed: z .boolean() @@ -617,8 +611,8 @@ export const medicationIntakeEventResource = z ), source: z.enum(["WEB", "API", "REMINDER", "IMPORT", "APPLE_HEALTH"]), idempotencyKey: z.string().nullable(), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), injectionSite: z .enum([ "ABDOMEN_LEFT", @@ -658,8 +652,7 @@ export const medicationIntakeEventResource = z .describe( "Monotonic per-row version for incremental sync readers; bumps on every mutation.", ), - deletedAt: z.iso - .datetime({ offset: true }) + deletedAt: isoDateTime() .nullable() .describe( "Soft-delete tombstone. Non-null rows are excluded from every list/aggregate read; sync readers use it to propagate deletions.", @@ -673,9 +666,9 @@ export const medicationIntakeEventResource = z export const medicationCadenceTimelinePoint = z .object({ - day: z.iso.datetime({ offset: true }), - windowStart: z.iso.datetime({ offset: true }), - windowEnd: z.iso.datetime({ offset: true }), + day: isoDateTime(), + windowStart: isoDateTime(), + windowEnd: isoDateTime(), scheduleIndex: z.number().int().nonnegative(), status: z.string(), }) @@ -725,11 +718,11 @@ export const medicationCadenceChips = z export const medicationCadenceResponse = z .object({ windowDays: z.number().int().positive(), - anchorIso: z.iso.datetime({ offset: true }), + anchorIso: isoDateTime(), next: z .object({ - windowStart: z.iso.datetime({ offset: true }), - windowEnd: z.iso.datetime({ offset: true }), + windowStart: isoDateTime(), + windowEnd: isoDateTime(), scheduleIndex: z.number().int().nonnegative(), }) .nullable(), @@ -878,14 +871,12 @@ export const complianceDisplay = z .describe( "Open-cycle state, decoupled from the percentage rows: `on_track` = next dose not yet due; `due` = due now / in grace; `missed` = past grace with no logged intake (the only red state); `none` = no projected next dose (PRN / paused / ended).", ), - nextDueAt: z.iso - .datetime({ offset: true }) + nextDueAt: isoDateTime() .nullable() .describe( "The open cycle's due instant. Null when `state` is `none`.", ), - graceUntil: z.iso - .datetime({ offset: true }) + graceUntil: isoDateTime() .nullable() .describe( "End of the due slot's grace window. Null when `state` is `none`.", @@ -914,8 +905,7 @@ export const complianceDisplay = z .describe( "Per-dose state of the open cycle, server-derived from the window model so the card renders the take-window (green) / overdue / heavily-overdue escalation from one authority. `upcoming` when no dose is open (PRN / paused / ended).", ), - targetAt: z.iso - .datetime({ offset: true }) + targetAt: isoDateTime() .nullable() .describe( "Target instant of the open dose. Null when no dose is open.", @@ -1030,12 +1020,12 @@ export const scheduleRevisionEntrySummary = z export const scheduleRevisionResource = z .object({ id: z.string(), - validFrom: z.iso.datetime().describe("Inclusive start instant of the era."), - validUntil: z.iso - .datetime() - .describe( - "Exclusive end instant of the era — the moment the next plan took over.", - ), + validFrom: isoDateTime({ offset: false }).describe( + "Inclusive start instant of the era.", + ), + validUntil: isoDateTime({ offset: false }).describe( + "Exclusive end instant of the era — the moment the next plan took over.", + ), source: z .enum(["ARCHIVED", "MANUAL"]) .describe( @@ -1050,11 +1040,9 @@ export const scheduleRevisionResource = z }); export const scheduleRevisionListResponse = z.object({ - currentSince: z.iso - .datetime() - .describe( - "Instant the LIVE plan took over: the newest revision's `validUntil`, or the medication's `createdAt` when no era has been archived.", - ), + currentSince: isoDateTime({ offset: false }).describe( + "Instant the LIVE plan took over: the newest revision's `validUntil`, or the medication's `createdAt` when no era has been archived.", + ), revisions: z .array(scheduleRevisionResource) .describe("Archived eras, newest first."), @@ -1099,14 +1087,12 @@ export const medicationExtractRequest = z // (iOS #64) adds `intake.source` so a client can label how each dose was // recorded. export const doseHistoryQuery = z.object({ - from: z.iso - .datetime({ offset: true }) + from: isoDateTime() .optional() .describe( "Window start (inclusive). Defaults to 90 days before `to`; clamped to a 366-day span floor. Recorded doses are returned from this instant. A slot from before the medication's `createdAt` appears only when a recorded row (a take, a skip, or a miss carried in with restored history) claims it; the server's own placeholder for such a slot is not returned.", ), - to: z.iso - .datetime({ offset: true }) + to: isoDateTime() .optional() .describe( "Window end (inclusive). Defaults to now. Must be ≥ `from`. A dose recorded ahead of its slot (the slot's time within a day after `to`) is still returned.", @@ -1120,9 +1106,9 @@ const doseHistoryRow = z .describe( "`slot` — a scheduled dose window; `ad_hoc` — a standalone off-schedule intake.", ), - at: z.iso - .datetime({ offset: true }) - .describe("The slot anchor instant, or the ad-hoc take's own time."), + at: isoDateTime().describe( + "The slot anchor instant, or the ad-hoc take's own time.", + ), timeOfDay: z .string() .nullable() @@ -1143,7 +1129,7 @@ const doseHistoryRow = z ), nearestSlot: z .object({ - at: z.iso.datetime({ offset: true }), + at: isoDateTime(), timeOfDay: z.string(), filled: z.boolean(), }) @@ -1154,8 +1140,8 @@ const doseHistoryRow = z intake: z .object({ id: z.string().nullable(), - scheduledFor: z.iso.datetime({ offset: true }), - takenAt: z.iso.datetime({ offset: true }).nullable(), + scheduledFor: isoDateTime(), + takenAt: isoDateTime().nullable(), skipped: z.boolean(), autoMissed: z.boolean(), autoRecorded: z @@ -1181,16 +1167,12 @@ const doseHistoryRow = z export const doseHistoryResponse = z .object({ - from: z.iso - .datetime({ offset: true }) - .describe( - "Start of the window recorded doses were read over: the requested `from` after the span clamp.", - ), - to: z.iso - .datetime({ offset: true }) - .describe( - "End of the window recorded doses were read over: the requested `to`, or the slot time of a dose recorded ahead of its slot when that lies later (at most one day after `to`).", - ), + from: isoDateTime().describe( + "Start of the window recorded doses were read over: the requested `from` after the span clamp.", + ), + to: isoDateTime().describe( + "End of the window recorded doses were read over: the requested `to`, or the slot time of a dose recorded ahead of its slot when that lies later (at most one day after `to`).", + ), family: z .enum(["daily", "weekly", "one_shot", "none"]) .describe("Cadence family the window's slots were minted under."), @@ -1251,8 +1233,7 @@ export const medicationListLayoutPutBody = medicationListLayoutSchema export const medicationListLayoutResult = medicationListLayoutSchema .extend({ - updatedAt: z.iso - .datetime({ offset: true }) + updatedAt: isoDateTime() .optional() .describe( "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque.", @@ -1404,13 +1385,11 @@ export const medicationIntakeImportJobStatusResponse = z .string() .nullable() .describe("Set only when `status` is `failed`; null otherwise."), - createdAt: z.iso.datetime({ offset: true }), - startedAt: z.iso - .datetime({ offset: true }) + createdAt: isoDateTime(), + startedAt: isoDateTime() .nullable() .describe("When the worker picked the job up; null while still queued."), - completedAt: z.iso - .datetime({ offset: true }) + completedAt: isoDateTime() .nullable() .describe("When the job reached `done` or `failed`; null while active."), }) @@ -1539,19 +1518,16 @@ export const todayIntakeEntry = z .object({ id: z.string().describe("Intake-event id."), medicationId: z.string(), - scheduledAt: z.iso - .datetime({ offset: true }) - .describe( - "The slot the dose belongs to — the event's `scheduledFor`, renamed on this wire only.", - ), - takenAt: z.iso.datetime({ offset: true }).nullable(), + scheduledAt: isoDateTime().describe( + "The slot the dose belongs to — the event's `scheduledFor`, renamed on this wire only.", + ), + takenAt: isoDateTime().nullable(), status: z .enum(["skipped", "taken", "missed", "pending", "snoozed"]) .describe( "Resolved in that precedence order. `missed` is TERMINAL — the nightly cron closed a never-acted slot — and is not a `pending` that can still be answered. `snoozed` is not a property of this event at all: it reflects the MEDICATION's deferral stamp still lying in the future, so every pending dose of that medication reads snoozed at once.", ), - snoozedUntil: z.iso - .datetime({ offset: true }) + snoozedUntil: isoDateTime() .nullable() .describe( "The medication's deferral stamp, repeated on each of its events. Null when the medication is not deferred.", @@ -1585,7 +1561,7 @@ export const complianceDayBucket = z export const glp1DoseChange = z .object({ id: z.string(), - effectiveFrom: z.iso.datetime({ offset: true }), + effectiveFrom: isoDateTime(), doseValue: z.number(), doseUnit: z.string(), note: z @@ -1642,7 +1618,7 @@ export const glp1DetailResponse = z recentIntakes: z .array( z.object({ - takenAt: z.iso.datetime({ offset: true }).nullable(), + takenAt: isoDateTime().nullable(), injectionSite: z .string() .nullable() @@ -1670,7 +1646,7 @@ export const glp1InventoryEvent = z medicationId: z.string(), delta: z.number().int().describe("Positive added, negative consumed."), reason: z.string(), - occurredAt: z.iso.datetime({ offset: true }), + occurredAt: isoDateTime(), }) .meta({ id: "Glp1InventoryEvent", @@ -1776,7 +1752,7 @@ export const medicationSideEffect = z "The RECORD owner, which is not the actor on a delegated write.", ), medicationId: z.string(), - occurredAt: z.iso.datetime({ offset: true }), + occurredAt: isoDateTime(), category: z .string() .describe( @@ -1790,7 +1766,7 @@ export const medicationSideEffect = z .describe( "The free-text note, decrypted on read. Stored encrypted; the ciphertext column is stripped before the row leaves the server.", ), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }) .meta({ id: "MedicationSideEffect", diff --git a/src/lib/openapi/routes/meta.ts b/src/lib/openapi/routes/meta.ts index 088cb1032..6f9430ccc 100644 --- a/src/lib/openapi/routes/meta.ts +++ b/src/lib/openapi/routes/meta.ts @@ -9,6 +9,7 @@ import { z } from "zod/v4"; import type { ZodOpenApiObject } from "zod-openapi"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // v1.10.2 — live capability / discovery response. Every list is sourced // server-side from the canonical registry it documents, so the wire shape @@ -221,8 +222,7 @@ const versionResponse = z .describe( "Short Git SHA baked at image build time. Null for a local dev run, which is how a client tells a released image from one.", ), - builtAt: z.iso - .datetime({ offset: true }) + builtAt: isoDateTime() .nullable() .describe("ISO-8601 build timestamp. Null for a local dev run."), license: z.string(), @@ -297,13 +297,11 @@ const updateCheckResponse = z .nullable() .optional() .describe("Release page to deep-link to. Only on `newer_available`."), - published_at: z.iso - .datetime({ offset: true }) + published_at: isoDateTime() .nullable() .optional() .describe("When that release was published. Only on `newer_available`."), - checked_at: z.iso - .datetime({ offset: true }) + checked_at: isoDateTime() .optional() .describe("When the check ran. Absent when `unknown`."), reason: z diff --git a/src/lib/openapi/routes/mood.ts b/src/lib/openapi/routes/mood.ts index bcae9fb38..38ffde670 100644 --- a/src/lib/openapi/routes/mood.ts +++ b/src/lib/openapi/routes/mood.ts @@ -62,6 +62,7 @@ import { updatedAtTokenField, } from "./shared"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Request schemas — annotated for spec emission ──────────────────── // @@ -303,7 +304,7 @@ export const moodContextWire = z .max(5) .nullish() .describe("-5 (clearly bad) to +5 (clearly good)."), - eventAt: z.iso.datetime({ offset: true }).nullish(), + eventAt: isoDateTime().nullish(), note: z .string() .max(500) @@ -374,9 +375,7 @@ const bulkMoodEntry = z .describe( "The day's context. Absent leaves a stored context untouched on a re-post; an empty one removes it.", ), - moodLoggedAt: z.iso - .datetime({ offset: true }) - .describe("ISO instant the entry was logged."), + moodLoggedAt: isoDateTime().describe("ISO instant the entry was logged."), source: moodSourceEnum.optional().describe("Defaults to MANUAL."), externalId: z .string() @@ -515,16 +514,16 @@ const moodEntryDto = z .string() .nullable() .describe("The sync client's source-stable dedup id, or null."), - moodLoggedAt: z.iso.datetime({ offset: true }), + moodLoggedAt: isoDateTime(), tz: z .string() .nullable() .describe( `IANA zone the \`date\` label is anchored to. Null on legacy rows (read as ${DEFAULT_TIMEZONE}).`, ), - syncedAt: z.iso.datetime({ offset: true }), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + syncedAt: isoDateTime(), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), syncVersion: z .number() .int() @@ -712,7 +711,7 @@ const moodPrognosisPresent = z.object({ "Complete days the fit used. Display it WHEREVER `predicted` is displayed: a value built from 30 days and one built from 300 look identical otherwise.", ), modelVersion: z.string(), - computedAt: z.iso.datetime({ offset: true }), + computedAt: isoDateTime(), ageDays: z .number() .int() diff --git a/src/lib/openapi/routes/notifications.ts b/src/lib/openapi/routes/notifications.ts index da0d64aad..45ce29dbc 100644 --- a/src/lib/openapi/routes/notifications.ts +++ b/src/lib/openapi/routes/notifications.ts @@ -25,6 +25,7 @@ import { reEnableChannelSchema, } from "@/lib/validations/notifications"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const channelTypeEnum = z .enum(CHANNEL_TYPES) @@ -108,11 +109,10 @@ const channelDeliveryStatus = z "Why the dispatcher auto-disabled the channel; null when it did not. This is the field that separates `auto_disabled` from `manually_disabled`.", ), consecutiveFailures: z.number().int(), - lastSuccessAt: z.iso.datetime({ offset: true }).nullable(), - lastFailureAt: z.iso.datetime({ offset: true }).nullable(), + lastSuccessAt: isoDateTime().nullable(), + lastFailureAt: isoDateTime().nullable(), lastFailureReason: z.string().nullable(), - nextRetryAt: z.iso - .datetime({ offset: true }) + nextRetryAt: isoDateTime() .nullable() .describe("End of the current cooldown; null when not paused."), }) @@ -124,8 +124,7 @@ const channelDeliveryStatus = z const eventDeliveryStatus = z .object({ - lastDeliveredAt: z.iso - .datetime({ offset: true }) + lastDeliveredAt: isoDateTime() .nullable() .describe("Null when this category has never been delivered."), }) diff --git a/src/lib/openapi/routes/onboarding.ts b/src/lib/openapi/routes/onboarding.ts index d306cdc26..bbff1c01e 100644 --- a/src/lib/openapi/routes/onboarding.ts +++ b/src/lib/openapi/routes/onboarding.ts @@ -37,6 +37,7 @@ import { onboardingRestartSchema, } from "@/lib/validations/onboarding-needs"; import { dataEnvelope, errorEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; /* ── v1.39 (C1): the needs-based flow ─────────────────────────────────────── */ @@ -97,8 +98,7 @@ const onboardingFirstResultResource = z .describe( "What the task is about — a source key for a connection, an area key for a reading. Null for the medication task.", ), - completedAt: z.iso - .datetime({ offset: true }) + completedAt: isoDateTime() .nullable() .describe("When the offered task produced its result, or null."), }) @@ -112,8 +112,7 @@ export const onboardingStateResource = z .object({ steps: z.array(onboardingStepResource), needs: onboardingNeedsResource, - completedAt: z.iso - .datetime({ offset: true }) + completedAt: isoDateTime() .nullable() .describe( "When the needs flow was completed. Distinct from the account payload's `onboardingCompletedAt`, which stays the first-run redirect's gate: `POST /api/onboarding/restart` clears this one and leaves that one alone.", diff --git a/src/lib/openapi/routes/profile.ts b/src/lib/openapi/routes/profile.ts index de83b2e61..cf4e37f81 100644 --- a/src/lib/openapi/routes/profile.ts +++ b/src/lib/openapi/routes/profile.ts @@ -60,6 +60,7 @@ import { updatedAtTokenField, validationIssue, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // v1.18.0 — module enable/disable. The PATCH request is the REAL runtime // validator (`modulePrefsPatchSchema` — strict over the toggleable key @@ -412,8 +413,7 @@ const healthScoreConfigResponse = z .describe( "Per-user recipe version, monotonic and incremented on every accepted write. 0 while no selection has been written. A score record names the version that produced it, so a comparison can see the recipe move.", ), - changedAt: z.iso - .datetime({ offset: true }) + changedAt: isoDateTime() .nullable() .describe("When the recipe last changed; null while it never has."), updatedAt: updatedAtTokenField, @@ -542,7 +542,7 @@ const profileResponse = z username: z.string(), displayName: z.string().nullable().optional(), email: z.string().nullable(), - dateOfBirth: z.iso.datetime({ offset: true }).nullable(), + dateOfBirth: isoDateTime().nullable(), gender: z.enum(["MALE", "FEMALE", "OTHER"]).nullable(), heightCm: z.number().nullable(), locale: z.string().nullable(), @@ -590,7 +590,7 @@ const profileUpdateResponse = z username: z.string(), displayName: z.string().nullable().optional(), email: z.string().nullable(), - dateOfBirth: z.iso.datetime({ offset: true }).nullable(), + dateOfBirth: isoDateTime().nullable(), gender: z.enum(["MALE", "FEMALE", "OTHER"]).nullable(), heightCm: z.number().nullable(), locale: z.string().nullable(), @@ -905,8 +905,8 @@ const deviceInfo = z .describe( "Which push transports this device row can currently be reached on. Derived from which token columns are populated, not from a stored list.", ), - lastSeenAt: z.iso.datetime({ offset: true }), - createdAt: z.iso.datetime({ offset: true }), + lastSeenAt: isoDateTime(), + createdAt: isoDateTime(), isCurrent: z .boolean() .describe( @@ -1526,7 +1526,7 @@ export const profilePaths: NonNullable = { "image/png", "image/webp", ]), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }), "UploadAvatarResponse", ), diff --git a/src/lib/openapi/routes/record-settings.ts b/src/lib/openapi/routes/record-settings.ts index fc7dde72d..9f61eddf7 100644 --- a/src/lib/openapi/routes/record-settings.ts +++ b/src/lib/openapi/routes/record-settings.ts @@ -32,6 +32,7 @@ import { recordRefusal, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; const thresholdMetricEnum = z .enum(Object.keys(METRIC_BOUNDS) as [string, ...string[]]) @@ -95,8 +96,8 @@ const recordSettingsIntegrationsResponse = z "disconnected", "parked", ]), - lastSuccessAt: z.iso.datetime({ offset: true }).nullable(), - lastAttemptAt: z.iso.datetime({ offset: true }).nullable(), + lastSuccessAt: isoDateTime().nullable(), + lastAttemptAt: isoDateTime().nullable(), health: connectionHealth .nullable() .describe( diff --git a/src/lib/openapi/routes/shared.ts b/src/lib/openapi/routes/shared.ts index 3a9227705..c8722ac9c 100644 --- a/src/lib/openapi/routes/shared.ts +++ b/src/lib/openapi/routes/shared.ts @@ -17,6 +17,7 @@ import { import { loginPasswordSchema as loginPasswordSchemaBase } from "@/lib/validations/auth"; import { coachPrefsSchema as coachPrefsSchemaBase } from "@/lib/validations/coach-prefs"; import { createBatchWorkoutSchema as createBatchWorkoutSchemaBase } from "@/lib/validations/workout"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Common envelopes — every HealthLog API response wraps payload in @@ -197,8 +198,7 @@ export const createBatchWorkoutSchema = createBatchWorkoutSchemaBase.meta({ * OpenAPI layer: the runtime strips it pre-Zod (`takeBaseToken`), so the * runtime schema alone would under-document the wire. */ -export const baseUpdatedAtField = z.iso - .datetime({ offset: true }) +export const baseUpdatedAtField = isoDateTime() .optional() .describe( "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored row still carrying this exact value — a stale token fails with 409 and changes nothing. A present-but-unparseable value fails with 422 and `meta.errorCode` = `invalid_base_updated_at` — note that this is NOT the unconditional write: sending `null` or a malformed string is rejected rather than treated as an omitted token. Treat as opaque: only ever echo a server-returned value, never parse or synthesise it.", @@ -209,8 +209,7 @@ export const baseUpdatedAtField = z.iso * Every guarded GET / write response carries the stored row's `updatedAt`; the * client echoes it back as `baseUpdatedAt` on the next write. */ -export const updatedAtTokenField = z.iso - .datetime({ offset: true }) +export const updatedAtTokenField = isoDateTime() .optional() .describe( "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as `baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or synthesise it.", diff --git a/src/lib/openapi/routes/sync.ts b/src/lib/openapi/routes/sync.ts index ce59001c3..0a690d5f8 100644 --- a/src/lib/openapi/routes/sync.ts +++ b/src/lib/openapi/routes/sync.ts @@ -11,6 +11,7 @@ import { measurementResource } from "./measurements"; import { moodContextWire } from "./mood"; import { cycleDayLogDto, menstrualCycleDto } from "./cycle"; import { dataEnvelope, stdResponses } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Sync (v1.7.0 offline / server-optional) ───────────────────────── @@ -18,20 +19,20 @@ const syncStateResponse = z .object({ userId: z.string(), timezone: z.string(), - lastSyncedAt: z.iso.datetime({ offset: true }).nullable(), - serverNow: z.iso.datetime({ offset: true }), + lastSyncedAt: isoDateTime().nullable(), + serverNow: isoDateTime(), measurements: z.object({ - lastUpdatedAt: z.iso.datetime({ offset: true }).nullable(), + lastUpdatedAt: isoDateTime().nullable(), liveCount: z.number().int().nonnegative(), tombstonedCount: z.number().int().nonnegative(), }), mood: z.object({ - lastUpdatedAt: z.iso.datetime({ offset: true }).nullable(), + lastUpdatedAt: isoDateTime().nullable(), liveCount: z.number().int().nonnegative(), tombstonedCount: z.number().int().nonnegative(), }), intakes: z.object({ - lastUpdatedAt: z.iso.datetime({ offset: true }).nullable(), + lastUpdatedAt: isoDateTime().nullable(), liveCount: z.number().int().nonnegative(), tombstonedCount: z.number().int().nonnegative(), }), @@ -73,7 +74,7 @@ const syncMeasurementUpsert = measurementResource .describe( "LWW reconciliation counter; echo to keep the mirror monotonic.", ), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .meta({ id: "SyncMeasurementUpsert" }); @@ -85,8 +86,8 @@ const syncMeasurementTombstone = z .nullable() .describe("The identity key the client dedups on for measurements."), syncVersion: z.number().int().positive(), - deletedAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + deletedAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "SyncMeasurementTombstone", @@ -124,14 +125,14 @@ const syncMoodUpsert = z ), tags: z.string().nullable().describe("JSON array of tag keys, or null."), note: z.string().nullable(), - moodLoggedAt: z.iso.datetime({ offset: true }), + moodLoggedAt: isoDateTime(), source: z.string(), syncVersion: z .number() .int() .nonnegative() .describe("LWW reconciliation counter; mood is last-writer-wins by it."), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .meta({ id: "SyncMoodUpsert" }); @@ -141,8 +142,8 @@ const syncMoodTombstone = z .string() .describe("Server id — the identity key the client dedups on for mood."), syncVersion: z.number().int().nonnegative(), - deletedAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + deletedAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "SyncMoodTombstone", @@ -154,8 +155,8 @@ const syncIntakeUpsert = z .object({ id: z.string(), medicationId: z.string(), - scheduledFor: z.iso.datetime({ offset: true }), - takenAt: z.iso.datetime({ offset: true }).nullable(), + scheduledFor: isoDateTime(), + takenAt: isoDateTime().nullable(), skipped: z.boolean(), source: z.string(), autoRecorded: z @@ -170,7 +171,7 @@ const syncIntakeUpsert = z .describe( "Reconciliation counter. An intake is immutable; a correction is a tombstone + re-insert.", ), - updatedAt: z.iso.datetime({ offset: true }), + updatedAt: isoDateTime(), }) .meta({ id: "SyncIntakeUpsert" }); @@ -182,8 +183,8 @@ const syncIntakeTombstone = z "Server id — the identity key the client dedups on for intakes.", ), syncVersion: z.number().int().nonnegative(), - deletedAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + deletedAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "SyncIntakeTombstone", @@ -199,8 +200,8 @@ const syncCycleDayTombstone = z .nullable() .describe("The client's own key for the day log, when it set one."), syncVersion: z.number().int().nonnegative(), - deletedAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + deletedAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "SyncCycleDayTombstone", @@ -212,8 +213,8 @@ const syncCycleTombstone = z .object({ id: z.string(), syncVersion: z.number().int().nonnegative(), - deletedAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + deletedAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "SyncCycleTombstone", @@ -251,7 +252,7 @@ const syncChangesQuery = z */ export const syncChangesResponse = z .object({ - serverNow: z.iso.datetime({ offset: true }), + serverNow: isoDateTime(), cursor: z .string() .nullable() diff --git a/src/lib/openapi/routes/vaccinations.ts b/src/lib/openapi/routes/vaccinations.ts index 1cb6c1730..bbc34d674 100644 --- a/src/lib/openapi/routes/vaccinations.ts +++ b/src/lib/openapi/routes/vaccinations.ts @@ -44,6 +44,7 @@ import { recordWriteRateLimitResponse, stdResponses, } from "./shared"; +import { isoDateTime } from "@/lib/validations/datetime"; // Zod 4's `.meta()` returns a NEW instance carrying the id rather than // annotating in place, so each annotated schema is bound to a const and the @@ -164,8 +165,8 @@ const customVaccine = z ), typicalSeriesDoses: z.number().int().nullable(), boosterIntervalMonths: z.number().int().nullable(), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "CustomVaccine", diff --git a/src/lib/openapi/routes/workouts.ts b/src/lib/openapi/routes/workouts.ts index e3c4678a1..a66e14b6d 100644 --- a/src/lib/openapi/routes/workouts.ts +++ b/src/lib/openapi/routes/workouts.ts @@ -20,6 +20,7 @@ import { stdResponses, } from "./shared"; import { aiCapabilityState } from "./profile"; +import { isoDateTime } from "@/lib/validations/datetime"; // v1.4.25 W16b — typed workout batch ingest response envelope. Mirrors // the measurements batch shape but reports the `workouts` count rather @@ -94,8 +95,8 @@ const workoutListEntry = z // session can arrive without a sport type or timing. Declaring them // non-null was a strict-decoder bomb for the Swift client. sportType: z.string().nullable(), - startedAt: z.iso.datetime({ offset: true }).nullable(), - endedAt: z.iso.datetime({ offset: true }).nullable(), + startedAt: isoDateTime().nullable(), + endedAt: isoDateTime().nullable(), durationSec: z.number().int().nonnegative().nullable(), distanceM: z.number().nullable(), activeEnergyKcal: z.number().nullable(), @@ -198,7 +199,7 @@ const workoutActivityInsight = z .object({ /** Plain text. No markup of any kind — clients render it as text. */ paragraph: z.string(), - generatedAt: z.iso.datetime({ offset: true }), + generatedAt: isoDateTime(), }) .meta({ id: "WorkoutActivityInsight" }); @@ -206,8 +207,8 @@ const workoutDetailResponse = z .object({ id: z.string(), sportType: z.string(), - startedAt: z.iso.datetime({ offset: true }), - endedAt: z.iso.datetime({ offset: true }), + startedAt: isoDateTime(), + endedAt: isoDateTime(), /** * The local calendar day the session belongs to, `YYYY-MM-DD`, resolved * server-side from `startedAt` in the user's timezone. Clients address @@ -280,8 +281,8 @@ export const workoutPaths: NonNullable = { query: z.object({ limit: z.coerce.number().int().min(1).max(200).optional(), offset: z.coerce.number().int().min(0).optional(), - since: z.iso.datetime({ offset: true }).optional(), - until: z.iso.datetime({ offset: true }).optional(), + since: isoDateTime().optional(), + until: isoDateTime().optional(), sportType: z.string().optional(), }), }, diff --git a/src/lib/symptoms/shared.ts b/src/lib/symptoms/shared.ts index 60e9b17b7..5738d3467 100644 --- a/src/lib/symptoms/shared.ts +++ b/src/lib/symptoms/shared.ts @@ -2,6 +2,7 @@ import { z } from "zod/v4"; import { CUSTOM_SYMPTOM_ICON_ALLOWLIST } from "@/lib/cycle/custom-symptoms-shared"; import { validateEntryInstant } from "@/lib/validations/entry-instant"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Person-defined symptoms (v1.40): client-safe constants, wire types and the @@ -108,8 +109,7 @@ const intensitySchema = z .max(SYMPTOM_INTENSITY_MAX); const occurredAtSchema = validateEntryInstant( - z.iso - .datetime({ offset: true }) + isoDateTime() .transform((s) => new Date(s)) .pipe(z.date()), ); @@ -141,8 +141,8 @@ export const symptomEventUpdateSchema = z ); export const symptomEventListQuerySchema = z.object({ - from: z.iso.datetime({ offset: true }).optional(), - to: z.iso.datetime({ offset: true }).optional(), + from: isoDateTime().optional(), + to: isoDateTime().optional(), definitionId: z.string().min(1).max(40).optional(), episodeId: z.string().min(1).max(40).optional(), limit: z.coerce diff --git a/src/lib/validations/__tests__/datetime.test.ts b/src/lib/validations/__tests__/datetime.test.ts new file mode 100644 index 000000000..0271ffe88 --- /dev/null +++ b/src/lib/validations/__tests__/datetime.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + DATETIME_NO_SECONDS_MESSAGE, + isoDateTime, + registerDatetimeNoSecondsObserver, +} from "@/lib/validations/datetime"; + +describe("isoDateTime", () => { + it("accepts an instant with seconds, with or without fraction and offset", () => { + const s = isoDateTime(); + for (const v of [ + "2026-10-15T06:15:00Z", + "2026-10-15T06:15:00.000Z", + "2026-10-15T06:15:00.123456Z", + "2026-10-15T06:15:00+02:00", + ]) { + expect(s.safeParse(v).success, v).toBe(true); + } + }); + + it("refuses a minute-precision instant and says seconds are missing", () => { + for (const v of ["2026-10-15T06:15Z", "2026-10-15T06:15+02:00"]) { + const r = isoDateTime().safeParse(v); + expect(r.success, v).toBe(false); + expect(r.error?.issues[0].message).toBe(DATETIME_NO_SECONDS_MESSAGE); + } + }); + + it("keeps the caller's message for every other refusal", () => { + const s = isoDateTime({ offset: false, message: "takenAt is bad" }); + expect(s.safeParse("yesterday").error?.issues[0].message).toBe( + "takenAt is bad", + ); + expect(s.safeParse("2026-10-15T06:15Z").error?.issues[0].message).toBe( + DATETIME_NO_SECONDS_MESSAGE, + ); + }); + + it("offset: false accepts only Z and does not count an offset form", () => { + const s = isoDateTime({ offset: false }); + expect(s.safeParse("2026-10-15T06:15:00Z").success).toBe(true); + expect(s.safeParse("2026-10-15T06:15:00+02:00").success).toBe(false); + expect( + s.safeParse("2026-10-15T06:15+02:00").error?.issues[0].message, + ).not.toBe(DATETIME_NO_SECONDS_MESSAGE); + }); + + it("tells the observer about minute-precision refusals only", () => { + const seen = vi.fn(); + registerDatetimeNoSecondsObserver(seen); + const s = isoDateTime(); + s.safeParse("2026-10-15T06:15Z"); + s.safeParse("not a date"); + s.safeParse("2026-10-15T06:15:00Z"); + expect(seen).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/lib/validations/account-sharing.ts b/src/lib/validations/account-sharing.ts index 5e47ad567..6395ac832 100644 --- a/src/lib/validations/account-sharing.ts +++ b/src/lib/validations/account-sharing.ts @@ -13,6 +13,7 @@ import { z } from "zod/v4"; import { SHARE_DOMAINS } from "@/lib/sharing/scope"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * How long a value naming an account may be. @@ -41,7 +42,7 @@ export const inviteGrantSchema = z * ends it — the common household case, where an expiry the owner has to * remember to renew is worse than one that does not exist. */ - expiresAt: z.iso.datetime({ offset: true }).nullable().optional(), + expiresAt: isoDateTime().nullable().optional(), /** * What the invitation offers, and the ONLY place the level is defaulted. * diff --git a/src/lib/validations/allergy.ts b/src/lib/validations/allergy.ts index 8f336ed20..956f7585c 100644 --- a/src/lib/validations/allergy.ts +++ b/src/lib/validations/allergy.ts @@ -14,17 +14,19 @@ */ import { z } from "zod/v4"; import { isPlausibleEntryInstant } from "@/lib/validations/entry-instant"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Plausible-instant bound shared with the measurement / illness paths: no * future instant beyond the 5-min skew, no instant before 1900. Stays a * string on the wire (iOS reads it back verbatim). */ -const boundedInstant = z.iso - .datetime({ offset: true }) - .refine((s) => isPlausibleEntryInstant(new Date(s)), { +const boundedInstant = isoDateTime().refine( + (s) => isPlausibleEntryInstant(new Date(s)), + { message: "must be a plausible instant (not future, not pre-1900)", - }); + }, +); /* ── enums (mirror the Prisma enums) ─────────────────────────────── */ diff --git a/src/lib/validations/clinician-share-link.ts b/src/lib/validations/clinician-share-link.ts index cbe22ff72..c04883074 100644 --- a/src/lib/validations/clinician-share-link.ts +++ b/src/lib/validations/clinician-share-link.ts @@ -22,6 +22,7 @@ import { z } from "zod/v4"; import { reportSelectionSchema } from "@/lib/report-selection/selection"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * The one leaf a share link may never carry. The share view has never @@ -56,8 +57,8 @@ export const SHARE_LINK_MAX_DOCUMENTS = 50; export const createShareLinkSchema = z .object({ label: z.string().trim().min(1).max(120), - rangeStart: z.iso.datetime({ offset: true }), - rangeEnd: z.iso.datetime({ offset: true }).nullable().optional(), + rangeStart: isoDateTime(), + rangeEnd: isoDateTime().nullable().optional(), selection: reportSelectionSchema.optional(), documentIds: z .array(z.string().trim().min(1).max(64)) @@ -72,8 +73,7 @@ export const createShareLinkSchema = z * server-side so a document link can never widen into a record. */ documentOnly: z.boolean().optional(), - expiresAt: z.iso - .datetime({ offset: true }) + expiresAt: isoDateTime() .refine((v) => new Date(v).getTime() > Date.now(), { message: "expiresAt must be in the future", }) diff --git a/src/lib/validations/coach-plan.ts b/src/lib/validations/coach-plan.ts index 75b37f4a3..a9c65a8b2 100644 --- a/src/lib/validations/coach-plan.ts +++ b/src/lib/validations/coach-plan.ts @@ -16,6 +16,7 @@ import { z } from "zod/v4"; import { COACH_PLAN_STATUSES } from "@/lib/ai/coach/plans"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Optional `?scope=` filter on the list endpoint — a named status group so @@ -48,7 +49,7 @@ export const coachPlansListQuerySchema = z export const coachPlanPatchSchema = z .object({ status: z.enum(COACH_PLAN_STATUSES).optional(), - reviewDate: z.iso.datetime({ offset: true }).nullable().optional(), + reviewDate: isoDateTime().nullable().optional(), }) .strict() .refine( diff --git a/src/lib/validations/consent.ts b/src/lib/validations/consent.ts index ffd154cb4..aac483e5e 100644 --- a/src/lib/validations/consent.ts +++ b/src/lib/validations/consent.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.4.40 SB-10 — AI consent receipts (App-Store Guideline 5.1.2(i) + @@ -51,7 +52,7 @@ export const consentPostBody = z.object({ .refine((value) => Buffer.byteLength(value, "utf8") <= ARTEFACT_MAX_BYTES, { message: "artefact exceeds 64 KB cap (UTF-8 byte length)", }), - signedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + signedAt: isoDateTime().transform((s) => new Date(s)), }); export type ConsentPostBody = z.infer; diff --git a/src/lib/validations/custom-metrics.ts b/src/lib/validations/custom-metrics.ts index 6b4367c24..5670ec9ff 100644 --- a/src/lib/validations/custom-metrics.ts +++ b/src/lib/validations/custom-metrics.ts @@ -17,6 +17,7 @@ import { z } from "zod/v4"; import { validateEntryInstant } from "@/lib/validations/entry-instant"; +import { isoDateTime } from "@/lib/validations/datetime"; /** Trimmed, bounded free-text field with a min-length guard. */ const requiredText = (max: number) => z.string().trim().min(1).max(max); @@ -98,10 +99,10 @@ const ENTRY_MAX_AGE_MS = 50 * 365.25 * 24 * 60 * 60 * 1000; /** Shared `measuredAt` field: ISO string → Date, plausibility-bounded. */ const measuredAtField = validateEntryInstant( - z - .string() - .datetime({ message: "measuredAt must be an ISO 8601 datetime" }) - .transform((s) => new Date(s)), + isoDateTime({ + offset: false, + message: "measuredAt must be an ISO 8601 datetime", + }).transform((s) => new Date(s)), { maxAgeMs: ENTRY_MAX_AGE_MS, pastMessage: "Measurement date is too far in the past", diff --git a/src/lib/validations/cycle.ts b/src/lib/validations/cycle.ts index 86fbd8cab..e01d0c842 100644 --- a/src/lib/validations/cycle.ts +++ b/src/lib/validations/cycle.ts @@ -12,6 +12,7 @@ import { z } from "zod/v4"; import { isPlausibleEntryInstant } from "@/lib/validations/entry-instant"; import { assertStableExternalId } from "@/lib/validations/external-id"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.17 W1b — `loggedAt` instant on the cycle wire stays a string (the iOS @@ -21,11 +22,12 @@ import { assertStableExternalId } from "@/lib/validations/external-id"; * the measurement / mood / medication-intake paths use: no future instant * beyond the 5-min skew, no instant before 1900. */ -const boundedLoggedAt = z.iso - .datetime({ offset: true }) - .refine((s) => isPlausibleEntryInstant(new Date(s)), { +const boundedLoggedAt = isoDateTime().refine( + (s) => isPlausibleEntryInstant(new Date(s)), + { message: "loggedAt must be a plausible instant (not future, not pre-1900)", - }); + }, +); /* ── enums (mirror the Prisma enums) ─────────────────────────────── */ diff --git a/src/lib/validations/datetime.ts b/src/lib/validations/datetime.ts new file mode 100644 index 000000000..ef9a752dc --- /dev/null +++ b/src/lib/validations/datetime.ts @@ -0,0 +1,89 @@ +/** + * The one ISO 8601 instant validator every schema uses. + * + * Since v1.43 an instant must carry seconds: `2026-10-15T06:15:00Z`, + * `2026-10-15T06:15:00.000Z` and `2026-10-15T06:15:00+02:00` pass, + * `2026-10-15T06:15Z` is refused. zod 4.5 made that the behaviour of + * `z.iso.datetime()`; routing every call site through this helper keeps the + * rule in one place, gives the refusal a message that says what is missing, + * and counts each refusal so a client that still sends minute-precision + * instants shows up in the logs before its users do. + * + * `src/__tests__/datetime-helper-guard.test.ts` forbids a direct + * `z.iso.datetime(` or `z.string().datetime(` anywhere else under `src/`. + * + * Client-safe: the counter goes through an observer the server logging module + * registers on import (the same pattern as `registerSourcePriorityParseObserver`), + * so a form that validates with these schemas never pulls `node:async_hooks` + * into the browser bundle. + */ +import { z } from "zod/v4"; + +/** Wide-event meta key: how many instants this request refused for lacking seconds. */ +export const DATETIME_NO_SECONDS_COUNTER = "validation.datetime.no_seconds"; + +/** Message for an instant that would be valid if it carried seconds. */ +export const DATETIME_NO_SECONDS_MESSAGE = + "Invalid ISO datetime: seconds are required (YYYY-MM-DDTHH:mm:ssZ)"; + +// Date, hours and minutes, then an optional zone and nothing else: the shape a +// minute-precision encoder produces. Only this shape counts, so the counter +// measures exactly the clients the seconds rule turned away and not every +// malformed string. A field that requires `Z` does not count an offset form, +// which it would refuse with seconds too. +const MINUTE_PRECISION_OFFSET = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$/; +const MINUTE_PRECISION_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/; + +type NoSecondsObserver = () => void; +let noSecondsObserver: NoSecondsObserver = () => {}; + +/** + * Server-side opt-in for the refusal counter. Called once from + * `@/lib/logging/context`; the browser keeps the no-op default. + */ +export function registerDatetimeNoSecondsObserver( + observer: NoSecondsObserver, +): void { + noSecondsObserver = observer; +} + +export interface IsoDateTimeOptions { + /** + * Accept a numeric UTC offset (`+02:00`) besides `Z`. Default `true`; + * `false` keeps the handful of fields that have always required `Z`. + */ + offset?: boolean; + /** Message for every other refusal; zod's default when omitted. */ + message?: string; +} + +/** + * An ISO 8601 instant with seconds. Use it for every timestamp a schema + * accepts or publishes; chain `.optional()`, `.nullable()`, `.describe()` and + * `.meta()` as on any zod string. + */ +export function isoDateTime(options: IsoDateTimeOptions = {}) { + const { offset = true, message } = options; + const minutePrecision = offset + ? MINUTE_PRECISION_OFFSET + : MINUTE_PRECISION_UTC; + return ( + z.iso + .datetime({ + offset, + // zod builds issue messages lazily, when a caller first reads them, + // so this callback only words the refusal; it cannot count it. + error: (issue) => + typeof issue.input === "string" && minutePrecision.test(issue.input) + ? DATETIME_NO_SECONDS_MESSAGE + : message, + }) + // The count runs here instead: a refinement runs on every parse of a + // string, even after the format check failed, and adds no issue. + .refine((value) => { + if (minutePrecision.test(value)) noSecondsObserver(); + return true; + }) + ); +} diff --git a/src/lib/validations/encounters.ts b/src/lib/validations/encounters.ts index 5bd8f3cb3..ddf2deca1 100644 --- a/src/lib/validations/encounters.ts +++ b/src/lib/validations/encounters.ts @@ -18,6 +18,7 @@ * park a row a century out. */ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** Ten years out. Long enough for any real appointment, short enough that a * mistyped year lands as a 422 rather than as a row nobody sees again. */ @@ -30,7 +31,7 @@ const MAX_FUTURE_MS = 10 * 365 * 24 * 60 * 60 * 1000; * five-minute clock skew. That bound is right for a measurement, which can only * describe something that already happened, and wrong here. */ -const visitInstant = z.iso.datetime({ offset: true }).refine( +const visitInstant = isoDateTime().refine( (value) => { const at = new Date(value).getTime(); if (Number.isNaN(at)) return false; @@ -150,8 +151,8 @@ export type EncounterUpdate = z.infer; */ export const encounterListQuerySchema = z .object({ - from: z.iso.datetime({ offset: true }).optional(), - to: z.iso.datetime({ offset: true }).optional(), + from: isoDateTime().optional(), + to: isoDateTime().optional(), status: encounterStatusEnum.optional(), practitionerId: id.optional(), /** diff --git a/src/lib/validations/health-profile-facts.ts b/src/lib/validations/health-profile-facts.ts index b8bbf614c..9e7a56100 100644 --- a/src/lib/validations/health-profile-facts.ts +++ b/src/lib/validations/health-profile-facts.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; export const HEALTH_PROFILE_AI_SECTIONS = [ "ABOUT_ME", @@ -116,11 +117,11 @@ export const healthProfileFactDtoSchema = z.object({ kind: healthProfileFactKindSchema, value: z.string().nullable(), unreadable: z.boolean(), - validFrom: z.iso.datetime({ offset: true }), - validUntil: z.iso.datetime({ offset: true }).nullable(), + validFrom: isoDateTime(), + validUntil: isoDateTime().nullable(), provenance: z.enum(["USER_REPORTED", "USER_CORRECTION"]), supersededByRevisionId: z.string().nullable(), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }); export const healthProfileFactsResponseSchema = z.object({ diff --git a/src/lib/validations/health-record-export.ts b/src/lib/validations/health-record-export.ts index 3b0ffea17..2a031ed69 100644 --- a/src/lib/validations/health-record-export.ts +++ b/src/lib/validations/health-record-export.ts @@ -22,14 +22,15 @@ import { z } from "zod/v4"; import { locales } from "@/lib/i18n/config"; import { reportSelectionSchema } from "@/lib/report-selection/selection"; +import { isoDateTime } from "@/lib/validations/datetime"; export const exportSelectionSchema = z .object({ format: z.enum(["pdf", "fhir", "package"]), range: z .object({ - startDate: z.iso.datetime({ offset: true }).optional(), - endDate: z.iso.datetime({ offset: true }).optional(), + startDate: isoDateTime().optional(), + endDate: isoDateTime().optional(), days: z.number().int().min(1).max(365).optional(), }) .strict() diff --git a/src/lib/validations/illness.ts b/src/lib/validations/illness.ts index afc5e5a83..49e7e7a48 100644 --- a/src/lib/validations/illness.ts +++ b/src/lib/validations/illness.ts @@ -16,17 +16,19 @@ import { z } from "zod/v4"; import { isPlausibleEntryInstant } from "@/lib/validations/entry-instant"; import { lateralityEnum } from "@/lib/validations/encounters"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Plausible-instant bound shared with the measurement / mood / cycle * paths: no future instant beyond the 5-min skew, no instant before 1900. * Stays a string on the wire (iOS reads it back verbatim). */ -const boundedInstant = z.iso - .datetime({ offset: true }) - .refine((s) => isPlausibleEntryInstant(new Date(s)), { +const boundedInstant = isoDateTime().refine( + (s) => isPlausibleEntryInstant(new Date(s)), + { message: "must be a plausible instant (not future, not pre-1900)", - }); + }, +); const dateString = z .string() @@ -141,8 +143,8 @@ export const illnessEpisodeListQuerySchema = z.object({ * episodes around a visit and pages older or newer from there. */ offset: z.coerce.number().int().min(0).max(100_000).optional(), - from: z.iso.datetime({ offset: true }).optional(), - to: z.iso.datetime({ offset: true }).optional(), + from: isoDateTime().optional(), + to: isoDateTime().optional(), /** Onset order; newest first by default. */ sortDir: z.enum(["asc", "desc"]).optional(), /** Case-insensitive substring over the episode's label. */ diff --git a/src/lib/validations/labs-ocr.ts b/src/lib/validations/labs-ocr.ts index 38c7937dd..0da5ede72 100644 --- a/src/lib/validations/labs-ocr.ts +++ b/src/lib/validations/labs-ocr.ts @@ -22,6 +22,7 @@ import { z } from "zod/v4"; import type { WrittenOutcome } from "@/lib/outcome/written-outcome"; import { validateEntryInstant } from "@/lib/validations/entry-instant"; +import { isoDateTime } from "@/lib/validations/datetime"; /** Max rows a single scan / commit may carry — a dense panel is ~30 analytes. */ export const OCR_MAX_ROWS = 100; @@ -34,10 +35,10 @@ const LAB_TAKEN_AT_MAX_AGE_MS = 50 * 365.25 * 24 * 60 * 60 * 1000; /** Shared committed-row `takenAt`: ISO string → Date, plausibility-bounded. */ const takenAtField = validateEntryInstant( - z - .string() - .datetime({ message: "takenAt must be an ISO 8601 datetime" }) - .transform((s) => new Date(s)), + isoDateTime({ + offset: false, + message: "takenAt must be an ISO 8601 datetime", + }).transform((s) => new Date(s)), { maxAgeMs: LAB_TAKEN_AT_MAX_AGE_MS, pastMessage: "Sample date is too far in the past", diff --git a/src/lib/validations/labs.ts b/src/lib/validations/labs.ts index 834ef6721..d19a4bfe5 100644 --- a/src/lib/validations/labs.ts +++ b/src/lib/validations/labs.ts @@ -16,6 +16,7 @@ import { z } from "zod/v4"; import { SOURCE_REFERENCE_TEXT_MAX } from "@/lib/labs/parse-reference-range"; import { validateEntryInstant } from "@/lib/validations/entry-instant"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Far-past floor for a lab sample. A self-hoster legitimately backdates an @@ -26,10 +27,10 @@ const LAB_TAKEN_AT_MAX_AGE_MS = 50 * 365.25 * 24 * 60 * 60 * 1000; /** Shared `takenAt` field: ISO string → Date, plausibility-bounded. */ const takenAtField = validateEntryInstant( - z - .string() - .datetime({ message: "takenAt must be an ISO 8601 datetime" }) - .transform((s) => new Date(s)), + isoDateTime({ + offset: false, + message: "takenAt must be an ISO 8601 datetime", + }).transform((s) => new Date(s)), { maxAgeMs: LAB_TAKEN_AT_MAX_AGE_MS, pastMessage: "Sample date is too far in the past", @@ -289,14 +290,10 @@ export const listLabResultsSchema = z.object({ * picker can find one result in a long history without paging to it. */ q: z.string().trim().min(1).max(120).optional(), - from: z - .string() - .datetime() + from: isoDateTime({ offset: false }) .transform((s) => new Date(s)) .optional(), - to: z - .string() - .datetime() + to: isoDateTime({ offset: false }) .transform((s) => new Date(s)) .optional(), limit: z.coerce.number().int().min(1).max(500).default(100), diff --git a/src/lib/validations/life-circumstances.ts b/src/lib/validations/life-circumstances.ts index c7a6e5ece..6cf0fb8d0 100644 --- a/src/lib/validations/life-circumstances.ts +++ b/src/lib/validations/life-circumstances.ts @@ -18,6 +18,7 @@ import { SMOKING_STATUS_VALUES, isHealthProfileFactValue, } from "@/lib/validations/health-profile-facts"; +import { isoDateTime } from "@/lib/validations/datetime"; // ── Fact kinds and their values ────────────────────────────────────────────── @@ -189,7 +190,7 @@ export type LifeAiSection = (typeof LIFE_AI_SECTIONS)[number]; export const lifeAiSectionsPatchSchema = z .strictObject({ sections: z.partialRecord(z.enum(LIFE_AI_SECTIONS), z.boolean()), - baseUpdatedAt: z.iso.datetime({ offset: true }).optional(), + baseUpdatedAt: isoDateTime().optional(), }) .refine((value) => Object.keys(value.sections).length > 0, { path: ["sections"], @@ -212,7 +213,7 @@ const auditCItem = z.number().int().min(0).max(4); export const auditCRequestSchema = z .strictObject({ items: z.tuple([auditCItem, auditCItem, auditCItem]), - takenAt: z.iso.datetime({ offset: true }).optional(), + takenAt: isoDateTime().optional(), tz: z.string().min(1).max(64).optional(), locale: z.string().min(2).max(16).optional(), source: z.enum(["WEB", "IOS"]), @@ -343,7 +344,7 @@ export type LifeFactValue = export const removedFactSchema = z.object({ id: z.string(), kind: z.enum(STORED_FACT_KINDS), - removedAt: z.iso.datetime({ offset: true }), + removedAt: isoDateTime(), }); /** @@ -362,10 +363,10 @@ export const lifeFactRevisionSchema = z.object({ unreadable: z .boolean() .describe("The sealed value could not be opened on this host."), - validFrom: z.iso.datetime({ offset: true }), + validFrom: isoDateTime(), validUntil: z.null(), provenance: z.enum(["USER_REPORTED", "USER_CORRECTION"]), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }); /** diff --git a/src/lib/validations/managed-profiles.ts b/src/lib/validations/managed-profiles.ts index e582c7c32..5653f9856 100644 --- a/src/lib/validations/managed-profiles.ts +++ b/src/lib/validations/managed-profiles.ts @@ -13,6 +13,7 @@ import { z } from "zod/v4"; import { isValidTimezone } from "@/lib/tz/format"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * The record's sex, as `User.gender` already stores it. @@ -84,6 +85,6 @@ export const updateManagedProfileSchema = z export const inviteManagedProfileGuardianSchema = z .object({ identifier: z.string().trim().min(1).max(255), - expiresAt: z.iso.datetime({ offset: true }).nullable().optional(), + expiresAt: isoDateTime().nullable().optional(), }) .strict(); diff --git a/src/lib/validations/measurement-reminders.ts b/src/lib/validations/measurement-reminders.ts index a329afa46..6852527fb 100644 --- a/src/lib/validations/measurement-reminders.ts +++ b/src/lib/validations/measurement-reminders.ts @@ -13,6 +13,7 @@ * recomputes). */ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * The auto-resolve target types. Kept as an explicit allow-list rather @@ -147,10 +148,7 @@ const cadenceRefinement = ( * The first-due date: a calendar date (`YYYY-MM-DD`, read as that day in the * user's zone) or a date-time (the instant it names). */ -const anchorDateField = z.union([ - z.iso.date(), - z.iso.datetime({ offset: true }), -]); +const anchorDateField = z.union([z.iso.date(), isoDateTime()]); export const createMeasurementReminderSchema = z .object({ @@ -212,19 +210,19 @@ export const measurementReminderDto = z measurementType: measurementReminderTypeEnum.nullable(), intervalDays: z.number().int().nullable(), rrule: z.string().nullable(), - anchorDate: z.iso.datetime({ offset: true }).nullable(), - endsOn: z.iso.datetime({ offset: true }).nullable(), + anchorDate: isoDateTime().nullable(), + endsOn: isoDateTime().nullable(), origin: z.enum(["VORSORGE", "COACH"]), notifyHour: z.number().int(), location: z.string().nullable(), - nextDueAt: z.iso.datetime({ offset: true }).nullable(), - lastSatisfiedAt: z.iso.datetime({ offset: true }).nullable(), - snoozedUntil: z.iso.datetime({ offset: true }).nullable(), - lastSkippedAt: z.iso.datetime({ offset: true }).nullable(), + nextDueAt: isoDateTime().nullable(), + lastSatisfiedAt: isoDateTime().nullable(), + snoozedUntil: isoDateTime().nullable(), + lastSkippedAt: isoDateTime().nullable(), skipCount: z.number().int(), enabled: z.boolean(), - createdAt: z.iso.datetime({ offset: true }), - updatedAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), + updatedAt: isoDateTime(), }) .meta({ id: "MeasurementReminderDTO", @@ -310,7 +308,7 @@ export const measurementReminderEventDto = z .object({ id: z.string(), kind: z.enum(["SATISFIED", "SKIPPED"]), - occurredAt: z.iso.datetime({ offset: true }), + occurredAt: isoDateTime(), onTime: z.boolean(), source: z.enum([ "manual", @@ -321,7 +319,7 @@ export const measurementReminderEventDto = z "encounter", "skip", ]), - createdAt: z.iso.datetime({ offset: true }), + createdAt: isoDateTime(), }) .meta({ id: "MeasurementReminderEvent", diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 0659540e8..ac2b8da61 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -2,6 +2,7 @@ import { z } from "zod/v4"; import { EXTERNAL_SOURCE } from "@/lib/measurements/external-source"; import { validateEntryInstant } from "./entry-instant"; import { isCalendarDateKey } from "@/lib/tz/date-only"; +import { isoDateTime } from "@/lib/validations/datetime"; export const measurementTypeEnum = z.enum([ "WEIGHT", @@ -551,7 +552,7 @@ export const createMeasurementSchema = z // 1900. Closes the data-portability gap where a hand-rolled POST could // backdate or forward-date a reading arbitrarily. measuredAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ), notes: z.string().max(MEASUREMENT_NOTES_MAX_LENGTH).optional(), // v1.10.0 QA — validate against the client-writable subset, not the full @@ -611,7 +612,7 @@ export const updateMeasurementSchema = z.object({ // v1.17 W1b — same plausibility bound on the edit path; an edit cannot // forward-date a reading into the future nor before 1900. measuredAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ).optional(), notes: z .string() @@ -645,12 +646,10 @@ export const listPersonalRecordsSchema = z.object({ export const listMeasurementsSchema = z .object({ type: measurementTypeEnum.optional(), - from: z.iso - .datetime({ offset: true }) + from: isoDateTime() .transform((s) => new Date(s)) .optional(), - to: z.iso - .datetime({ offset: true }) + to: isoDateTime() .transform((s) => new Date(s)) .optional(), // v1.4.28 FB-D2 — when the chart sends an explicit from/to window the diff --git a/src/lib/validations/medication/base.ts b/src/lib/validations/medication/base.ts index 6590fad67..3680e749c 100644 --- a/src/lib/validations/medication/base.ts +++ b/src/lib/validations/medication/base.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; import { INJECTION_SITE_KEYS } from "@/lib/medications/injection-sites"; import { validateEntryInstant } from "@/lib/validations/entry-instant"; import { hhmmToMinutes } from "@/lib/medications/scheduling/hhmm"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.8.5 — the eight injection-site enum values, mirrored from the @@ -244,7 +245,7 @@ export const TAKEN_AT_MAX_AGE_MS = 5 * 365 * 24 * 60 * 60 * 1000; * 1900 floor. The 5-min clock-skew tolerance is the shared default. */ export const boundedTakenAtSchema = validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), { maxAgeMs: TAKEN_AT_MAX_AGE_MS, // Preserve the established wording so the iOS contract + tests stay stable. diff --git a/src/lib/validations/medication/intake.ts b/src/lib/validations/medication/intake.ts index 5f6eae4d2..e2d277103 100644 --- a/src/lib/validations/medication/intake.ts +++ b/src/lib/validations/medication/intake.ts @@ -2,6 +2,7 @@ import { z } from "zod/v4"; import { boundedTakenAtSchema, injectionSiteEnum } from "./base"; import { assertStableIdempotencyKey } from "@/lib/validations/external-id"; +import { isoDateTime } from "@/lib/validations/datetime"; export const intakeSchema = z .object({ @@ -11,8 +12,7 @@ export const intakeSchema = z .describe( "Server-narrowed from the URL path. The route layer overwrites whatever the body supplies before Zod parsing so a caller cannot log an intake against another medication.", ), - scheduledFor: z.iso - .datetime({ offset: true }) + scheduledFor: isoDateTime() .transform((s) => new Date(s)) .optional() .describe( @@ -60,8 +60,7 @@ export const intakeSchema = z * server validates it against the band anchors); an arbitrary instant is * rejected with 422. Absent → default band attribution. */ - forceSlotInstant: z.iso - .datetime({ offset: true }) + forceSlotInstant: isoDateTime() .transform((s) => new Date(s)) .optional() .describe( @@ -149,8 +148,7 @@ export const updateIntakeEventSchema = z .object({ takenAt: boundedTakenAtSchema.nullable().optional(), skipped: z.boolean().optional(), - scheduledFor: z.iso - .datetime({ offset: true }) + scheduledFor: isoDateTime() .transform((s) => new Date(s)) .optional(), /** @@ -165,8 +163,7 @@ export const updateIntakeEventSchema = z * resets to AUTO — the "Zuordnung lösen" path. A pin / unpin no longer * requires `takenAt` or `skipped` to change in the same request. */ - forceSlotInstant: z.iso - .datetime({ offset: true }) + forceSlotInstant: isoDateTime() .transform((s) => new Date(s)) .nullable() .optional() @@ -242,15 +239,13 @@ export const intakeStatusUpdateSchema = z.object({ .describe( "`snoozed` writes the deferral onto the MEDICATION row rather than the event, so it defers every pending dose of that medication and not just this one.", ), - takenAt: z.iso - .datetime({ offset: true }) + takenAt: isoDateTime() .transform((s) => new Date(s)) .optional() .describe( "When the dose was taken. Defaults to now. Ignored unless the status is `taken`.", ), - snoozedUntil: z.iso - .datetime({ offset: true }) + snoozedUntil: isoDateTime() .transform((s) => new Date(s)) .optional() .describe( @@ -261,8 +256,7 @@ export const intakeStatusUpdateSchema = z.object({ .describe( "Site recorded alongside a `taken` toggle, validated server-side against the medication's effective allowed set. Dropped for a non-injection medication, for one with site tracking off, and for a skip or snooze.", ), - forceSlotInstant: z.iso - .datetime({ offset: true }) + forceSlotInstant: isoDateTime() .transform((s) => new Date(s)) .optional() .describe( diff --git a/src/lib/validations/medication/inventory.ts b/src/lib/validations/medication/inventory.ts index acf0ff406..bd6abf9d9 100644 --- a/src/lib/validations/medication/inventory.ts +++ b/src/lib/validations/medication/inventory.ts @@ -1,4 +1,5 @@ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * v1.4.25 W19b — inventory (pen / vial) CRUD validators. @@ -47,13 +48,11 @@ export const createInventoryItemSchema = z .describe( "Kind of physical container (PEN / AMPOULE / BLISTER / INHALER / BOTTLE / OTHER). Defaults to OTHER. PEN and AMPOULE are the sealed reservoirs the first dose breaches, so they carry a 30-day post-opening window; every other kind expires only on its printed date.", ), - printedExpiry: z.iso - .datetime({ offset: true }) + printedExpiry: isoDateTime() .transform((s) => new Date(s)) .nullable() .optional(), - purchasedAt: z.iso - .datetime({ offset: true }) + purchasedAt: isoDateTime() .transform((s) => new Date(s)) .nullable() .optional(), @@ -85,8 +84,7 @@ export const createInventoryItemSchema = z export const updateInventoryItemSchema = z .object({ - markAsFirstUseAt: z.iso - .datetime({ offset: true }) + markAsFirstUseAt: isoDateTime() .transform((s) => new Date(s)) .nullable() .optional() @@ -99,8 +97,7 @@ export const updateInventoryItemSchema = z .describe( "Terminal override: zero the remaining units and mark the container USED_UP (physically discarded).", ), - printedExpiry: z.iso - .datetime({ offset: true }) + printedExpiry: isoDateTime() .transform((s) => new Date(s)) .nullable() .optional(), @@ -183,8 +180,7 @@ const MIN_EFFECTIVE_FROM = new Date("2020-01-01T00:00:00Z"); const FIVE_YEARS_MS = 5 * 365 * 24 * 60 * 60 * 1000; export const glp1DoseChangePostSchema = z.object({ - effectiveFrom: z.iso - .datetime({ offset: true }) + effectiveFrom: isoDateTime() .transform((s) => new Date(s)) .refine((d) => d.getTime() >= MIN_EFFECTIVE_FROM.getTime(), { message: "effectiveFrom must be on or after 2020-01-01", diff --git a/src/lib/validations/mental-health.ts b/src/lib/validations/mental-health.ts index 003f5f619..e153bd624 100644 --- a/src/lib/validations/mental-health.ts +++ b/src/lib/validations/mental-health.ts @@ -13,6 +13,7 @@ import { } from "@/lib/mental-health/instruments"; import { assertStableExternalId } from "@/lib/validations/external-id"; import { isValidTimezone } from "@/lib/tz/format"; +import { isoDateTime } from "@/lib/validations/datetime"; export const assessmentInstrumentEnum = z.enum(["PHQ9", "GAD7", "WHO5", "SCI"]); @@ -53,7 +54,7 @@ export const createAssessmentSchema = z /** Optional functional-impairment follow-up (not scored into the total). */ functionalDifficulty: z.number().int().min(0).max(3).optional(), /** ISO instant the screener was taken; defaults to now server-side. */ - takenAt: z.string().datetime().optional(), + takenAt: isoDateTime({ offset: false }).optional(), tz: z .string() .max(64) diff --git a/src/lib/validations/mood.ts b/src/lib/validations/mood.ts index d407d8e59..a9a4cc1bc 100644 --- a/src/lib/validations/mood.ts +++ b/src/lib/validations/mood.ts @@ -23,6 +23,7 @@ import { LEISURE_CATEGORY_KEYS, WORK_STATUS_KEYS, } from "@/lib/mood/context-vocabulary"; +import { isoDateTime } from "@/lib/validations/datetime"; // --- CRUD schemas for mood entries --- @@ -192,9 +193,7 @@ export const moodContextSchema = z.object({ .optional(), // The same plausibility bound the entry's own instant carries: no future // instants beyond the clock-skew tolerance, nothing before 1900. - eventAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), - ) + eventAt: validateEntryInstant(isoDateTime().transform((s) => new Date(s))) .nullable() .optional(), // Capped like the entry note beside it. One note for the whole context. @@ -242,7 +241,7 @@ export const createMoodEntrySchema = z // future instants beyond a 5-min clock-skew tolerance, no instant before // 1900. Mirrors the measurement + medication-intake bound. moodLoggedAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ), source: moodSourceEnum.optional().default("MANUAL"), // v1.12.1 — optional source-stable id (e.g. an iOS SwiftData row UUID). @@ -285,7 +284,7 @@ export const updateMoodEntrySchema = z.object({ context: moodContextSchema.nullable().optional(), // v1.17 W1b — same plausibility bound on the edit path. moodLoggedAt: validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + isoDateTime().transform((s) => new Date(s)), ).optional(), }); diff --git a/src/lib/validations/nutrients.ts b/src/lib/validations/nutrients.ts index 2fad5c706..b0d2fb69f 100644 --- a/src/lib/validations/nutrients.ts +++ b/src/lib/validations/nutrients.ts @@ -14,6 +14,7 @@ import { NUTRIENT_CODES, SYNCABLE_NUTRIENT_CODES, } from "@/lib/nutrients/catalog"; +import { isoDateTime } from "@/lib/validations/datetime"; export const MAX_NUTRIENT_ENTRIES_PER_BATCH = 500; @@ -160,7 +161,7 @@ export const nutrientOverviewSchema = z ), lastAttempt: z .object({ - at: z.iso.datetime({ offset: true }), + at: isoDateTime(), topReason: z.string(), }) .nullable() diff --git a/src/lib/validations/schedule-revision.ts b/src/lib/validations/schedule-revision.ts index cec714987..6f0521fa2 100644 --- a/src/lib/validations/schedule-revision.ts +++ b/src/lib/validations/schedule-revision.ts @@ -14,6 +14,7 @@ * minter reads it exactly like a write-path archive. */ import { z } from "zod/v4"; +import { isoDateTime } from "@/lib/validations/datetime"; /** HH:mm wall-clock literal — mirrors `validations/medication.ts`. */ const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/; @@ -21,20 +22,18 @@ const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/; export const scheduleRevisionCreateSchema = z .object({ /** Inclusive start instant of the manual era. */ - validFrom: z.iso - .datetime({ offset: true }) - .describe("Inclusive start instant of the manual era (ISO 8601)."), + validFrom: isoDateTime().describe( + "Inclusive start instant of the manual era (ISO 8601).", + ), /** * Exclusive end instant — the moment the next plan took over. Must * lie after `validFrom` and at or before the start of the live * schedule era (the newest archived revision's `validUntil`, or * "now" when no revision exists yet). */ - validUntil: z.iso - .datetime({ offset: true }) - .describe( - "Exclusive end instant of the manual era (ISO 8601). Must precede the current plan.", - ), + validUntil: isoDateTime().describe( + "Exclusive end instant of the manual era (ISO 8601). Must precede the current plan.", + ), /** * Daily dose times the era ran at. Same cap as live schedules. * Duplicates collapse and the result sorts ascending — "07:00, diff --git a/src/lib/validations/series-batch.ts b/src/lib/validations/series-batch.ts index af60fa87b..b43a68b86 100644 --- a/src/lib/validations/series-batch.ts +++ b/src/lib/validations/series-batch.ts @@ -7,6 +7,7 @@ */ import { z } from "zod/v4"; import { measurementTypeEnum } from "@/lib/validations/measurement"; +import { isoDateTime } from "@/lib/validations/datetime"; /** Upper bound on the types per batch — the dashboard never asks for more. */ export const SERIES_BATCH_MAX_TYPES = 16; @@ -28,12 +29,10 @@ export const seriesBatchQuerySchema = z `${SERIES_BATCH_MAX_TYPES}). Each is read as a daily series through ` + "the rollup tier.", ), - from: z.iso - .datetime({ offset: true }) + from: isoDateTime() .transform((s) => new Date(s)) .describe("Window start (ISO-8601 instant)."), - to: z.iso - .datetime({ offset: true }) + to: isoDateTime() .transform((s) => new Date(s)) .describe("Window end (ISO-8601 instant)."), }) diff --git a/src/lib/validations/sleep-stage-entry.ts b/src/lib/validations/sleep-stage-entry.ts index 3949c5390..2559f9c2e 100644 --- a/src/lib/validations/sleep-stage-entry.ts +++ b/src/lib/validations/sleep-stage-entry.ts @@ -23,6 +23,7 @@ import { validateMeasurementRange, writableMeasurementSourceEnum, } from "./measurement"; +import { isoDateTime } from "@/lib/validations/datetime"; /** The stage names the `SleepStage` column stores, as a caller writes them. */ export const SLEEP_STAGE_NAMES = [ @@ -40,9 +41,7 @@ export type SleepStageName = (typeof SLEEP_STAGE_NAMES)[number]; const VALUE_TOLERANCE_MINUTES = 1; const instant = () => - validateEntryInstant( - z.iso.datetime({ offset: true }).transform((s) => new Date(s)), - ); + validateEntryInstant(isoDateTime().transform((s) => new Date(s))); export const sleepStageEntrySchema = z .object({ diff --git a/src/lib/validations/vaccinations.ts b/src/lib/validations/vaccinations.ts index 4155b7564..2ef829b05 100644 --- a/src/lib/validations/vaccinations.ts +++ b/src/lib/validations/vaccinations.ts @@ -29,6 +29,7 @@ import { ANTIGEN_SLUGS, VACCINE_CATALOG_SLUGS, } from "@/lib/vaccinations/vaccine-catalog"; +import { isoDateTime } from "@/lib/validations/datetime"; const KNOWN_SLUGS: ReadonlySet = new Set(VACCINE_CATALOG_SLUGS); @@ -43,7 +44,7 @@ const KNOWN_SLUGS: ReadonlySet = new Set(VACCINE_CATALOG_SLUGS); */ const DAY_SKEW_MS = 24 * 60 * 60 * 1000; -const doseInstant = z.iso.datetime({ offset: true }).refine( +const doseInstant = isoDateTime().refine( (value) => { const at = new Date(value).getTime(); if (Number.isNaN(at)) return false; @@ -191,8 +192,8 @@ export type VaccinationUpdate = z.infer; export const vaccinationListQuerySchema = z .object({ antigenSlug: z.string().min(1).max(64).optional(), - from: z.iso.datetime({ offset: true }).optional(), - to: z.iso.datetime({ offset: true }).optional(), + from: isoDateTime().optional(), + to: isoDateTime().optional(), limit: z.coerce.number().int().min(1).max(500).optional(), }) .strict() @@ -299,7 +300,7 @@ export type VaccinationBoosterInput = z.infer; */ export const vaccinationSuggestQuerySchema = z .object({ - anchor: z.iso.datetime({ offset: true }), + anchor: isoDateTime(), }) .strict(); diff --git a/src/lib/validations/workout.ts b/src/lib/validations/workout.ts index 95fa195bb..3174f1d10 100644 --- a/src/lib/validations/workout.ts +++ b/src/lib/validations/workout.ts @@ -1,5 +1,6 @@ import { z } from "zod/v4"; import { writableMeasurementSourceEnum } from "./measurement"; +import { isoDateTime } from "@/lib/validations/datetime"; /** * Workout sport-type union. @@ -84,7 +85,7 @@ export type GeoJsonLineString = z.infer; */ export const workoutRouteSamplesSchema = z.array( z.object({ - t: z.iso.datetime({ offset: true }), + t: isoDateTime(), speedMs: z.number().min(0).max(50).optional(), hr: z.number().int().min(20).max(300).optional(), }), @@ -122,7 +123,7 @@ export const MAX_WORKOUT_HR_SAMPLES = 30_000; export const workoutHrSamplesSchema = z .array( z.object({ - t: z.iso.datetime({ offset: true }), + t: isoDateTime(), hr: z.number().int().min(20).max(300).optional(), speedMs: z.number().min(0).max(50).optional(), power: z.number().min(0).max(3000).optional(), @@ -157,8 +158,8 @@ export type WorkoutHrSamples = z.infer; export const createWorkoutSchema = z .object({ sportType: workoutSportTypeEnum, - startedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)), - endedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)), + startedAt: isoDateTime().transform((s) => new Date(s)), + endedAt: isoDateTime().transform((s) => new Date(s)), totalEnergyKcal: z.number().min(0).max(50000).optional(), totalDistanceM: z.number().min(0).max(1_000_000).optional(), avgHeartRate: z.number().int().min(20).max(300).optional(), diff --git a/src/lib/workouts/list-read.ts b/src/lib/workouts/list-read.ts index a9d637673..52aa9e32d 100644 --- a/src/lib/workouts/list-read.ts +++ b/src/lib/workouts/list-read.ts @@ -21,6 +21,7 @@ import { annotate } from "@/lib/logging/context"; import { pickCanonicalWorkoutRows } from "@/lib/measurements/pick-canonical-workout-rows"; import { cached, caches } from "@/lib/cache/server-cache"; import { workoutSportTypeEnum } from "@/lib/validations/workout"; +import { isoDateTime } from "@/lib/validations/datetime"; function emptyQueryValueToUndefined(value: unknown): unknown { if (value == null) return undefined; @@ -32,8 +33,7 @@ function emptyQueryValueToUndefined(value: unknown): unknown { const normalizedTimestampFilter = z .preprocess( emptyQueryValueToUndefined, - z.iso - .datetime({ offset: true }) + isoDateTime() .transform((value) => new Date(value).toISOString()) .optional(), ) diff --git a/tests/integration/drop-plaintext-location-migration.test.ts b/tests/integration/drop-plaintext-location-migration.test.ts index e20d48ea4..f20ce0785 100644 --- a/tests/integration/drop-plaintext-location-migration.test.ts +++ b/tests/integration/drop-plaintext-location-migration.test.ts @@ -13,7 +13,7 @@ * - Without readable values it drops the nine columns and the three * indexes, and the sealed copies are untouched. */ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; @@ -177,6 +177,19 @@ describe("migration 0409 (drop the readable location columns)", () => { expect(error?.message).toMatch( /1 home location\(s\), 0 location period\(s\) and 0 daily location\(s\)/, ); + // The operator reads this line in the container log; it points at the + // recovery steps, which exist. + expect(error?.message).toMatch( + /docs\/ops\/migration-0409-plaintext-location\.md/, + ); + expect( + existsSync( + resolve( + process.cwd(), + "docs/ops/migration-0409-plaintext-location.md", + ), + ), + ).toBe(true); // Nothing was dropped and the value is where it was. expect(await columnCount(tx)).toBe(9); diff --git a/tests/integration/mcp-output-schema-conformance.test.ts b/tests/integration/mcp-output-schema-conformance.test.ts index 508eb9cf3..407342fc2 100644 --- a/tests/integration/mcp-output-schema-conformance.test.ts +++ b/tests/integration/mcp-output-schema-conformance.test.ts @@ -416,7 +416,15 @@ describe("MCP tool outputs against their advertised schemas", () => { .filter((name) => !(name in CASES)); expect(uncovered, "add every new tool to CASES").toEqual([]); - const ajv = new Ajv2020({ strict: true, allErrors: true }); + // `allowUnionTypes`: zod 4.6 writes a nullable or primitive union as a + // `type` array (`["string", "null"]`), which is plain 2020-12. Strict + // mode otherwise refuses that spelling as an authoring style, not as an + // invalid schema; every other strict rule stays on. + const ajv = new Ajv2020({ + strict: true, + allowUnionTypes: true, + allErrors: true, + }); const failures: string[] = []; let validated = 0; // Tools that answered with data, not only a miss: a run where every diff --git a/tests/integration/mood-prognosis-ladder.test.ts b/tests/integration/mood-prognosis-ladder.test.ts index 7176734b4..294941871 100644 --- a/tests/integration/mood-prognosis-ladder.test.ts +++ b/tests/integration/mood-prognosis-ladder.test.ts @@ -418,8 +418,9 @@ describe("GET /api/mood/prognosis", () => { expect(res.status).toBe(403); // Asserted on the bytes, not on a parsed field: the point is that not one - // figure of the owner's reached the response. - const raw = await res.text(); + // figure of the owner's reached the response. The error's random request + // id is taken out first, since its hex digits can spell any small count. + const raw = (await res.text()).replace(/"requestId":"[^"]*"/, ""); expect(raw).not.toContain("predicted"); expect(raw).not.toContain("ciLow"); expect(raw).not.toContain("dimension:a2");