[1.4.28] — 2026-05-16
Bug-fix and consistency follow-through after the v1.4.27 mobile sweep.
The headline is a tighter dashboard and insights surface: six widgets
retire from code entirely (GLP-1 tile, dashboard <DrugLevelChart>
mount, GLP-1 detail-page intake history + inventory, the
<InsightAdvisorCard> block and its regeneration affordance, the
weekly-report route), four broken edges close (workout-edit duplicate-
timestamp 409, BD-Zielbereich tile rebuilt on the shared
<TrendCard> primitive, /insights/puls chart timeout fallback,
sticky tab-strip scroll lock), and one consistency contract lands per
maintainer directive: every medication-list row, every medication-
detail section header, every trend tile, every Coach launch glyph,
every chart-height contract reads on one shape. The HealthScore card
grows to fill its column, gains an accessible ? tooltip explaining
the delta, and drops the placeholder "Wochenbericht erstellen"
button. iOS contracts are intact; the only /api/* evolution is
additive (the aggregate=monthly grain on /api/measurements and an
internal-only /api/internal/web-vitals beacon route).
Removed
- GLP-1 dashboard tile.
src/components/dashboard/glp1-tile.tsx
and every reference (dashboard-layout.tsentry, the
dashboard.glp1.*i18n keys, the test fixtures) retire. The tile
is gone from/; the recovered vertical real estate flows to the
trends row + HealthScore column. - Dashboard
<DrugLevelChart>mount. The standalone Drug-Level
pane retired from/. The component itself stays — it serves the
/medications/[id]/historypage exclusively. Thecompactmode
andwindowHoursBeforeoverride drop with the dashboard mount; the
history-page recipe paints the chart inside
<MedicationDetailSection>on the same heading scale as every
other section on the page. <InsightAdvisorCard>surface. The "Persönlicher Berater"
card at the bottom of/insightsand its "Insights aktualisieren"
regeneration button retire. The Coach drawer is the single
assistant entry point. Component, mounts, test fixtures, i18n
keys, and the cached-advisor query all retire together.- Weekly-report route.
/insights/report/[week]and the
<WeeklyReportBanner>mount on the hero retire. No remaining
consumer; the Coach drawer covers the equivalent ask. - GLP-1 detail-page intake history + inventory. The "Dosis-
Historie" disclosure and "Bestand" section retire from the GLP-1
medication detail page. The page now collapses to header,
schedule, dose-titration ladder, and side-effects — every section
the maintainer flagged as wanted; every section flagged as
unwanted is gone. Inventory tracking remains opt-in for a future
release. - Hero "Wochenbericht erstellen" button. The placeholder
affordance retires from the/insightsaction row. The row now
carries a single "Coach fragen" button so the HealthScore card on
the opposite column has the height to reach the last suggested
prompt.
Fixed
- Workout edit raised a 500 on duplicate timestamps. The save
path onPUT /api/measurements/[id]returned a generic 500 when a
sport-typed sample (ACTIVE_ENERGY_BURNED,FLIGHTS_CLIMBED,
WALKING_RUNNING_DISTANCE,EXERCISE_TIME) collided with an
existing row on the same(type, measuredAt)natural key. Returns
409 with ameasurements.duplicateTimestamptranslation key now;
the iOS native client and the web measurement-form both surface
the conflict to the user instead of a generic toast. - BD-Zielbereich tile rendered "1.1." instead of numbers. The
tile's bespoke rendering path read the schedule date through
Intl.DateTimeFormatand missed the underlying value. Tile
rebuilt on the shared<TrendCard>primitive so the chrome, the
state shape, the prop API, and the fallback behaviour match the
Weight and BP siblings. The divergence has been the open finding
across v1.4.22, v1.4.25, v1.4.26 patches — this release rewrites
the tile instead of patching. /insights/pulshung waiting on the assessment provider. The
pulse status card called the language-model provider with no
client-side timeout. A timeout helper (with-timeout.ts) caps
every status-card provider call at 20 s and returns the rule-based
fallback string on timeout; the page renders the chart immediately
and the assessment line resolves async.- Scroll stuck on
/insightsmother-page navigation. The sticky
tab-strip's intersection-observer was fighting the focus-on-mount
logic so the page sometimes refused to scroll past the tab row.
The observer rebinds on route change and the focus-on-mount
effect now waits for the next animation frame so the scroll
position is locked in before the observer fires. <DrugLevelChart>paint exceeded the active range window. The
chart fetched every dose-event in history regardless of the
selected range. Now bounded to the active range — 30 days / 90
days / all-time — so the wire payload and the recharts render
scale linearly with the visible window.- Mood trend tile painted a residual
rounded-xlborder. The
shadcn<Card>default paintsrounded-xlwhile the trends row
needsrounded-mdto match the<HealthChart mini>shape. Mini-
mode override addsrounded-md; the visual rhythm of the row
reads on one envelope. - Side-effects card overflowed at 320 px. The "Nebenwirkungen
erfassen" CTA chip ran past the section header. The qualifier
drops from every locale ("Erfassen" / "Log" / "Consigner" /
"Registrar" / "Registra" / "Dodaj"); the section title carries
the context. The date column on the entry rows narrows from 88 px
to 56 px so the free-text slot recovers 32 px of wrap headroom. - Medication-list row shape diverged for GLP-1 entries. The
GLP-1 row painted with a brand icon and a middle-dot separator;
every other row painted two-line without an icon. Both routes
through a shared<MedicationCardHeader>now — line 1 is{name} {dose}, line 2 is the class label plus state badges. The GLP-1
outlier shape is gone. - HealthScore card came up short below the action+prompts column.
The card now opts intoflex h-full flex-colwith the disclaimer
footer pinned viamt-auto; the hero row switches to
md:items-stretchonmd+so the score column reaches the
bottom of the suggested-prompt rail. Equal-height contract across
the hero strip. /measurementsaggregation truncated long-window queries. The
takecap applied before bucketising, so a 365-day daily-grain
query returned only the first N raw rows. Aggregation now runs
as a Postgresdate_truncGROUP BY and the cap applies to the
bucketised result; a 1-year window returns up to 365 daily
buckets. The all-time chart range resolves to monthly grain (24-
bucket ceiling) or weekly when history is under two years.- Schlaf sub-page missed the per-section assessment slot. Six of
seven insights sub-pages mount<InsightStatusCard>underneath
the chart; Sleep did not. Documented as the intentional skip
with a// no per-section assessment yetmarker so future
contributors do not re-flag it. - Insights-targets locale strings missed on FR / ES / IT / PL.
measurements.duplicateTimestampandinsights.sleep.description
now carry native translations in every locale.
Changed
- BD-Zielbereich tile aligned to
<TrendCard>. Tile chrome,
state shape, prop API, and fallback states all match Weight and
BP. The Z-value rounds to one decimal at the boundary, the legend
carries the same micro labels across siblings, the empty state
reads the same copy with the same CTA shape. - Medication-list rows route through
<MedicationCardHeader>.
GLP-1 and standard rows both render{name} {dose}on line 1 and
class label plus state badges on line 2. State badges break onto
their own row at 320 px so the two-line shape holds. - Medications detail-page chrome collapsed to one heading scale.
Every section on/medications/[id]mounts through
<MedicationDetailSection>(text-base font-semibold leading-6 tracking-tight). DrugLevelChart's standalone header migrates to
<h2>with the same classes. Micro labels lift from
text-[10px]/text-[11px]totext-xsacross Scheduling,
Titration, SideEffects. Three scales survive the page: heading,
body (text-sm), micro (text-xs). - Coach launch shape consolidated to three primitives. A single
<LayoutCoachFab>mounts once per Insights surface (FAB on<lg,
hidden onlg+);<CoachLaunchButton>paints the inline desktop
ghost button only;<TargetCoachButton>paints the per-card
icon-only chat-bubble. The five-shape inventory collapses to
three. Every Coach launch glyph reads on one vocabulary
(Sparkles); the suggested-prompt chips stay their own visual
class because the pre-fill flow is conceptually distinct. - Targets page Coach launch is icon-only. The bottom-left "Coach
fragen" pill on/insights/zielwertecollapses to a 44 px
icon-only affordance. The visible label drops; the same string
carries througharia-label+titleso screen readers still
announce the action. - HealthScore delta gains a
?explainer. Tap or hover on the
icon next to the "-3 vs last week" line opens a popover onmd+
and a<ResponsiveSheet>bottom-sheet on phone viewports.
Three-sentence body per locale: which components contribute, what
window, what the user can do to nudge it. - Trends row pins to an equal-height contract. The
auto-rows-frrule lifts frommd:to every breakpoint; each
chart wraps in atrends-row-chart-slotdiv withshrink-0so
the slot is the load-bearing height anchor.<MoodChart>mini
envelope tightens to match<HealthChart mini>. Captions clamp
at three lines. <CoachLaunchScope>.metricnarrows toCoachScopeSource. The
type is forward-looking — no call site passes the parameter yet —
but the union now mirrors what the iOS client speaks. The
v1.4.28 narrowing closes the open type-system note from
v1.4.27.- Coach mobile sheet caps at 90 dvh. The Coach drawer's bottom-
sheet branch on phones now matches the<ResponsiveSheet>
convention (the v1.4.27 release picked 95 dvh; this release
aligns the cap). - Insights sub-pages share one data-fetch hook. The seven sub-
pages duplicated the same React-Query analytics fetch plus the
empty-state branch; both now route throughuseInsightsAnalytics
and<MetricEmptyState>. Adding a future metric sub-page is a
one-file change. - Chart dynamic imports consolidate on
<HealthChartDynamic>.
Sixdynamic(() => import("@/components/charts/health-chart"))
call sites collapse to one re-export. Every dynamic chart slot
ships a layout-stable<ChartSkeleton>loading state so the
page does not jump as the bundle resolves. - Side-effects add CTA shortens across all six locales. "Log",
"Erfassen", "Consigner", "Registrar", "Registra", "Dodaj" — the
qualifier drops; the section title carries the context. /api/measurementsaggregate branch flips to GROUP BY. The
Postgresdate_truncpath resolves daily / weekly / monthly
grain server-side; theBUCKET_CAPkeeps the response bounded.
iOS callers that passlimitonly land in the unchanged raw
branch (byte-stable against v1.4.27)./medications/[id]/historypage wrapper stride.
space-y-4 → space-y-6so the section stride matches
/insights/*.
Added
useInsightsAnalytics()hook +<MetricEmptyState>primitive.
Shared data-fetch + empty-state scaffolding across the insights
sub-pages. Adding a new metric route reads on one fetcher and one
empty-state recipe.AnalyticsDatahoists to
src/types/analytics.tsasSubPageAnalyticsData.<HealthChartDynamic>re-export. Single canonical lazy-loaded
health-chart entry consumed by the dashboard, the five/insights
metric pages, the trends row, and the VO2 max chart row.<ChartSkeleton>loading state across every dynamic chart.
Layout-stable placeholder pinned at the same height the chart
paints when loaded. Ninenext/dynamicchart call sites lift onto
the shared primitive.with-timeout.tsenvelope helper. Wraps any provider call in
a 20 s timeout and returns a structuredTimeoutEnvelope<T>
({ ok: true, value } | { ok: false, error }). Adopted by the
insights status cards; the user-visible chart paints immediately
while the language-model assessment resolves async.HealthScoreDeltaExplainer. New
src/components/insights/health-score-delta-explainer.tsx
surfaces the?icon next to the delta line; tap opens a
popover onmd+and a<ResponsiveSheet>on phone viewports.
Three-sentence body per locale. The trigger paints
aria-expanded+aria-controls; the body owns anidmatched
byaria-describedbyon the delta<span>.<LayoutCoachFab>mount. Floating Coach affordance lifts out
of<CoachLaunchButton>and mounts once per Insights layout. The
duplicate-FAB nodes (one per sub-page) collapse to one node in the
a11y tree.<MobileRailTray>carve-out. The two<Sheet>mounts that
wrap the Coach history rail and sources rail lift out of
<CoachDrawer>into their own ~80 LOC primitive. Pure refactor:
everydata-slotidentifier and breakpoint class survives intact.useReportWebVitalsbeacon + bundle analyzer.pnpm analyze
runsnext buildwith@next/bundle-analyzerenabled (reports
land in.next/analyze/). The beacon POSTs CLS / LCP / INP / FCP
/ TTFB / INP to/api/internal/web-vitalsvia
navigator.sendBeaconwith afetch({ keepalive: true })
fallback. The route validates the payload with Zod, rate-limits
to 60 / min per IP, requires same-origin Referer (when
NEXT_PUBLIC_APP_URLis set), and forwards the metric name +
value + rating to the wide-event logger viaannotate({ meta }).aggregate=monthlygrain on/api/measurements. Additive
enum value resolving 30-day buckets. All-time chart range now
resolves to monthly aggregation when full history ≥ 2 years,
weekly when shorter. The 24-bucket ceiling keeps the response
bounded. Schema is iOS-additive.dispatchLocalisedNotificationuser-lookup cache. 30 s TTL
LRU keyed onuserId; repeat dispatches inside the window share
one Prisma query. Capped at 1 000 entries with FIFO eviction.
For a burst of admin alerts to the same recipient the cache
collapses N round-trips to 1.insights.coach.window.lastYearkey across six locales. The
enum value shipped in v1.4.27; the strings now carry native
copy in every locale ("year so far", "Jahresrückblick", "depuis
le début de l'année", "lo que va de año", "anno in corso",
"od początku roku").
Performance
- Health-chart fetches bound to the active range window. The
client now passes the resolvedfrom/tofor the selected range
instead of pulling the full history every time; the wire payload
and the Recharts render scale linearly with the visible window. - Insights status-card provider call capped at 20 s. Hung
assessments resolve to the rule-based fallback instead of blocking
the chart paint. The chart is rendered from local analytics on
first paint; the assessment line streams in when (and if) the
provider returns. - Dispatch-localised notification user lookup cached. See the
Added entry — same line item, performance impact is the
collapsed Prisma round-trips on burst dispatches. - Dynamic chart imports collapsed onto one re-export. Six
duplicatenext/dynamiccall sites reduce to one; chart
skeletons pin layout across the dynamic resolve so the user
never sees a height jump.
Tests
- 17 new Vitest cases for the range-aggregation route. Cover
the iOS gate (raw branch byte-stable whenaggregateis omitted),
the daily 365-bucket assertion, the monthly cap, the all-time
monthly path, the all-time weekly fallback, and the per-grain
threshold. - 8 new cases on the web-vitals beacon route. Happy 204, three
schema rejections, malformed JSON, 429 under rate limit,
cross-origin drop, same-origin accept. - 5 new cases on the dispatch-localised cache. Cache hit, TTL
expiry, just-inside-TTL, reset helper, per-user isolation. - 11 new tests across the medication, insights, and target
surfaces. Three on<MedicationCardHeader>, one on the side-
effects narrow-viewport responsive shape, four on the HealthScore
delta explainer (size, screen-reader wiring, trigger label,
closed-by-default), one on the mood-tile radius, one on the
trends-row equal-height contract, one on the targets-page Coach
icon affordance. - 5 new cases on
<MobileRailTray>. Slot identifiers,
breakpoint hides, rail content forwarding, localised titles,
closed-state gating. - 1 new case on the workout edit duplicate-timestamp path. The
ACTIVE_ENERGY_BURNEDsport-typed row pins the 409 response
shape and themeasurements.duplicateTimestampkey resolves to
native translations on every locale. - Test totals: 3953 → 3974 passing across the broad sweep, with
546 / 546 green on the touched surfaces.
Deferred
- SD-H1 client wire-up. The server machinery lands in
8144281d; the client still defaults the "All time" tab to a
365-day window with noaggregateparam. Flipping the client to
passaggregate=monthlyplus the user's earliest measurement as
fromis a four-line edit deferred to v1.4.29 — the bucketed
rows carry a divergent shape and the chart adapter needs a small
helper to merge bucketed vs raw inputs. - R4 Medium-tier findings. The simplifier, design, UI-conformity,
i18n, and senior-dev reviewers each surfaced a Medium-tier
backlog. Closed only the high-impact items in this release;
Medium-tier items (8 design, 4 UI-conformity, 5 i18n, 7 senior-
dev, plus the<ResponsiveSheet>footer-slot wiring and the 5
<Dialog>consumers still to migrate) defer to v1.4.29 per the
scope-discipline directive ("less scope, more depth"). - Five admin / monitoring orphan endpoints. Unchanged from
v1.4.27. The wire-or-remove decision still pends — each is
documented in README but has no runtime consumer. Carry forward
to v1.4.29.