From 262a3ad70a2152f8d26aef9ede5bd8dd4dbb50eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 31 May 2026 22:10:12 +0200 Subject: [PATCH] docs(insights): document english tile-id contract + naming ADR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ....8.0-server-to-ios-insights-slug-rename.md | 80 ++++++++++++++++++ docs/adr/0001-insights-naming-convention.md | 82 +++++++++++++++++++ docs/api/openapi.yaml | 44 ++++++++-- src/lib/openapi/routes.ts | 20 ++++- 4 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 .planning/ios-coord/v1.8.0-server-to-ios-insights-slug-rename.md create mode 100644 docs/adr/0001-insights-naming-convention.md diff --git a/.planning/ios-coord/v1.8.0-server-to-ios-insights-slug-rename.md b/.planning/ios-coord/v1.8.0-server-to-ios-insights-slug-rename.md new file mode 100644 index 000000000..081ec2a4f --- /dev/null +++ b/.planning/ios-coord/v1.8.0-server-to-ios-insights-slug-rename.md @@ -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. diff --git a/docs/adr/0001-insights-naming-convention.md b/docs/adr/0001-insights-naming-convention.md new file mode 100644 index 000000000..2dd5ba69a --- /dev/null +++ b/docs/adr/0001-insights-naming-convention.md @@ -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/`), 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. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 7a9805e4e..0716df9f4 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -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/` 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/` 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: diff --git a/src/lib/openapi/routes.ts b/src/lib/openapi/routes.ts index 98760b4f5..aa2a52e74 100644 --- a/src/lib/openapi/routes.ts +++ b/src/lib/openapi/routes.ts @@ -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/` 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