mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
docs(insights): document english tile-id contract + naming ADR
Regenerate `docs/api/openapi.yaml` so the `InsightsLayoutBody` enum carries the canonical English tile ids first, then the accepted-but-deprecated legacy German ids, with the normalisation + deprecation spelled out in the schema description. Add ADR 0001 fixing the convention — localised UI copy, English internal identifiers — and the non-breaking alias strategy. Add an iOS coordination note covering the new canonical ids, the continued acceptance of the legacy ids, and the recommended migration path.
This commit is contained in:
1 parent
607dd9e5df
commit
262a3ad70a
4 files changed
+214
-12
No files matched your search
@@ -0,0 +1,80 @@
|
||||
# v1.8.0 — server → iOS: Insights tile-id rename (non-breaking)
|
||||
|
||||
Date: 2026-05-31
|
||||
From: server
|
||||
To: iOS
|
||||
Status: informational — no action required to keep working; action recommended before a future major
|
||||
|
||||
## Summary
|
||||
|
||||
v1.8.0 renames the Insights tile ids (and the matching web route slugs) from
|
||||
German to English to make the identifier space consistent. **This is
|
||||
non-breaking for the iOS layout sync.** The `GET/PUT /api/insights/layout`
|
||||
endpoint keeps accepting the old German ids. You do not need to ship anything to
|
||||
stay working. When convenient, migrate to the canonical English ids.
|
||||
|
||||
## What changed
|
||||
|
||||
The canonical tile ids in `GET/PUT /api/insights/layout` are now English:
|
||||
|
||||
| Legacy (≤ v1.7.x) | Canonical (v1.8.0+) |
|
||||
|---|---|
|
||||
| `blutdruck` | `blood-pressure` |
|
||||
| `puls` | `pulse` |
|
||||
| `sauerstoff` | `oxygen` |
|
||||
| `koerpertemperatur` | `body-temperature` |
|
||||
| `gewicht` | `weight` |
|
||||
| `aktive-energie` | `active-energy` |
|
||||
| `schlaf` | `sleep` |
|
||||
| `ruhepuls` | `resting-pulse` |
|
||||
| `stimmung` | `mood` |
|
||||
| `medikamente` | `medications` |
|
||||
|
||||
`overview`, `bmi`, `hrv`, and `workouts` were already language-neutral and are
|
||||
unchanged.
|
||||
|
||||
## Why it does not break you
|
||||
|
||||
- **PUT still accepts the legacy German ids.** The server's validation enum is
|
||||
the union of the canonical English ids and the legacy German aliases, so a
|
||||
`PUT` carrying `blutdruck` / `puls` / … validates (no 422).
|
||||
- **The server normalises legacy → canonical on write.** A `PUT` with German ids
|
||||
persists the English equivalents. So after any save, the stored layout is
|
||||
canonical English regardless of what you sent.
|
||||
- **GET always returns canonical English ids.** A layout you persisted before
|
||||
v1.8.0 (German ids) is normalised on read — `GET` surfaces the English ids
|
||||
without forcing a re-`PUT`.
|
||||
- **Dedupe.** If a payload carries both a legacy and a canonical id for one tile,
|
||||
the server keeps a single canonical entry.
|
||||
|
||||
So the worst case for an un-migrated iOS build is: it keeps sending German ids,
|
||||
the server keeps accepting and normalising them, and on the next `GET` the app
|
||||
sees English ids it does not recognise in its local enum. If your client filters
|
||||
unknown tile ids defensively, that filter must learn the English ids — see
|
||||
below.
|
||||
|
||||
## What iOS should do (recommended, before a future major)
|
||||
|
||||
1. **Add the English ids to your tile-id enum / model** (keep the German ones as
|
||||
decode-only aliases if your decoder is strict, or just map both).
|
||||
2. **Send English ids on `PUT`.** Once your build is out, you stop relying on the
|
||||
server's alias acceptance.
|
||||
3. **Decode `GET` responses expecting English ids.** Because the server
|
||||
normalises on read, a current `GET` already returns English; an app that hard-
|
||||
filters to the old German enum would drop every tile. Decode the English ids
|
||||
before you rely on this endpoint in a v1.8.0+ environment.
|
||||
|
||||
## Deprecation timeline
|
||||
|
||||
The legacy German ids are accepted-but-deprecated. They will be **removed from
|
||||
the accepted input set in a future major version** once we confirm clients have
|
||||
migrated. No date is fixed yet; we will give notice through this channel before
|
||||
the removal. Until then, both id sets validate.
|
||||
|
||||
## References
|
||||
|
||||
- Contract: `docs/api/openapi.yaml` → `InsightsLayoutBody` (lists canonical +
|
||||
legacy ids, with the deprecation noted in the schema description).
|
||||
- Rationale: `docs/adr/0001-insights-naming-convention.md`.
|
||||
- Web route slugs got the same English rename with 301 redirects from the legacy
|
||||
German URLs; that is web-only and does not affect the iOS contract.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 0001 — Insights naming convention: localised UI, English identifiers
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-05-31
|
||||
|
||||
## Context
|
||||
|
||||
The Insights surface grew bilingually. The mother page (`/insights`) and its
|
||||
routed metric sub-pages were keyed by German slugs (`/insights/blutdruck`,
|
||||
`/insights/gewicht`, …) because German was the primary author voice when the
|
||||
routed strip first shipped. Over time the surface accumulated a mix: route
|
||||
folders, the tile-layout enum, and several internal maps were German, while
|
||||
later additions (`hrv`, `oxygenSaturation`, `restingHr`, `activeEnergy`,
|
||||
`workouts`) landed in English. The result was an inconsistent identifier space
|
||||
where a contributor could not predict whether a given key was German or English.
|
||||
|
||||
Identifiers leak. A German route slug shows up in URLs, in browser history, in
|
||||
the OpenAPI contract the iOS client reads, and in every internal map that pivots
|
||||
on the slug. Mixed-language identifiers make the codebase harder to navigate and
|
||||
make the public API contract read inconsistently.
|
||||
|
||||
Two surfaces also speak these identifiers across a boundary we do not control:
|
||||
|
||||
1. **URLs** — bookmarked and cached by the PWA.
|
||||
2. **The tile-layout contract** — `GET/PUT /api/insights/layout`, which the
|
||||
native iOS client persists against. The tile ids in that payload are a
|
||||
client-visible contract.
|
||||
|
||||
## Decision
|
||||
|
||||
Separate the two naming axes cleanly:
|
||||
|
||||
- **UI copy is always localised.** Every visible string resolves through an
|
||||
i18n `t()` key and is translated across all six locales (`de / en / es / fr /
|
||||
it / pl`). The user never sees a raw identifier.
|
||||
- **Internal identifiers are English.** Route slugs (`/insights/<slug>`), the
|
||||
tile-layout enum (`INSIGHTS_TILE_IDS`), query keys, and the internal
|
||||
slug→metric / slug→target maps all use established English terms. English is
|
||||
the lingua franca of the codebase and of the public API contract.
|
||||
|
||||
Concretely, v1.8.0 migrates the routed Insights slugs and the tile-layout enum
|
||||
from German to English (e.g. `blutdruck` → `blood-pressure`, `gewicht` →
|
||||
`weight`, `medikamente` → `medications`). The full rename table lives in
|
||||
`src/lib/insights/sub-page-metric.ts` (slugs) and `src/lib/insights-layout.ts`
|
||||
(tile ids).
|
||||
|
||||
## Non-breaking strategy
|
||||
|
||||
The migration must not break either client-visible boundary:
|
||||
|
||||
- **URLs.** `next.config.ts` `redirects()` emits a `permanent: true` (301)
|
||||
redirect from every legacy German slug to its English target, including a
|
||||
`:path*` variant so any nested route survives. Bookmarks and cached PWA
|
||||
navigation keep resolving.
|
||||
- **Tile-layout contract.** The layout endpoint keeps **accepting** the legacy
|
||||
German tile ids on input. The Zod enum is built from
|
||||
`ACCEPTED_INSIGHTS_TILE_IDS` (canonical English ids *plus* the legacy German
|
||||
aliases), so a `PUT` carrying old ids validates rather than tripping a 422.
|
||||
`normalizeInsightsTileId` collapses each legacy id onto its canonical English
|
||||
replacement; this normalisation runs on **both** the read path
|
||||
(`resolveInsightsLayout`) and the write path (`serializeInsightsLayout`), so:
|
||||
- a layout persisted by a `≤ v1.7.x` client surfaces canonical English ids on
|
||||
`GET` without forcing a re-`PUT`;
|
||||
- a `PUT` sending legacy ids persists canonical English ids;
|
||||
- a layout that carries both a legacy and a canonical id for one tile dedupes
|
||||
to a single canonical entry.
|
||||
|
||||
The legacy ids are accepted-but-deprecated. They will be removed from the
|
||||
accepted set in a future major version once telemetry shows clients have
|
||||
migrated. The OpenAPI `InsightsLayoutBody` schema documents the canonical ids,
|
||||
the accepted legacy ids, and the deprecation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The Insights identifier space is uniformly English and predictable.
|
||||
- No client breaks on upgrade: old URLs redirect, old tile ids normalise.
|
||||
- The legacy-alias map and the redirect table are deliberate, finite debt to be
|
||||
retired in a future major. Until then they are covered by tests
|
||||
(`src/app/api/insights/layout/__tests__/route.test.ts`) that assert the
|
||||
non-breaking round-trip.
|
||||
- New Insights surfaces follow the convention from the start: English slug,
|
||||
English tile id, localised `t()` copy.
|
||||
+36
-8
@@ -2440,17 +2440,27 @@ components:
|
||||
type: string
|
||||
enum:
|
||||
- overview
|
||||
- blood-pressure
|
||||
- pulse
|
||||
- oxygen
|
||||
- body-temperature
|
||||
- weight
|
||||
- bmi
|
||||
- active-energy
|
||||
- workouts
|
||||
- sleep
|
||||
- resting-pulse
|
||||
- hrv
|
||||
- mood
|
||||
- medications
|
||||
- blutdruck
|
||||
- puls
|
||||
- sauerstoff
|
||||
- koerpertemperatur
|
||||
- gewicht
|
||||
- bmi
|
||||
- aktive-energie
|
||||
- workouts
|
||||
- schlaf
|
||||
- ruhepuls
|
||||
- hrv
|
||||
- stimmung
|
||||
- medikamente
|
||||
visible:
|
||||
@@ -2467,7 +2477,11 @@ components:
|
||||
- version
|
||||
- tiles
|
||||
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
|
||||
schema version; tile ids are a closed enum derived from the server's tile registry."
|
||||
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
|
||||
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
|
||||
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
|
||||
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
|
||||
ids. The legacy ids are deprecated and will be removed in a future major version."
|
||||
CoachMessageFeedbackRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -5935,17 +5949,27 @@ components:
|
||||
type: string
|
||||
enum:
|
||||
- overview
|
||||
- blood-pressure
|
||||
- pulse
|
||||
- oxygen
|
||||
- body-temperature
|
||||
- weight
|
||||
- bmi
|
||||
- active-energy
|
||||
- workouts
|
||||
- sleep
|
||||
- resting-pulse
|
||||
- hrv
|
||||
- mood
|
||||
- medications
|
||||
- blutdruck
|
||||
- puls
|
||||
- sauerstoff
|
||||
- koerpertemperatur
|
||||
- gewicht
|
||||
- bmi
|
||||
- aktive-energie
|
||||
- workouts
|
||||
- schlaf
|
||||
- ruhepuls
|
||||
- hrv
|
||||
- stimmung
|
||||
- medikamente
|
||||
visible:
|
||||
@@ -5964,7 +5988,11 @@ components:
|
||||
- tiles
|
||||
additionalProperties: false
|
||||
description: "Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout
|
||||
schema version; tile ids are a closed enum derived from the server's tile registry."
|
||||
schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed
|
||||
`/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht,
|
||||
aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and
|
||||
are normalised to their English equivalents before persisting; GET responses always carry the canonical English
|
||||
ids. The legacy ids are deprecated and will be removed in a future major version."
|
||||
Medication:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
MEDICATION_TREATMENT_CLASS_VALUES,
|
||||
} from "@/lib/validations/medication";
|
||||
import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt";
|
||||
import { INSIGHTS_TILE_IDS } from "@/lib/insights-layout";
|
||||
import { ACCEPTED_INSIGHTS_TILE_IDS } from "@/lib/insights-layout";
|
||||
import { exportSelectionSchema } from "@/lib/validations/health-record-export";
|
||||
|
||||
/**
|
||||
@@ -1121,14 +1121,26 @@ medicationExtractionSchema.meta({
|
||||
|
||||
// Insights tile layout — mirrors the Zod schema in
|
||||
// `src/app/api/insights/layout/route.ts`. The tile-id enum is derived
|
||||
// from the same `INSIGHTS_TILE_IDS` source so the contract cannot drift.
|
||||
// from the same `ACCEPTED_INSIGHTS_TILE_IDS` source so the contract
|
||||
// cannot drift.
|
||||
//
|
||||
// v1.8.0 — the canonical ids are English (`blood-pressure`, `pulse`,
|
||||
// `oxygen`, `body-temperature`, `weight`, `active-energy`, `sleep`,
|
||||
// `resting-pulse`, `mood`, `medications`). The endpoint still ACCEPTS
|
||||
// the legacy German ids (`blutdruck`, `puls`, `sauerstoff`,
|
||||
// `koerpertemperatur`, `gewicht`, `aktive-energie`, `schlaf`,
|
||||
// `ruhepuls`, `stimmung`, `medikamente`) on input so existing iOS
|
||||
// layouts keep validating; the server normalises them to the canonical
|
||||
// English id before persisting, and GET always returns canonical ids.
|
||||
// The legacy ids are accepted-but-deprecated and will be dropped from
|
||||
// the accepted set in a future major.
|
||||
const insightsLayoutSchema = z
|
||||
.object({
|
||||
version: z.literal(1),
|
||||
tiles: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.enum(INSIGHTS_TILE_IDS),
|
||||
id: z.enum(ACCEPTED_INSIGHTS_TILE_IDS),
|
||||
visible: z.boolean(),
|
||||
order: z.number().int().min(0).max(99),
|
||||
}),
|
||||
@@ -1139,7 +1151,7 @@ const insightsLayoutSchema = z
|
||||
.meta({
|
||||
id: "InsightsLayoutBody",
|
||||
description:
|
||||
"Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout schema version; tile ids are a closed enum derived from the server's tile registry.",
|
||||
"Per-user Insights tile layout: an ordered list of tiles with a visibility flag. `version` is the layout schema version. Tile ids are a closed enum: the canonical ids are English (matching the routed `/insights/<slug>` sub-pages). The legacy German ids (blutdruck, puls, sauerstoff, koerpertemperatur, gewicht, aktive-energie, schlaf, ruhepuls, stimmung, medikamente) remain accepted on input for backward compatibility and are normalised to their English equivalents before persisting; GET responses always carry the canonical English ids. The legacy ids are deprecated and will be removed in a future major version.",
|
||||
});
|
||||
|
||||
// v1.7.0 — health-record export selection. Strict shape: unknown keys
|
||||
|
||||
Reference in new issue
Block a user