The bottom-sheet header already carries the avatar, title, new-chat,
settings, and close buttons on <sm viewports — adding the four-state
window pill there shrunk the title to a single character before
truncation. Wrap the pill in a hidden sm:block container so phone
users reach the same override via the sources-rail picker. Pin
min-w-0 on the SheetTitle so very long conversation titles always
clip cleanly inside the header.
The medication-form schedule editor's day-of-week row packed the
wide "Daily" pill (min-w-24) into the same single-flex row as the
seven weekday pills; at 320 px the row overflowed and the last
weekday clipped off-screen. Stack the Daily pill above a fixed
seven-column grid so every weekday keeps the 44 px tap-target floor
regardless of container width.
Drop two dead axis decorations from the GLP-1 DrugLevelChart: an
empty `<text>` child beneath the XAxis that rendered an invisible
SVG node, and a duplicate Recharts `label={…}` prop on the YAxis
that painted the unit-less caption inside a 1 px-wide axis where it
could never be read. The external `<p>` above the chart remains the
single source of truth for the caption.
v1.4.27 MB7 / CF-10 — touch-pin tooltip via pointer events runs in
parallel with the existing mouse hover, so a tap on a cell holds the
day-level breakdown open until the user taps outside or on a different
cell. Pen + mouse keep the hover affordance unchanged. The heatmap now
clamps the stretch-branch cell size to a 14 px floor and wraps the
non-stretch render in overflow-x-auto so narrow viewports horizontal
scroll instead of compressing the calendar below the touch target.
Promote the Coach drawer mount from the mother /insights page up to
/insights/layout.tsx so every routed sub-page can reach it. A new
CoachLaunchProvider exposes useCoachLaunch() for the hero strip,
suggested-prompt chips, and the seven sub-pages; the layout renders
the drawer beside the provider so navigation across Insights surfaces
no longer unmounts the conversation.
Each sub-page (blutdruck, gewicht, puls, stimmung, medikamente, bmi,
schlaf) mounts a new CoachLaunchButton that pins to the bottom on
narrow viewports and renders as an inline header action on lg+.
Promote the register page's submit button to the 44 px tap-target
floor (size="lg" + min-h-11 + w-full) so the primary action stays
finger-tap reachable on a narrow viewport.
Add a new branded not-found.tsx page. The previous implicit fallback
was Next.js's bare framework text; this introduces a lighter wrapper
than ErrorDetails (404 is not an application error) — Logo, headline,
explanatory paragraph, and a single "Back to dashboard" link. The
wrapper carries min-h-dvh and safe-area-inset-top so the panel
anchors centred under iOS Safari's animated URL bar and stays clear
of the notch.
Apply the same dvh + safe-area pattern on the root-layout global
error boundary and lift its inline button heights to 44 px. Wrap the
ErrorDetails panel in a min-h-dvh flex column so server-error pages
also centre instead of hugging the notch.
The /about page (GeoLite2 attribution) was listed in proxy.ts
PUBLIC_PATHS but missed in AuthShell's client-side PUBLIC_PATHS, so
unauthenticated visitors bounced through /auth/login. Add /about to
the AuthShell list and treat it as a standalone public page (same
edge-to-edge render as /privacy).
Apply safe-area-inset-top on the privacy and about sticky headers so
iOS notch / Dynamic Island viewports don't overlap the brand row. Add
a default-closed Contents <details> TOC at the top of the privacy
policy and wrap every HealthKit identifier <code> with break-all so
long camelCase strings (heartRateVariabilitySDNN,
environmentalAudioExposure) wrap inside narrow viewports.
CF-19 follow-up: the endpoint catalogue, active-token table, and
revoked-token table on `/settings/api` each gain a `md:hidden`
card-list partner so phones never have to horizontal-scroll the
tabular layout. Desktop tables stay verbatim behind `hidden md:block`.
The active-token cards expose a full-width revoke button that clears
the 44pt tap-target floor.
Final surface sweep for the v1.4.27 tap-target work: the chart band
overlay, the error-details action row, and the public-page header
links.
- `health-chart.tsx`: band overlay used to inset `right: 18px` while
the underlying ComposedChart margin is `right: 8`, so the colour
band drifted left of the plotted line by 10 px on every viewport.
Pin the overlay to the chart's actual right edge.
- `error-details.tsx`: drop `size="sm"` from the retry, copy, and
report-issue buttons so they sit at the default 40 px primitive
height — clears the WCAG 2.5.5 floor on touch.
- `auth/login/page.tsx`: back-to-passkey toggle wraps in `inline-flex
min-h-11 w-full` so the form-mode swap is reachable on touch.
- `privacy/page.tsx` and `about/page.tsx`: header HealthLog + Sign-in
links wrap in `inline-flex min-h-11` so the public-page chrome
clears the floor.
Lift the GLP-1 tile range strip + tab pills, the getting-started
checklist dismiss buttons, and the settings reorder + source-ladder
arrow buttons so every interactive element clears the 44 px WCAG
2.5.5 floor.
- `glp1-tile.tsx`: range-strip radio buttons gain `inline-flex
min-h-11 items-center justify-center px-3` so each preset sits at
44 px; the tab pills (level / weight) lift the same way.
- `getting-started-checklist.tsx`: dismiss-all CTA drops `size="sm"`
so it sits at the default 40 px primitive height; per-row dismiss
`X` button wraps in `inline-flex h-11 w-11`.
- `dashboard-layout-section.tsx`: reorder up + down arrows `h-5 w-5`
→ `size-11`; column-header spacer widens to `w-11` to keep the
layout grid aligned.
- `sources-section.tsx`: metric source ladder + device-type ladder
up/down arrows stack vertically on `<sm` so each button keeps the
`h-11 w-11` floor without crowding the row; sit side-by-side again
from `sm:` up to preserve the desktop layout.
`measurements.filterByType` shipped in en + de with the
v1.4.27 measurement-list aria-label wiring; the locale-integrity
test failed because the four English-fallback locales (es, fr, it,
pl) had not been backfilled. Add the placeholder English string so
each locale file is key-parity with `messages/en.json` again; real
translations follow in the v1.4.28 locale sweep.
- New `<Popover>` primitive (Radix-backed, Dracula surface tokens)
mirrors the existing `<Tooltip>` wrapper. Tooltips do not
tap-toggle reliably on touch; popovers do.
- The Coach composer info-icon swaps off `<Tooltip>` onto
`<Popover>` so the long-form "Enter to send, Shift+Enter for a new
line" copy is reachable via a tap on mobile while the keyboard
hover-preview semantic is preserved on desktop. The `aria-label`
on the trigger keeps screen-reader users announced.
- The composer textarea now carries `enterKeyHint="send"`,
`autoCapitalize="sentences"`, and an opt-in `autoFocusOnOpen` prop.
The drawer flips that prop on when it mounts the fallback
composer, so the textarea claims the cursor the first time the
drawer opens; subsequent re-renders of an already-open drawer do
not steal focus back.
- Coach evidence `<details>` is now controlled by local state. The
summary carries an accurate `aria-expanded` that flips in
lock-step with the disclosure; the native Enter / Space toggle
semantics are preserved.
Lift the icon-button and pagination controls on the list + form
surfaces under `/measurements` and `/medications` so every touch
target clears the 44 px WCAG 2.5.5 floor.
- `measurement-list.tsx`: mobile-row edit + delete buttons `size-8`
→ `size-11`; pagination chevrons `size="sm"` → `size="icon"
size-11` with translated `aria-label`; edit-dialog kebab
`h-9 w-9` → `size-11`; `DeleteButton` default `h-8 w-8` →
`size-11`. Empty-state CTAs drop `size="sm"` so they sit at the
default 40 px primitive height.
- `measurement-form.tsx`: reset kebab `h-9 w-9` → `size-11`.
- `intake-history-list.tsx`: desktop + mobile per-row edit + delete
buttons `size-8` → `size-11`; pagination chevrons → `size-11`
icon-buttons; edit-dialog kebab → `size-11`; shared `DeleteButton`
in this file → `size-11`.
- `SideEffectsSection.tsx`: per-entry delete `h-7 w-7` → `size-11`.
- `inventory-section.tsx`: live-pen "mark as in use" / "mark as used
up" pills drop `size="sm"`/`h-7` so they sit at `min-h-11`; past-pen
delete `h-6 w-6` → `size-11` icon-button.
Adds `measurements.previousPage` / `measurements.nextPage` and
`medications.previousPage` / `medications.nextPage` translation keys
so the pagination icon-buttons carry accessible labels in every
locale.
Bring the auth flows to parity with the rest of the v1.4.27 form
sweep so screen-reader users hear the form-level error banner tied
to the offending field and the mobile keyboard surfaces the right
on-screen affordance per step.
Login: email field flips to `inputMode="email"` +
`enterKeyHint="next"` and disables capitalize / spell-check;
password field carries `enterKeyHint="go"`. Both inputs reference
the error banner via `aria-describedby` and the banner itself gains
`aria-live="polite"`.
Register: same pattern across email, username, and the new-password
field. The submit key on password is `"go"` to fire the create-
account submit on virtual-keyboard return.
Records the five MB1 commits, the `<ResponsiveSheet>` API, every
migrated form mount, two race-conditioned commit-message
mis-attributions (`bd7cb938`, `44554729`), and the optional
sticky-CTA pattern deferral.
Bring the same `inputMode`, `enterKeyHint`, `autoComplete`, and
`spellCheck` defaults to the settings and admin surfaces:
- Account: profile email gets `autoComplete="email"` +
`enterKeyHint="next"`, the disabled username carries
`autoComplete="username"` so the password manager can still bind
the credential, and the height field hints the decimal keyboard.
- Telegram card: bot token and chat ID both opt out of autofill,
disable spell-check + autocapitalize, and lead the chained next →
done keyboard order.
- Integrations: Withings client-id / secret and the moodLog URL +
API key share the off-spell + no-capitalize secret-input contract
and chain into the test / save action via `enterKeyHint`.
- Notifications anchors: each card wrapper carries `scroll-mt-28`
so direct anchor navigation to `#telegram`, `#ntfy`, `#web-push`
lands the section header below the safe-area sticky header
instead of under it.
- Thresholds editor: min/max numeric inputs derive `inputMode`
("numeric" for integer ACTIVITY_STEPS, "decimal" elsewhere) and
chain next → done.
- Admin reminders: the late-window and missed-window minute inputs
pin `inputMode="numeric"` and the next → done keyboard order.
The primitive `<SheetContent>` close-X is an absolutely positioned
`opacity-70 rounded-xs` button that clashes with the rest of the
sheet's in-header chrome. Hide it via `showCloseButton={false}` and
render a matching ghost-icon `<SheetClose>` inline in the header
so the close affordance shares the same baseline, ghost variant,
and 44 px tap floor as the coach-drawer header pattern. The
settings-sheet test mock picks up a `SheetClose` passthrough so the
existing skeleton + form assertions keep working.
The mobile history + sources tray triggers used to mount as absolute
chevron buttons over the message thread, so they (a) overlapped the
first message bubble, (b) sat at a 28 px tap target (`h-7`), and (c)
relied on `backdrop-blur` to stay legible — which still failed on
busy threads.
Lift them into a sub-header strip above the thread: a flex row with
two ghost buttons at `min-h-11`. The strip itself carries `xl:hidden`
since both rails are inline at xl+; the history trigger keeps its own
`lg:hidden` because the history rail mounts inline already at lg+.
While the rail surface is in scope, drop the `opacity-0
group-hover:opacity-100` reveal on the history-rail per-row delete
button — touch surfaces have no hover, so the affordance was
invisible on mobile. The button now sits at `size-11` always-visible;
the confirming state still styles via `data-confirming`.
Test re-baselined to assert the new strip wrapper + `min-h-11` floor.
Pair every required field with `enterKeyHint`, `aria-required`,
`aria-invalid`, and `aria-describedby` so the form-level error banner
gets announced the moment validation fails, and so each input
surfaces the right virtual-keyboard hint (next, done) when the user
moves through the form.
- Measurement form (single + BP triple) and the inline edit dialog
pin `aria-describedby` to the alert region, add `enterKeyHint` on
every numeric and notes input, and pass the filter Select an
explicit `aria-label`.
- Medication form gets the chained `enterKeyHint` flow on name →
dose → unit → doses-per-pen, switches the schedule window inputs
to `type="time"`, and forces `text-base` on the dense schedule
cells so iOS Safari does not auto-zoom the field on focus.
- Intake create + edit dialogs reference the dialog-local error
banner via `aria-describedby`.
- Inventory add-pen dialog wires the same error descriptor and adds
`enterKeyHint` to the date and doses-total fields plus
`autoCapitalize="sentences"` on the notes textarea.
- Side-effect log notes textarea gets `enterKeyHint="done"` and
`autoCapitalize="sentences"`.
- Mood form pins the timestamp input to the error banner and lifts
the tags input to `autoCapitalize="none"` so colour-coded chips do
not get title-cased.
Adds the `measurements.filterByType` translation in de + en.
Lift every interactive element on the insights hero + advisor + coach
chrome to clear the 44 px WCAG 2.5.5 floor on touch.
- `hero-strip.tsx`: drop `size="sm"` from the action buttons (weekly
report, ask the Coach, weekly banner read/share/export). The default
primitive height now sits at 40 px so the explicit `sm` was the only
thing keeping these below the floor.
- `suggested-prompts.tsx`: chip floor `min-h-9` → `min-h-11`.
- `insight-advisor-card.tsx`: regenerate icon buttons drop their
`h-7 w-7` override so the `size="icon"` primitive default (40 px)
takes over; legacy-payload CTAs drop their `size="sm"`/`h-7` overrides.
- `coach-drawer.tsx`: window pill `h-7` → `h-11`; the three header
icon buttons (new chat, settings, close) `size-9` → `size-11`.
- `coach-input.tsx`: composer hint icon trigger `h-7 w-7` → `h-11
w-11`. The Tooltip → Popover swap is owned by MB3 — distinct lines.
- `message-thread.tsx`: thumbs feedback row `py-0.5 text-[11px]` →
`min-h-11 py-1.5 text-xs`. Same surface row, padding does the lift.
Swaps the medication create/edit, intake create/edit, side-effect
log, and inventory add-pen mounts from the centred `<Dialog>` to
the shared `<ResponsiveSheet>` primitive so the same surfaces flip
to a bottom-anchored sheet on narrow viewports. The "log this
thing" mental model — measurement, mood, medication, intake,
side-effect, inventory — now lands on a consistent mobile shape.
Configuration surfaces (IntakeImport, ApiEndpoint, PhaseConfig)
stay centred per Decision G; the dialog primitive's new
`max-h-[calc(100dvh-2rem)]` default keeps them inside the viewport
on short / soft-keyboard screens without the previous per-callsite
`max-h-[90vh]` ceiling.
Move `<PasswordInput>` from `src/components/settings/password-input.tsx`
to `src/components/ui/password-input.tsx` so the auth surfaces and
every settings card that masks a secret consume one primitive. The
two surfaces previously diverged on toggle markup and aria-label
wiring; the new home in `ui/` keeps them in lock-step.
While the primitive moves, lift the icon-only show/hide toggle to a
44 px hit area per WCAG 2.5.5 — `inline-flex h-11 w-11` wrapper with
the eye icon centred inside. The input itself gets `pr-12` so user
input never collides with the toggle glyph.
Consumer imports updated in:
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/integrations-section.tsx`
- `src/components/settings/ntfy-card.tsx`
- `src/components/settings/telegram-card.tsx`
- `src/components/admin/_shared.tsx`
Surface the right on-screen keyboard on iOS Safari and Android Chrome
without forcing every call site to repeat the attribute. The primitive
maps `type="number"` to `inputMode="decimal"` (most numeric fields in
HealthLog accept decimals — kg, mmol/L, sleep hours), `type="email"`
to `"email"`, `type="url"` to `"url"`, `type="tel"` to `"tel"`, and
`type="search"` to `"search"`. Integer-only call sites (steps, doses
per pen, reminder minutes) still opt into `inputMode="numeric"`
explicitly and the override wins.
Cover the derivation with seven additional unit tests on top of the
three existing autoComplete contracts; the suite now pins every
mapping plus the override path.
Move `<PasswordInput>` from `src/components/settings/password-input.tsx`
to `src/components/ui/password-input.tsx` so the auth surfaces (login
and register) and every settings card that masks a secret consume one
primitive. The two surfaces previously diverged on toggle markup and
aria-label wiring; the new home in `ui/` keeps them in lock-step.
While the primitive moves, lift the icon-only show/hide toggle to a
44 px hit area per WCAG 2.5.5 — `inline-flex h-11 w-11` wrapper with
the eye icon centred inside. The input itself gets `pr-12` so user
input never collides with the toggle glyph.
Consumer imports updated in:
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/integrations-section.tsx`
- `src/components/settings/ntfy-card.tsx`
- `src/components/settings/telegram-card.tsx`
- `src/components/admin/_shared.tsx`
The `<DialogContent>` close-X tap-target lift to `min-h-9 min-w-9`
landed in the parallel `ResponsiveSheet` commit because both buckets
share the same `dialog.tsx` file. See Decision I — 36 px is the
intentional exception for fixed icon-only close affordances.
Shared `<ResponsiveSheet>` flips between a bottom `<Sheet>` on narrow
viewports and the existing centred `<Dialog>` at `md+`, with a
sticky-pinned footer slot on the Sheet branch so primary CTAs stay
reachable when the keyboard collapses the viewport. Picks the
breakpoint via a new SSR-safe `useIsMobile` hook (`matchMedia`-driven,
default `false` on the server so SSR markup always matches the
desktop branch). Adds a `max-h-[calc(100dvh-2rem)] overflow-y-auto`
default to `<DialogContent>` so the existing centred-dialog forms
also scroll inside the viewport on short / soft-keyboard mobile
screens instead of spilling off-screen. Smoke suite locks both
branches, the header sr-only escape hatch, sticky-vs-flow footer
shape, and controlled-state plumbing.
Move primitive default heights up so any consumer that does not opt
into a specific size already meets the 40-44 px target-size minimum
without per-callsite overrides.
- `<Button>` default → `h-10` (40 px), `lg` → `h-11` (44 px), `icon`
→ `size-10`, `icon-lg` → `size-11`. `sm` and `xs` stay compact for
explicit-dense contexts.
- `<Input>` default → `h-10`.
- `<Select>` trigger default → `h-10`.
- `<DropdownMenuItem>` → `min-h-11` so menu rows always clear the
44 px floor on touch.
No same-line collisions with MB1 (`dialog.tsx` max-h cap) or MB3
(`input.tsx` inputMode derivation) — distinct touch zones.
Per-commit subjects, deliverable-vs-actual verification matrix for
every fix-plan item, the orphan-endpoint grep table that grounds the
five-endpoint v1.4.28 defer, and the seven recorded deviations from
the original fix-plan scope.
Audits the chart-tick timezone contract called out in BL-P4-2.
`sleep-stage-stacked-bar`, `mood-chart`, `health-chart`, and
`medication-compliance-chart` already anchor every day-key
construction at `Date.UTC(y, m - 1, d)` and route tick rendering
through the timezone-aware formatter pair (`tzFmt.date` / `tzFmt.dateShort`),
so they audit clean.
The compliance heatmap was the outlier: the day cells walked the
weekday and month-boundary axes with `getDay()` and `getMonth()`,
which read in the SSR server's local timezone while the dateKey
above used `toISOString().slice(0, 10)` (UTC). An SSR pass running
on a non-Berlin host would land the Monday alignment and the month
markers one position off relative to the cells they sit under.
Swapped to the UTC accessor pair so all three coordinates (dateKey,
weekday row, month-marker label) read off the same clock.
The tooltip date formatter also parsed the day-key against local
time; pinned to `T00:00:00Z` + the UTC accessor trio for the same
reason.
Sweeps the leftover export surface across four modules surfaced by
the v1.4.26 backlog dead-M5 / dead-M7 / simp-M6 / simp-M7 / BL-P4-5
items:
* src/lib/medications/glp1-knowledge.ts — eight constituent type
exports (`Glp1Route`, `Glp1DrugClass`, `Glp1Pharmacology`,
`Glp1Storage`, `Glp1Pen`, `Glp1AdverseReactions`, `Glp1Indications`,
`Glp1SourceCitations`) drop the `export` keyword. All eight are
used only as constituents of the still-exported `Glp1DrugRecord`
aggregate and have no external import sites; narrowing the module
surface stops a future tree-shaker from re-pulling them.
* src/lib/medications/scheduling/cadence.ts — `ExpectedDose` drops
its `export` keyword (no external readers).
* src/lib/medications/scheduling/cadence.ts — `pairDoses` drops the
redundant second `.sort()`. Sorting by `windowStart` and by the
centre `(start+end)/2` produce the same order for the chronologically
separable windows pairDoses actually sees, so the O(n log n) tail
that re-sorted the result was wasted work.
* src/lib/medications/titration/ladder.ts — `escalationDue`'s
JSDoc gets the parameter-role documentation simp-M7 called out.
The `drugId` parameter renames to `drugIdForCeiling` so the
"next-step exists" lookup role is obvious at the call site.
* src/lib/ai/coach/glp1-snapshot.ts — `__testables.WEEKDAY_KEYS`
and the underlying private constant retire (zero callers).
Drops the hand-maintained 14-key listing on `metricPriorityObjectSchema`
and the parallel 14-key block on the top-level `sourcePrioritySchema`
flat shape; both surfaces now derive their property set from the
`SOURCE_PRIORITY_METRIC_KEYS` constant via `Object.fromEntries`. The
two listings had already drifted twice during v1.4.25 polishing
passes when a new metric class was added. Adding a metric class is
now a single-line constant edit instead of three matching listings
the lint chain doesn't enforce.
Lifts the per-locale message map and the dotted-key resolver out of
`src/lib/i18n/context.tsx` and `src/lib/i18n/server-translator.ts`
into a new `src/lib/i18n/shared-resolve.ts`. The two surfaces had
each carried their own private copy of the same data structure and
the same six-step path resolver; the maps drifted twice when a new
locale was added during v1.4.18 and v1.4.25 polishing passes.
Pinning both call-sites to a shared module keeps client and server
fallback semantics in lockstep and shrinks the surface a future
locale addition has to touch from two files to one.
The /api/audit-log GET handler has zero runtime callers in src/, tests/,
scripts/, or docs/ — verified via grep against the route path plus the
auditLog model name. The admin counterpart at /api/admin/audit-log is
the surface every consumer reads from. Drops the orphan route file
outright per the v1.4.27 fix-plan Section 4 Decision A.
The five admin / monitoring endpoints flagged for the scope-
maximization sweep all carry a README + CHANGELOG documentation
reference, which the fix-plan classes as a consumer for the
defer-on-tie rule:
/api/admin/ai-settings — README L362–363, CHANGELOG L752
/api/admin/backup/test — README L368, CHANGELOG L752
/api/admin/status-overview — README L367, CHANGELOG L753
/api/monitoring/glitchtip/test — README L381
/api/monitoring/umami/test — README L382
All five defer to v1.4.28 for a maintainer call — either wire each to
a real consumer or drop the route + README mention together. Seeds
the v1.4.28 backlog entry with the per-endpoint grep findings so the
decision is one read away.
Also seeds the v1.4.28 backlog with the missing `insights.coach.window.lastYear`
i18n key across the six locale bundles — owned by the i18n bucket.
Adds a year-in-review window between last90days and allTime so the
Coach drawer can ground a long-horizon question (seasonal mood
patterns, year-over-year weight trajectory) without falling back to
the unbounded `allTime` window. The new enum value flows through the
scope schema, the snapshot builder's window-to-day-count mapping,
the provenance window-set seeding so the source-chips render the
year-in-review chip, and the Coach source-chips Record.
Extends the snapshot test with a `lastYear`-specific assertion that
pins the scope window and the provenance envelope.
Adds `GET /api/workouts` as a paginated read surface that runs every
fetch through `pickCanonicalWorkout()` so a duplicated workout that
Apple Health and Withings both publish collapses to the canonical
row before reaching the client. The picker stays a pure function;
the route owns the query window, the fetch multiplier that keeps the
page full after dedup, and the meta envelope that surfaces the
dedup ratio.
Collapses the batch-route route-attach lookup from `Promise.all` of
`tx.workout.findFirst` per manual workout to a single batched
`findMany` keyed by the deterministic `(source, startedAt, sportType)`
tuple. For a 100-row batch this cuts the per-batch round-trip count
from 1 + N to 2 while preserving the most-recent createdAt tie-break
through in-memory grouping.
Adds `__tests__/canonical-dedup.test.ts` covering twin collapse,
proximity-window separation, sport-type isolation, and the limit
clamp.
The W7d narrowing already pinned the catch to the V8 private-field
TypeError shape, but a real read regression — say, a header parser
crashing on a malformed value — still slipped through silently into
the fallback path because the narrow-catch swallowed every tolerated
error without trace. Logs every fallback through console.warn in
non-test environments so the dev console and run log surface a
regression while the production path keeps the same defensive
defaults. The /api/insights/targets stray-brace comment typo and the
BASE_SYSTEM_PROMPT / INSIGHTS_SYSTEM_PROMPT bare exports flagged by
the original BL-P1-2 / BL-P1-4 items were already retired earlier
in the v1.4.27 cycle.
Captures the i18n cleanup pass: 154 dead-key paths retired across
six locales (~924 strings), 38 new keys added for B1/B3/B4/B5
component-side inserts, locale-native date format ordering for
FR/ES/IT/PL, drift-guard test pinning the GLP-1 / carrier / empty-
state / notification / personal-record key sets, mood-label
shared module landing BL-P6-11.
Also lands the four probe scripts (.planning/v1427-b6-*.mjs) used
to enumerate dead keys and build the new key inserts, sibling to
the existing .planning/v1422-w1a-*.mjs audit artifacts.
Lands every new translation key referenced by the v1.4.27 buckets B1,
B3, B4 and B5 across all six locale bundles (38 keys per locale):
- dashboard.glp1.tabLevel / tabWeight / tabsAria / rangeStripLabel /
levelUnavailable / weightUnavailable (B1 — drug-level vs weight tab
strip on the GLP-1 dashboard tile).
- admin.carrier / admin.carrierUnknown (B3 — IP-address carrier chip
on the admin login overview).
- insights.emptyState.{bloodPressure,weight,pulse,bmi,mood,medication,sleep}.{title,description,cta}
(B4 — per-metric empty-state cards on the insights sub-pages).
- notifications.admin.{deployFailedTitle,deployFailedBody,testNotificationTitle,testNotificationBody,reminderCheckMissedTitle,reminderCheckMissedBody,reminderCheckOverdueTitle,reminderCheckOverdueBody}
(B5 — admin notification messages routed through the localised
dispatch helper).
- notifications.user.telegramTestBody (B5 — user-facing Telegram test
message localised to User.locale).
Also retires the now-dead charts.moodLabel1..5 chart-axis label keys:
the mood chart and the mood list now consume the same MOOD_LABEL_KEYS
map from a new src/lib/mood/labels.ts so the chart-axis copy stays in
lockstep with the list (BL-P6-11). Three distinct copy sets for the
same five mood states drifted twice during v1.4.18 / v1.4.25
polishing; the shared resolver locks them together going forward.
New drift-guard test at src/__tests__/i18n-drift-guard.test.ts pins
the GLP-1 tab, admin carrier, insights empty-state, admin/user
notification, and personal-record key sets so a contributor who only
edits one bundle gets a precise failure instead of a 200-line parity
diff. The locale-integrity test continues to assert global parity;
the drift-guard layers on top with call-site-specific coverage.
W10 design-review carry-over items. M7 lifts the medication-form
day-of-week buttons, the more-horizontal trigger, and the schedule
label-input gaps to the 44 px / space-y-2 standard used by every
other settings surface. L1 pairs every animate-spin/pulse/bounce
with motion-reduce:animate-none across the component tree so users
with prefers-reduced-motion get a still surface. L3 adds the
aria-controls + panel-id pairing to the inventory + coach-evidence
<details> blocks for parity with the Health Score provenance
accordion. L4 inserts a sr-only h4 per therapy-timeline entry so
screen-reader users can jump between medication blocks.
Adds a new `format` namespace across every locale bundle that
documents the locale's native date / time pattern ordering as
template strings (`{day}.{month}.{year}`, `{month}/{day}/{year}`, …).
DE keeps DD.MM.YYYY with dot separators, EN keeps MM/DD/YYYY with
slashes, FR/ES/IT adopt DD/MM/YYYY with slashes (the previous
EN-shaped fallback was visibly wrong on French weekly-report PDFs),
PL adopts DD.MM.YYYY with dots.
The runtime `makeFormatters()` helper still routes through `Intl.*`;
the keys exist so PDF / CSV / email surfaces that render outside a
React context can still pick the right ordering hint.
A new smoke test at
`src/lib/i18n/__tests__/format-locale-order.test.ts` pins the
ordering per locale so a copy-paste regression can't silently flip
DD/MM to MM/DD on a non-English bundle.
Sweeps 154 dead keys per locale bundle (~924 strings across six
locales) flagged by the v1.4.27 dead-key audit. Drops the
insights.*Status* trio (loading / noKey / unavailable) across six
metrics, the legacy `insights.aiInsights` / `dashboard.insightsPreview`
roots that B1 replaced with the gated tab strip, the
`insights.healthScore.askCoach` / `coachPrompt` pair retired with the
inline button, the orphan onboarding.v2.* stub set, plus the
ad-hoc `medications.glp1.drug.*.class` and `notifications.eventPersonalRecord*`
strings that lost their callers in v1.4.25/W17.
Also drops the stale prefill-format test under
`health-score-card.test.tsx` that asserted the now-removed coachPrompt
key shape — the negative-assertion test for the retired inline button
stays in place.
The locale-integrity test already validates the new shape: no
re-baselining needed because the test asserts parity + no-empties
rather than a key-count threshold.