chore(openapi): document date-time fields as offset-bearing ISO-8601

v1.4.25 W7 phase 3.4 — proposal §3 symptom 10.

The schema patterns on every 'format: date-time' field already
accept both 'Z' and '±HH:MM' suffixes (the Zod sources use
{ offset: true }), so the wire contract is technically correct. The
discoverability problem is that consumers reading the spec assume
'date-time' implies UTC.

Adds an explicit 'Date-time contract' paragraph to the API
description naming the convention: server responses emit the
requesting user's offset when a user context is present (exports,
doctor-report PDF), background-job-authored payloads emit 'Z',
clients must accept both.

Regenerated docs/api/openapi.yaml via pnpm openapi:generate; check
passes.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 11:58:11 +02:00
1 parent ce6d6be95a
commit beb61b78bf
3 files changed
+32 -23

No files matched your search

+22 -21
View File
@@ -210,8 +210,7 @@ components:
- error
type: object
DeviceRegisterRequest:
description:
Native device registration. Re-registering an APNs token belonging to another user returns 409
description: Native device registration. Re-registering an APNs token belonging to another user returns 409
(cross-user-hijack guard).
properties:
apnsEnvironment:
@@ -354,8 +353,7 @@ components:
type: object
InsightsComprehensiveResponse:
additionalProperties: false
description:
AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface
description: AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface
needs day-level grounding.
properties:
citations:
@@ -699,7 +697,18 @@ info:
contact:
name: HealthLog
url: https://healthlog.dev
description: Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
## Date-time contract
Every field marked `format: date-time` is an ISO-8601 instant. The schema pattern accepts both `Z` (UTC) and
`±HH:MM` (offset) suffixes; clients must accept either. Server responses since v1.4.25 emit timestamps with the
requesting user's display-timezone offset when a user context exists (exports, doctor-report PDF).
Background-job-authored payloads (backups, admin tools) emit `Z`. Native iOS clients should always parse via a
Foundation ISO-8601 formatter with the `withInternetDateTime` + `withTimeZone` options enabled.
license:
name: AGPL-3.0
url: https://github.com/MBombeck/HealthLog/blob/main/LICENSE
@@ -709,8 +718,7 @@ openapi: 3.1.0
paths:
/api/auth/login:
post:
description:
"Browser callers receive a session cookie. Native callers (X-Client-Type: native or HealthLog-iOS UA
description: "Browser callers receive a session cookie. Native callers (X-Client-Type: native or HealthLog-iOS UA
prefix) additionally receive a paired access + refresh token."
requestBody:
content:
@@ -758,8 +766,7 @@ paths:
- Auth
/api/auth/me/coach-prefs:
get:
description:
Returns the persisted preferences with defaults filled in. Null persisted state resolves to the legacy
description: Returns the persisted preferences with defaults filled in. Null persisted state resolves to the legacy
v1.4.22 defaults.
responses:
"200":
@@ -775,8 +782,7 @@ paths:
tags:
- Auth
put:
description:
Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the
description: Persists the supplied prefs. Body is validated against `CoachPrefs`; missing keys fall back to the
documented defaults.
requestBody:
content:
@@ -854,8 +860,7 @@ paths:
- Auth
/api/auth/refresh:
post:
description:
Reuse of a consumed refresh token revokes every refresh token still active for the originating device
description: Reuse of a consumed refresh token revokes every refresh token still active for the originating device
(per-device blast radius from v1.4.23). Legacy tokens issued before v1.4.23 with a null deviceId fall back to
revoke-all-for-user.
requestBody:
@@ -879,8 +884,7 @@ paths:
- Auth
/api/devices:
post:
description:
Idempotent upsert by `token`. APNs token + environment are paired — supplying one without the other returns
description: Idempotent upsert by `token`. APNs token + environment are paired — supplying one without the other returns
422. Cross-user re-registration of either identifier returns 409.
requestBody:
content:
@@ -909,8 +913,7 @@ paths:
- Devices
/api/insights/chat/messages/{id}/feedback:
post:
description:
Persists a helpful/unhelpful rating for a single Coach reply. Reuses the v1.4.16 RecommendationFeedback
description: Persists a helpful/unhelpful rating for a single Coach reply. Reuses the v1.4.16 RecommendationFeedback
table via the polymorphic `targetType` column. The aggregator buckets ratings by (promptVersion, tone,
verbosity).
requestBody:
@@ -946,8 +949,7 @@ paths:
- Insights
/api/insights/comprehensive:
get:
description:
Full Insights surface — daily briefing, recommendations with rationale, optional weekly report + storyboard
description: Full Insights surface — daily briefing, recommendations with rationale, optional weekly report + storyboard
annotations. Strict-schema validated server-side.
responses:
"200":
@@ -1119,8 +1121,7 @@ paths:
- Measurements
/api/measurements/batch:
post:
description:
Up to 500 HealthKit entries per call. Idempotent via the `Idempotency-Key` header (replay window 24h).
description: Up to 500 HealthKit entries per call. Idempotent via the `Idempotency-Key` header (replay window 24h).
Per-entry status lets the iOS client advance its sync cursor accurately.
requestBody:
content:
+5 -1
View File
@@ -1015,6 +1015,7 @@ export default function InsightsPage() {
</div>
<HealthChart
chartKey="bp"
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
title={t("charts.bloodPressure")}
colors={["#ff79c6", "#8be9fd"]}
@@ -1227,6 +1228,7 @@ export default function InsightsPage() {
</div>
<HealthChart
chartKey="weight"
types={["WEIGHT"]}
title={t("charts.weight")}
colors={["#bd93f9"]}
@@ -1397,6 +1399,7 @@ export default function InsightsPage() {
</div>
<HealthChart
chartKey="pulse"
types={["PULSE"]}
title={t("charts.pulse")}
colors={["#50fa7b"]}
@@ -1431,7 +1434,7 @@ export default function InsightsPage() {
</Badge>
</div>
<MoodChart compareBaseline={compareBaseline} />
<MoodChart chartKey="mood" compareBaseline={compareBaseline} />
<InsightStatusCard
title={t("insights.assessmentTitle")}
@@ -1562,6 +1565,7 @@ export default function InsightsPage() {
{user?.heightCm ? (
<HealthChart
chartKey="bmi"
types={["WEIGHT"]}
title={t("targets.bmi")}
colors={["#f1fa8c"]}
+5 -1
View File
@@ -35,7 +35,11 @@ export const openApiBase: Pick<
title: "HealthLog API",
version: "1.4.23",
description:
"Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.",
"Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.\n\n" +
"## Date-time contract\n\n" +
"Every field marked `format: date-time` is an ISO-8601 instant. The schema pattern accepts both `Z` (UTC) and `±HH:MM` (offset) suffixes; clients must accept either. " +
"Server responses since v1.4.25 emit timestamps with the requesting user's display-timezone offset when a user context exists (exports, doctor-report PDF). Background-job-authored payloads (backups, admin tools) emit `Z`. " +
"Native iOS clients should always parse via a Foundation ISO-8601 formatter with the `withInternetDateTime` + `withTimeZone` options enabled.",
license: {
name: "AGPL-3.0",
url: "https://github.com/MBombeck/HealthLog/blob/main/LICENSE",