mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
chore(release): v1.4.28
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).
This commit is contained in:
1 parent
b74851f9d6
commit
ff4bfcfc3c
2 files changed
+332
-1
No files matched your search
+331
@@ -1,5 +1,336 @@
|
||||
# Changelog
|
||||
|
||||
## [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.ts` entry, 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]/history` page exclusively. The `compact` mode
|
||||
and `windowHoursBefore` override 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 `/insights` and 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 `/insights` action 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 on `PUT /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 a `measurements.duplicateTimestamp` translation 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.DateTimeFormat` and 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/puls` hung 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 `/insights` mother-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-xl` border.** The
|
||||
shadcn `<Card>` default paints `rounded-xl` while the trends row
|
||||
needs `rounded-md` to match the `<HealthChart mini>` shape. Mini-
|
||||
mode override adds `rounded-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 into `flex h-full flex-col` with the disclaimer
|
||||
footer pinned via `mt-auto`; the hero row switches to
|
||||
`md:items-stretch` on `md+` so the score column reaches the
|
||||
bottom of the suggested-prompt rail. Equal-height contract across
|
||||
the hero strip.
|
||||
- **`/measurements` aggregation truncated long-window queries.** The
|
||||
`take` cap applied before bucketising, so a 365-day daily-grain
|
||||
query returned only the first N raw rows. Aggregation now runs
|
||||
as a Postgres `date_trunc` GROUP 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 yet` marker so future
|
||||
contributors do not re-flag it.
|
||||
- **Insights-targets locale strings missed on FR / ES / IT / PL.**
|
||||
`measurements.duplicateTimestamp` and `insights.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]` to `text-xs` across 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 on `lg+`); `<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/zielwerte` collapses to a 44 px
|
||||
icon-only affordance. The visible label drops; the same string
|
||||
carries through `aria-label` + `title` so 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 on `md+`
|
||||
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-fr` rule lifts from `md:` to every breakpoint; each
|
||||
chart wraps in a `trends-row-chart-slot` div with `shrink-0` so
|
||||
the slot is the load-bearing height anchor. `<MoodChart>` mini
|
||||
envelope tightens to match `<HealthChart mini>`. Captions clamp
|
||||
at three lines.
|
||||
- **`<CoachLaunchScope>.metric` narrows to `CoachScopeSource`.** 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 through `useInsightsAnalytics`
|
||||
and `<MetricEmptyState>`. Adding a future metric sub-page is a
|
||||
one-file change.
|
||||
- **Chart dynamic imports consolidate on `<HealthChartDynamic>`.**
|
||||
Six `dynamic(() => 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/measurements` aggregate branch flips to GROUP BY.** The
|
||||
Postgres `date_trunc` path resolves daily / weekly / monthly
|
||||
grain server-side; the `BUCKET_CAP` keeps the response bounded.
|
||||
iOS callers that pass `limit` only land in the unchanged raw
|
||||
branch (byte-stable against v1.4.27).
|
||||
- **`/medications/[id]/history` page wrapper stride.**
|
||||
`space-y-4 → space-y-6` so 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. `AnalyticsData` hoists to
|
||||
`src/types/analytics.ts` as `SubPageAnalyticsData`.
|
||||
- **`<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. Nine `next/dynamic` chart call sites lift onto
|
||||
the shared primitive.
|
||||
- **`with-timeout.ts` envelope helper.** Wraps any provider call in
|
||||
a 20 s timeout and returns a structured `TimeoutEnvelope<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 on `md+` and a `<ResponsiveSheet>` on phone viewports.
|
||||
Three-sentence body per locale. The trigger paints
|
||||
`aria-expanded` + `aria-controls`; the body owns an `id` matched
|
||||
by `aria-describedby` on 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:
|
||||
every `data-slot` identifier and breakpoint class survives intact.
|
||||
- **`useReportWebVitals` beacon + bundle analyzer.** `pnpm analyze`
|
||||
runs `next build` with `@next/bundle-analyzer` enabled (reports
|
||||
land in `.next/analyze/`). The beacon POSTs CLS / LCP / INP / FCP
|
||||
/ TTFB / INP to `/api/internal/web-vitals` via
|
||||
`navigator.sendBeacon` with a `fetch({ 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_URL` is set), and forwards the metric name +
|
||||
value + rating to the wide-event logger via `annotate({ meta })`.
|
||||
- **`aggregate=monthly` grain 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.
|
||||
- **`dispatchLocalisedNotification` user-lookup cache.** 30 s TTL
|
||||
LRU keyed on `userId`; 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.lastYear` key 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 resolved `from`/`to` for 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
|
||||
duplicate `next/dynamic` call 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 when `aggregate` is 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_BURNED` sport-typed row pins the 409 response
|
||||
shape and the `measurements.duplicateTimestamp` key 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 no `aggregate` param. Flipping the client to
|
||||
pass `aggregate=monthly` plus the user's earliest measurement as
|
||||
`from` is 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.
|
||||
|
||||
## [1.4.27] — 2026-05-15
|
||||
|
||||
Mobile capability + maintainer-finding cleanup. The headline is a new
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.4.27",
|
||||
"version": "1.4.28",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
Reference in new issue
Block a user