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:
Marc-André Bombeck committed 2026-05-31 22:12:25 +02:00
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
View File
@@ -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:
+16 -4
View File
@@ -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