mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
aabc7915cbccdab3b050bc6d3002ec9f28fc2b2a
46
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aabc7915cb |
feat(settings): a time-format preference and one home for insight pills
The clock format followed the UI language with no way out — an English interface forced AM/PM everywhere. A new profile preference (automatic, 24-hour, 12-hour) feeds every formatter through the shared locale context, persists on the user row and rides the profile endpoints. The insight pill list in settings gains the same eye toggle the overview tiles have, so ordering and visibility live in one place; the page-side 'manage detail pages' block folds into a link there. Includes the locale bundles for both features and the regenerated API contract. |
||
|
|
c1976249c3 | chore(insights): drop the now-unused layout hook wrapper | ||
|
|
6ebac22c65 |
fix(insights): reconcile customizable-overview review findings
- Layout PUT accepts version 1 as well as 2 (live iOS client still PUTs version 1); the serializer normalises to the canonical v2 blob, so a v1 body never 422s. Mirrored in the OpenAPI schema + description; yaml regen. - Add the Weight tile to the inline tile editor so it can be hidden / reordered like every other Vitals grid tile. - Replace the non-transitive Vitals tile-drag comparator with a pure, total-order rebuild helper (reorder the Vitals subset, substitute it back into the full list in original-order slots, densify). - Gated-off section rows keep a disabled eye toggle in the same position with the gate hint as a caption below the title, so the row never jumps. - Cap the disclosed Vitals tile list height (max-h-[60vh] scroll, overscroll-contain) so a tile drag does not fight the page scroll. - Gate edit mode until the layout GET settles so a save can never flush defaults over a real saved layout. - Drop the duplicate top-right "Done" affordance: hide the customize toggle while editing; the in-card Done / Reset own save and reset. - Restore focus on enter (edit-card heading) and on close (customize toggle). Tests: route regression for a version-1 PUT persisting a resolved v2 layout; unit coverage for the new Vitals rebuild helper (order, non-Vitals stability, per-tile visibility, no mutation). |
||
|
|
e3539d7c94 |
feat(insights): drive overview section + tile render from saved layout
Read the resolved insights layout on /insights and render the customizable region from it. Sections render in saved order, hidden ones drop out, and the Vitals grid honours the per-tile order + visibility. HeroStrip stays anchored above the customizable region; every existing feature/data gate (briefing flag, cycle-enabled, per-tile data) still applies on top, so the default layout reproduces the prior page. - useInsightsLayout() reads GET /api/insights/layout through the query-key factory, defaulting to DEFAULT_INSIGHTS_LAYOUT while in flight. - orderedVisibleSectionIds + resolveTileLayout: pure, unit-tested helpers. - Widen DEFAULT_VISIBLE_TILE_IDS with the Vitals/Mobility grid tiles so a fresh account keeps the tiles it shows today. |
||
|
|
7333d8e40f |
feat(cycle): add the cycle-tracking web vertical
Build the web UI for the cycle-tracking category against the existing `/api/cycle/*` routes: a month calendar + circular phase wheel, a fast log-day sheet, a predictions panel, phase-insight cards, and a single-page settings form, all under a gated `/cycle` tab. - Calendar + wheel: a hand-rolled SVG phase ring (0 KB runtime, matching the wellness-ring aesthetic with calm per-phase hues) showing the current phase + day-of-cycle, alongside a month grid where logged period days show a filled pip, the predicted next period renders as a ranged band (never a single date), the fertile window shows only when the goal allows it (the API suppresses it server-side otherwise), and ovulation + per-day symptoms carry markers. Reads `GET /api/cycle/calendar`. - Log-day sheet: one-tap period start, flow / symptom / ovulation-test / mucus chips, basal-temperature, intercourse + protection, and a free-text note. Optimistic write to the day-log + period routes; invalidates the cycle query keys. - Predictions panel: next-period range with a confidence pill, goal-gated fertile window, a still-learning state for fewer than three cycles or raw-chart mode, cycle-history stats, and the fixed non-medical disclaimer (reusing the drug-level-estimate disclaimer pattern). - Insights cards render whatever phase-correlation cards the API surfaces, with a graceful empty-state until the feed lands. - Settings: a single dynamic form where the inclusive goal selector drives the copy (conception framing only for trying-to-conceive), plus typical- length priors, raw-chart mode, sensitive-category encryption, discreet notifications, and export / delete. PATCHes `/api/auth/me/cycle-prefs`. - Nav: a Cycle entry in the sidebar and the mobile More hub, gated on `cycleTrackingEnabled` from `/api/auth/me` and hidden otherwise. Add the cycle query keys + dependent-key bundle to the factory, full copy across all six locales, and dedicated per-theme phase hues. Plain second-person voice, WCAG 2.1 AA, no new dependencies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
20d7187b0d |
feat(insights): premium animated wellness-score rings
Rebuild the wellness-score strip on /insights as a signature, high-end moment instead of a flat dial. - Five distinct per-metric hues — readiness green, recovery teal, sleep indigo-blue, stress amber, strain violet — hand-tuned per theme so each holds contrast on the white light-mode card. Fixes the dark-theme collapse to a cyan/green wash (the semantic info token resolved to the same cyan as three rings) and the light-theme wash-out of the raw cyan on white. - A spring-eased arc sweep that overshoots ~6% then settles, a left-to-right staggered reveal with each number trailing its own arc, a conic specular sheen that sweeps once, a token-tinted bloom (a gentle breathing pulse only on a top-band ring), a segmented instrument-style track, and a faint film-grain tile surface for material depth. - A "vs your normal" ghost arc under the live arc for recovery, stress and strain, derived from the trailing score series already on the payload, so the user sees today against their baseline at a glance. The reveal plays once per browser session (sessionStorage-gated) so a background sync refetch never re-triggers it. All motion is CSS-first — no animation dependency, @property + a sampled linear() spring — and is fully disabled under prefers-reduced-motion. The arc stays a hand-rolled SVG gauge; the band semantic still rides data-band + the band word + the aria-label, never colour alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
465c8b8e33 |
feat(insights): drive metric stat strip from the chart range tab; add chart tile header
Remove the draggable chart brush. The Min/Max/Median/Mittelwert strip now always reflects the data visible under the chart's active range tab (7/30/90/ all): the chart reports per-type stats for its range-sliced series and the sub-page threads them into the strip. No drag, no "selected range" pill — the range tab is the single selector for both chart and strip. Blood pressure recomputes both systolic and diastolic. Give each metric chart card the canonical TileHeader (icon + title, same size and spacing as the assessment/target/stat tiles) via an optional titleIcon; the dashboard and mini mounts keep the compact heading. Drop the now-unused charts.selectRange and insights.subPage.stats.selectedRange keys from all six locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d0d0f49385 |
feat(insights): add loading skeleton and error+retry to HealthKit subpages
The HealthKit metric pages consumed only `{data, isEmpty}` and ignored the
hook's `isLoading`/`error`, so they painted nothing in flight and fell
through silently on failure. Branch on `isLoading` to a stat-strip + chart
skeleton, and on `error` to a message plus a Retry that re-issues the
analytics query (mirrors the vitals-dashboard pattern). Expose `refetch`
from the analytics hook and add a `subPage.loadError` string in all six
locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0995664893 |
feat(charts): add opt-in brush that reports per-type windowed stats
Add an optional Recharts <Brush> to the shared HealthChart, gated behind a new `selectableDomain` prop. When set, the chart mounts the brush beneath the plot and reports the Min/Max/Median/Mean over the brushed window per type through `onDomainStats`. A range-tab change or a series refetch clears or clamps the selection so the brush never indexes past the data. The dashboard, trends-row, VO2 row, sleep, and rationale mounts do not pass the prop, so they render unchanged — the feature is fully isolated behind the flag. `computeWindowStats` is a pure helper (no React/Recharts) so the chart can summarise the array it already holds without a refetch and the maths is unit-tested directly. The median uses the same linear-interpolated midpoint as `summarize()` so a window and the full-range summary read on one algorithm. `useChartDomainStats` lifts the per-type window into shared state with a stable callback and a shallow guard against repeat windows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4ace1b7706 |
refactor(insights): unify the metric subpage template — target tile, order, stat strip, drop the range row
Make every metric subpage identical by construction in the shared scaffold rather than per-page bespoke. - Target tile matches the other cards: dark `bg-card` background and the `rounded-xl border p-4` spacing the assessment and stat-strip cards use, instead of the old translucent `bg-card/20` panel. - Drop the 7T/30T/90T/1J range row and its "+X% vs previous" delta from every subpage. The chart owns its own window via its in-card range tabs, so the chart query is unaffected. Retire the now-dead `MetricRangeControls`, `MetricRangeDelta`, `TimeRangePills`, `useAnalyticsRange`, the `useInsightsRangePref` persisted-range hook, and `sentimentFromMetricDirection` along with their tests. - Fix the spine order to intro -> chart -> target card -> assessment -> Min/Max/Median/Mittelwert stat strip. The stat strip moves to the foot of the spine, below the assessment. Remove the "Letzte Messung" card and the headline primary tile from subpages; retire `MetricLastMeasurementCard` and `MetricPrimaryTile`. - Blood pressure gains the stat strip it was missing: two stacked systolic / diastolic strips, since a single-series strip would drop the diastolic half. `MetricStatStrip` takes an optional series caption. Update the spine guard to assert the target card precedes the assessment and that the retired range row and last-measurement card appear on no subpage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 80b2a810314b4c0951c0d68c04bffc89003005d6) |
||
|
|
496147687d |
refactor(insights): unify the assessment term, restore the canonical spine, consolidate status cards
The per-metric AI assessment was named four ways across the bundle (Einschätzung / Auswertung / Analyse / Bewertung in German, with parallel drift in the other locales). Sweep every assessment-state value — the no-provider, no-result, start / refresh / re-run, regenerate and warm strings — onto the one noun the card title already carries, in all six locales. Keys are unchanged; only the human strings unify. Restore the canonical metric-detail spine on the two bespoke pages that had drifted: the mood page rendered the assessment directly under the first chart with the target summary and breakdown sections below it, and the medications page rendered the therapy timeline after the assessment. Both now end on the assessment block, matching weight / bmi / pulse / blood-pressure and the generic scaffold. A source-level guard test asserts the assessment card is the last child of every bespoke page's shell so the order cannot silently regress again. Consolidate the repeated eight-prop assessment wiring: the five useInsightStatus-backed pages now mount a shared SlugInsightStatusCard seam (the sibling of MetricStatusCard for the generic route) instead of hand-wiring the card and the status?.x defaults inline. The medications page keeps its inline card because its route carries a richer summary-shaped envelope than the standard text-only generators. The assessment footer's "updated" caption now uses the shared relative-time formatter the briefing, hero, last-measurement and coach-history captions use, so adjacent cards no longer read one relative and one absolute timestamp. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 2cd73ce5d6cdca63fea0829990fac2a7e24afebf) |
||
|
|
02c50acd46 |
feat(insights): unify the derived-metric query hook onto one factory key
Two parallel loaders for the generic derived-metric route had grown — a components-local hook keyed on a 4-element tuple and a hooks/ hook keyed on a 3-or-4-element tuple. Both shared the ["insights","derived",metric,…] prefix with different tuple shapes, which silently poisons the TanStack cache between surfaces. Collapse to the single components-local hook (auth-gated, 8 s client ceiling, retry-free) on the one insightsDerived factory key; the fourth slot is always present so the metric prefix can no longer carry two shapes. |
||
|
|
95a2a7692f |
feat(insights): add composite scores, score-anatomy view, and correlation discovery
Build the composite tier of the derived-metrics layer on the shared seam: - Sleep score — a transparent 0–100 composite from sufficiency, efficiency, consistency, timing and composition sub-scores over the per-stage sleep rows, reweighting around any sub-score the data cannot support (a legacy asleep-only night drops composition gracefully rather than fabricating it). - Wellness/readiness index — extends the health-score null-redistribution blend with resting-HR, HRV (SDNN), sleep, respiratory-rate and mood deviation-from-baseline components; returns insufficient below two present components so it never headlines from one signal. - Coincident-deviation flag — counts vitals outside their personal band on the latest day and fires when two or more coincide, listing the contributing vitals as possible factors, never a cause or a diagnosis. Each composite cites its standard, carries coverage + confidence + provenance, and reweights around missing inputs. Add an FDR-controlled correlation discovery engine behind GET /api/insights/correlations: it scans a curated behaviour x outcome matrix, lag-joins each behaviour day to the next day's outcome, runs Pearson with the exact Student-t p-value, and applies Benjamini-Hochberg control across every tested pair so only statistically-defensible patterns surface — each tagged with n, r, p and the adjusted q, framed descriptive not causal. Add the score-anatomy detail view at /insights/scores/<metric>: a full score ring, ranked contributor rows each showing its push/pull and weight share, and the cited-standard provenance explainer — reused by every composite, mobile-first and accessible. Regenerate the OpenAPI contract for the new metric values + the correlation discovery response, and add the i18n keys across all six locales. |
||
|
|
32bc11f968 |
feat(insights): add the derived-metrics design-system primitives
Build the four reusable presentational components the v1.10 vitals and score-anatomy surfaces share: a radial score ring, a coverage meter, a sparkline delta tile, and a provenance explainer. Each extends the existing dracula band + trend-sentiment vocabulary rather than a new colour system, and ships its populated, provisional/empty, and stale states up front so a sparse-data surface never goes blank. - score-ring: centred tabular-nums number on a Recharts RadialBarChart, band-coloured, size prop (sm tile / md / lg anatomy), count-up + sweep on mount via a 0 KB requestAnimationFrame hook; role=img aria-label restates the number so the ring is never colour-only. - coverage-meter: CSS-only filled/hollow dots driven by the Derived<T> coverage + confidence facets, with an accessible breakdown tooltip (present vs required inputs, history days, missing list). - sparkline-delta-tile: extends the trend-card grammar with an inline Recharts area sparkline, a signed period delta, and an age/normal-range framing slot. - provenance-explainer: collapsible Popover (desktop) / bottom-sheet (mobile) rendering inputs, plain-language method, and an optional cited standard as React text children — no markdown library. All four read their data off the Derived<T> union as a type-only import so the client bundle stays free of the server graph. Recharts only, no new runtime dependency. New i18n keys propagated across all six locales. |
||
|
|
a87d6b94e4 |
fix(insights): keep the server-only rollup readers out of the insights client bundle
The v1.9.0 time-range work imported ANALYTICS_RANGES as a value into the time-range pills and the insights layout-prefs hook from range-delta.ts. That module also pulls the rollup readers and the Postgres client, so the value import dragged pg (dns/fs/net/tls) into the browser bundle and broke the production build with eight "Module not found" errors. Move the client-safe pieces — the range list, its type, the per-range window length, and the period-over-period result shapes — into a new range-shared module that imports nothing server-side. The insights client components import from there; range-delta.ts re-exports the same names so the server route and the OpenAPI registry keep their import paths. The window-composition logic and the rollup/db imports stay server-side. No runtime behaviour change — a pure module-boundary split. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 75124e76c1e987d8a022fb39d891049340199b38) |
||
|
|
d880385635 |
refactor(targets): retire the standalone Targets page
The standalone /targets (Zielwerte) page is retired. Target editing moved inline onto the Insights metric pages in v1.8.6 — each metric's reference panel mounts the editor and writes the same PUT /api/user/thresholds, reading the same /api/insights/targets payload. The page carried a deprecation notice and had already dropped out of the navigation, so removing it is a UI-only change: no data model, API route, or inline editor moves. Remove the page and its tests, the components reachable only from it (target card, per-card coach button, summary header, target prompt / scope helpers, coach-handoff hook), the now-dead /zielwerte→/targets proxy redirect, and the orphaned targets.* i18n keys across all six locales. Repoint the handful of code comments that pointed readers at the page to the inline Insights panel, and prune the two coach gate-inventory allowlists plus the queryKey factory guard root that named the removed files. The reference panel keeps the range bar, consistency strip, status pill, edit sheet, and source-link helpers — those are the live editing surface now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 7f842c5620087127e980fe22a56133135b7c0531) |
||
|
|
3694efe6e8 |
fix(insights): keep the assessment card polling while a stale text revalidates
The stale-while-revalidate path serves the last-good assessment on a cache miss and returns a terminal payload, so the status hook stopped polling. The worker warmed a fresh assessment out of band seconds later, but the open card never picked it up — the stale text stuck until the page remounted. Add a `revalidating` flag to the read-only miss outcome, set only when last-good text is served AND a refresh was enqueued (false on the suppressed-stub branch and when no prior text exists). Thread it through the generic metric-status generator and the seven specialised generators onto the wire, type it on the client, and make `nextStatusPollInterval` keep polling on `preparing || revalidating`. The existing 12-attempt ceiling bounds both signals, so a persistently failing generation still cannot poll forever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit c9217534fbdb304c4df1b6eb55fb19e4360b8a10) |
||
|
|
03925ddece |
feat(insights): time-range pills with period-over-period deltas on metric pages
Add a 7d / 30d / 90d / 1y segmented control to every HealthKit metric page, between the stat strip and the chart, with a sentiment-coloured "vs prior period" delta caption beside it. New route GET /api/analytics/range?type=<MeasurementType>&range=<7d|30d|90d|1y> returns the current-window aggregate, the previous comparable window, and the composed delta. Single metric type by construction, so the read is one rollup-tier call covering the trailing 2N days sliced into the two halves — no per-type fan-out, no Prisma-pool burst. Reuses the WMY readers (readBestGranularityRollups + aggregateWmyBuckets) and the two-window pattern the year-ago baseline already uses; count/min/max/mean/sum compose across buckets, SD/slope/r2 stay out (not composable). The cached summaries slice is untouched so the hottest dashboard cache key never fragments. The delta caption reuses the dashboard's metric-aware up-good/up-bad colour logic — extracted from trend-card into a shared trend-sentiment module — with the metric's good direction sourced from the metric-status registry (target-band metrics render neutral, matching deviation framing). When the prior window has no data the caption shows "no prior-period data" rather than a misleading 0%. The range choice persists across metrics via a localStorage seam and defaults to 30d so nothing regresses. Cache keyed per (type, range) through the query-key factory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit eac87a9f8b144f4d8e7a8b950528a19d5dcf8726) |
||
|
|
58b793ee4a |
fix(insights): correct the active-energy assessment id and share the status card
The active-energy page passed `ACTIVE_ENERGY_BURNED` as `statusMetric`, but that is the DB MeasurementType, not the registry id the metric-status route accepts. The route's closed enum only knows `ACTIVE_ENERGY`, so every fetch 422'd and the assessment card silently fell back to its empty body. Point the page at the `ACTIVE_ENERGY` registry id and type `statusMetric` (plus the `useInsightMetricStatus` metric param) to the closed `MetricStatusMetricId` union instead of a bare string, so a future MeasurementType-vs-registry-id mismatch is a compile error rather than a silent 422. A typecheck pass over all 29 pages confirms active-energy was the only diverging one. Extract a shared `MetricStatusCard` that owns the `useInsightMetricStatus` hook and the assessment-card wiring, collapsing the duplicated mount block in the HealthKit metric scaffold and the bespoke sleep page to a single line each (only the icon differs). Behaviour is unchanged: the card mounts only on the data-bearing branch, so a source-less account never fires an assessment round-trip. Add a test asserting the active-energy page wires an accepted registry id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 6e371aaeb9698f7b4251c22e1d8f381ddfa70a00) |
||
|
|
16b0c349f2 |
feat(insights): add generic per-metric assessment loader
Add a parameterised query key (insightsMetricStatus) and a useInsightMetricStatus hook that reads the generic per-metric assessment route GET /api/insights/metric-status?metric=<id>. The seven bespoke status routes keep their own keys and hook union; this one covers the HealthKit metric sub-pages that share a single route keyed by the metric id. The hook mirrors the existing status loader: 8 s client ceiling, no inline retry, and the bounded preparing poll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit a3407a2f6c1e9f54c92a6f182db894ae0593e69f) |
||
|
|
b99b07ad38 |
feat(profile): capture the insurer IK number
Add an optional German insurer institution number (IKNR, 9 digits) to the user profile, alongside the existing insurer name and KVNR. The IKNR is a public registry identifier for the institution, not a per-person quasi-identifier, so it is stored plaintext, matching insurer_name rather than the encrypted KVNR column. The profile GET echoes the field; PATCH/PUT accept it via Zod — optional, empty string maps to null, and a non-empty value must be exactly 9 digits (422 otherwise, no checksum). The write is built field-by-field. /api/user/profile gains an OpenAPI registration with the profile read/write schemas. Migration 0102 adds the nullable column, NULL-defaulted, forward-only. |
||
|
|
65cfa2b83a |
fix(insights): bound the status preparing-poll to a fixed ceiling
The per-metric status cards and the inline medication-compliance query poll the read-only status route every 4 s while the payload reports preparing:true. A configured provider whose generation persistently fails leaves preparing pinned true, so an open Insights page would poll indefinitely — steady battery and network draw on a screen the user has walked away from. Cap the poll at 12 attempts (~48 s) through a shared nextStatusPollInterval helper both call sites route through. Past the ceiling the card settles on its existing static / empty state. The happy path is unchanged: a generation that lands within a couple of intervals clears preparing and stops the timer exactly as before, and a terminal no-provider payload never carried preparing so it stays terminal. |
||
|
|
35800becbb |
fix(insights): make per-category status cards read-only so navigation never blocks on the LLM
Opening an Insights category page froze the whole UI: the per-metric status card fired a cold, uncapped LLM fetch on mount with no client abort, so the request hung up to the server's 60s ceiling and the pending/resolve re-renders thrashed the tab strip and ate touch events. Timeouts were never negatively cached, so every visit re-blocked. Fix the class holistically: - Bound every status GET on the client. The shared use-insight-status hook and the inline medication-compliance query now wrap fetch in an 8s AbortController and stop retrying inline; navigation can no longer await an uncapped round-trip. - Make the status routes read-only. On a cache miss the generator enqueues an out-of-band pg-boss job (insight-status-generate) and returns a preparing envelope; the client polls a bounded interval until the worker warms the cache. Models the dashboard snapshot's ready/preparing pattern. The queue is registered in the worker allQueues loop + a boss.work handler, with a guard test. - Fix the nightly warm-pass regression: warm every supported locale (the client reads against the active UI locale, not the persisted User.locale) and warm on a cached comprehensive outcome too (its write still evicts the per-status caches). Add a short-TTL negative cache on the timeout path so a transient provider stall no longer makes every subsequent visit re-block or re-enqueue. - Add loading.tsx for the insights segment + the root so any future RSC await streams a skeleton instead of blocking the transition. - Stabilise the advisor regenerate callback so a status-query flip can't re-render the memoised tab strip mid-gesture. New i18n key insights.assessmentPreparing across all six locales. |
||
|
|
c48541e74f |
feat(coach): back the chat source rail with persisted cluster prefs
The chat-side sources rail and the settings cog drove two different data-source models — the rail hardcoded the legacy five sources and kept its selection in ephemeral drawer state that reset on close, while the cog edited the ten persisted clusters on User.coachPrefsJson. The prompt scope followed the persisted clusters, so what the rail showed could differ from what the model received. Bind the rail to the same persisted prefs: it now reads useCoachPrefs and writes the cluster toggles + analysis window through a shared useSaveCoachPrefs mutation, so a toggle sticks and the cog reflects it (and vice-versa). The chat request no longer sends a scope override, so the route resolves the source set and window from coachPrefsJson and the rail display cannot drift from the prompt. Drop the ephemeral scope state, the duplicate header window pill, and the legacy five-source rail model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
50e0a65228 |
chore(release): v1.7.0
v1.7.0 — health-record export (PDF + FHIR), flexible medication schedules, first-paint dashboard snapshot, full HealthKit coverage, offline sync delta feed, Coach data clustering. See CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ca1c52ffcb |
feat(avatar): self-hosted avatar storage replacing the Gravatar leak
The previous /me payload carried a gravatarUrl built from
SHA-256(email) and pointed every authenticated page-load at
www.gravatar.com — Automattic could correlate the digest against
its known-email table on each request. This release moves the
avatar in-house: the user uploads through POST /api/user/avatar
(multipart, JPEG/PNG/WebP, 2 MiB / 2048×2048 max), the bytes ride
on the User row alongside a content-type and updatedAt timestamp,
and GET /api/user/avatar/{id} serves them from same-origin with
the cache-busting timestamp on the profile response.
The image-header parsing is hand-rolled in src/lib/avatar.ts so
the standalone image does not gain a native dep just to read width
and height. Validation rejects oversize / wrong content-type at
the multipart parse boundary; DELETE clears the row. The bytes
ride pg_dump alongside the rest of the row so the existing weekly
encrypted backup path already covers them.
The sidebar, the coach panel, the use-auth hook, and the disable-
coach card stop reading gravatarUrl; src/lib/gravatar.ts retires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f92e644154 |
chore(release): v1.4.49 — audit-backlog closure + server reminder suppression + diagnostic endpoint + analytics cold-path fix
Bundles the deferred v1.4.47 Mediums + Lows with the v1.4.48-deferred items into a single release. Headline: server-side suppression of MEDICATION_REMINDER APNs for iOS clients that own their local reminders (closes GH #206), a push_attempts table backing /api/admin/notifications/diagnostic, and a sweep of v1.4.48 QA forward-findings. Performance (the user-visible win): - /api/analytics cold-fallback path splits the heavy single-statement aggregate into two parallel queries. The linearly composable columns (count / min / max / mean) keep the full-partition scan; the windowed 7 / 30 / 90-day avg / slope / r² columns take a 90-day outer cap so the planner does an index range scan on (user_id, type, measured_at) instead of a full-partition sequential scan. Output is bit-identical; cold slim slice drops from multi-second to sub-second on tenants with large measurement partitions. Server-side notifications: - GET + PATCH /api/auth/me/notification-prefs with deep-merge semantics. Initial category medication.clientManaged: boolean (default false). Cron medicationReminderJob skips dose-due APNs when the flag is true, emitting a medication_reminder.suppressed_client_managed wide-event annotation per skip. - push_attempts table backs the diagnostic endpoint. Every APNS / Web-Push / Telegram / NTFY sender writes a fire-and-forget row. Daily 03:35 Europe/Berlin cleanup prunes rows older than 90 days. - /api/admin/notifications/diagnostic — admin-only endpoint surfacing device APNs token presence (masked), channel config presence, and recent push attempts. Registered in OpenAPI. - Per-channel APNs "Send test" button now fires with eventType: "MEDICATION_REMINDER" so the dispatcher's time-sensitive + priority-10 branch runs. A successful self-test now exercises the exact path production reminders take; an unsuccessful one points the user at iOS Settings → Time Sensitive Notifications rather than appearing to be a server-side bug. Withings reliability: - OAuth callback consumes the nonce atomically (delete-first with P2025 catch for the replay branch). The previous read-then-delete pattern allowed a race between two concurrent callbacks. - ?reason=state redirect differentiated into csrf1 / replay / expired / cross_user so operators can tell mismatch types apart without DB shell access. - connect endpoint rate-limited to 10/60s/user; row-create failures redirect to a consistent error surface. Observability hardening: - redactSensitiveFields(body) denylist helper covers password / token / secret / apiKey / authorization / csrfState / nonce, recursive, case-insensitive. Caught: /api/devices invalid_format issues were echoing apnsToken values verbatim into the audit log. - sanitiseZodIssues(issues, { stripValuesFromMessage: true }) overload applied to 14 audit-log sites. Migrations: 0079 (notification_prefs), 0080 (push_attempts). Tests: 4815 → 5272 unit + integration. Workout-batch integration suite that had been red on main since v1.4.45 (three release commits shipped through it) now green again — updated to match v1.4.42 cross-source dedup semantics. Resolves PR #207. |
||
|
|
66a7f8b6fa |
chore(release): v1.4.47 — audit-backlog closure + drag-to-reorder + Coach disable + OAuth nonce ledger
Eight changes: W0 idempotency test pin, W1 legacy column drop, W2 Textarea primitive, W3 Coach disable toggle, W4 dnd-kit drag-to-reorder, W5 onboarding 24h gate, W6 Withings OAuth nonce ledger, W7+W8 legacy_form_total drop + Coach onLine pre-check. 6-axis QA reconciled. Migrations 0076/0077/0078 required pre-image. Tests 5093 → ~5170. |
||
|
|
9b6e237f5f |
chore(release): v1.4.42 — Knip enforcing, queryKey factory closed, iOS Workouts dedup, Withings off-response classification
Six touch-disjoint waves landed on develop before this release: - W1-KNIP — exports/types tiers flipped to enforcing-mode on main; 35 unused exports + 52 unused types triaged to 0/0; shadcn surface + zod-infer contract types scoped via knip.json ignore-block. - W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper + `/api/dashboard/widgets` 422 now returns every Zod issue + worker audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43. - W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files migrated to the queryKeys factory; 9 new factory entries; ESLint rule + test-guard scope extended in lockstep. - W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine sites (seven status helpers + bucket-series + bp-in-target); Suspense double-comment consolidated; doctor-report sanitiser regex rewritten to escape-form so diffs are reviewable; pr-detection- worker soft-delete filter; offhost-backup DR-intent comment. - W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard tile-strip placeholder parity with the live TrendCard chrome. - W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier (transient/reauth/persistent) + WithingsApiError typed throw + the `pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2 silent-disable gap the wave was conceived to close. No migration, no env-var change, no API contract break for iOS v0.5.4. The new `/api/dashboard/widgets` 422 envelope is additively extended. pnpm test --run green at 4815 passing / 1 skipped. pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green. |
||
|
|
1114cf7e63 |
chore(release): v1.4.40 — comprehensive Critical+High architecture closure plus iOS PB30 enablement
Eleven touch-disjoint waves landed on develop plus a six-axis QA reconcile round before tagging. See CHANGELOG.md for the user-facing prose; this commit message documents the wave + report inventory for release-management traceability. Waves shipped (each with a phase report under .planning/): - phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the v1.4.39 empirical-trace finding #1 (Prisma pool starvation). - phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3 integration contracts close audit Critical Finding #3 (soft-delete invisibility) across dashboard, analytics, summaries, rollup tiers, and the Coach snapshot path. - phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three /api/insights/{features,targets,cards} routes plus the compliance-rollup hook gap closure for bulk-projection mints; the post-QA reconcile patch closes the six remaining measurement.findMany sites in the cluster that still leaked tombstoned rows. - phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation under src/lib/rollups/* with zero orphan re-exports; lint, typecheck, knip, and 93/93 rollup tests green post-restructure. - phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the queryKey factory-bypass walker test (extended to comparison/ in the reconcile pass). - phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17 paired-section tests pinning the 9 SB-3 disclosure requirements. - phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity assertions guarding against split rotation. - phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER time-sensitive APNs payload (priority 10 + interruption-level) scoped via a parameterised test that pins all 6 other event-types do not bypass Focus. AP-2 .p8 install gates real delivery. - phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with append-only invariant pinned in test; revoke-then-regrant leaves both rows; idempotent DELETE returns 200 + receipt: null. - phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers (week/month/year) wired through summaries-slice and health-score-fast-path with a linear-composability parity test pinning the cross-consumer routing contract. - phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation eliminates a sibling-drift risk; knip whitelist trimmed to three files pending follow-up. QA reconcile patches applied per the v1.4.40 QA round (six reports under .planning/round-v1440-QA-*-findings.md): - senior C1 — dashboard-suspense-boundaries regex aligned with the post-W-INFRA Thread 2 userTimezone-local lift. - senior H1 — soft-delete filter added to the six remaining insights measurement.findMany sites. - specialist H1 — Promise.allSettled around bulk-projection compliance recompute on both /api/medications/intake and /api/dashboard/summary. - ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets factory; walker test extended to src/components/comparison/. - security M1 — consent artefact 64 KB cap enforced via Buffer.byteLength (UTF-8 bytes, not UTF-16 code units). Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip --include files,dependencies,binaries,unlisted, pnpm test --run (4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over the v1.4.39 -> v1.4.40 span. User-facing prose: see CHANGELOG.md. |
||
|
|
7bef199039 |
v1.4.33 — polish and reliability
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path. Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container. Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app. The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces. |
||
|
|
c7a0ff214b |
chore(release): v1.4.32
v1.4.32 — HealthKit Tier 1 wave A: workouts API + list/detail pages + dashboard tile + 5 metric sub-pages; HRV/RESTING_HR moved to cardiovascular; latent workouts-list 500 fixed. |
||
|
|
5ea813e20c |
chore(release): v1.4.31
Operator-side assistant feature-flag matrix, /insights tab-strip blocking fix on mobile, and the GHCR-propagation-race Coolify auto-deploy fix. Full notes in CHANGELOG. |
||
|
|
d76c0b56e7 |
chore(release): v1.4.29
A targeted performance + polish patch. The headline is a faster dashboard on data-rich accounts: the pulse chart drops up to ~5 000 raw rows per range tab in favour of one row per day, the duplicate `/api/dashboard/widgets` fetch on every mount collapses to one, and three long-running analytics reads are bounded to the trailing window the tile path actually needs. Two production regressions close — the `aggregate=daily|weekly|monthly` path on `/api/measurements` 500'd for every grain, and the same path averaged cumulative step counts instead of summing. |
||
|
|
2d111fcb24 |
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). |
||
|
|
4f09c3f862 |
chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup. ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop) applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer (graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line release notes attached to the GitHub Release. |
||
|
|
17b8d8d2c2 |
chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites). See CHANGELOG.md for the full per-section delta. |
||
|
|
dfffe6bb3b |
refactor: simplify batch (S-02 to S-04)
Three small reuse wins from the W6 simplify review: - openapi/registry.ts swaps the lazy require() for a static import. The ESLint disable + as-cast carried no real benefit; the only callers always loaded the route table anyway. - Extract useCoachPrefs() into a shared hook so the settings sheet and message thread share one queryKey + one fallback shape. Removes the drift between the two existing inline blocks. - Replace the 9-deep wantsX boolean ladder in coach snapshot with a single CoachScopeSource -> MeasurementType[] map. Drives both the WHERE type IN (...) build and the appleHealthBlocks gate, so adding a new metric is one entry instead of three parallel edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5a8ad3d895 |
fix(charts): drop the redundant resize sync from useViewportWidth
The useState lazy initialiser already runs on the client during hydration and reads the live `window.innerWidth`, so the post-mount handler() call was a no-op that triggered a setState inside useEffect. The eslint react-hooks/set-state-in-effect rule flags this pattern; removing it lets the resize + orientationchange listeners alone drive updates without a hydration-time double set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
77a3ad3bc8 |
fix(charts): mobile header layout + unified x-axis tick density
Pixel-5 audit against PROD (393 px) showed two issues with the
chart cards on /dashboard and /insights:
1. The bucket-aggregation chip ("Wochendurchschnitt" / "Weekly avg"),
the optional comparison-period chip, the four range tabs and the
cog dropdown all sat on a single justify-between row in the chart
header. On viewports below the sm breakpoint the chips ate enough
horizontal space that the tabs wrapped to a second row inside the
same flex container, doubling the header height. On Galaxy Fold
compact (280 px) the tabs split across three or four rows and the
medication card overflowed horizontally.
2. The medication compliance chart drew one tick per data point on
the x-axis. A 30-day Pixel-5 window painted thirty overlapping
labels into a 393 px gutter while the weight / BMI charts looked
sparser by accident, so the chart row read inconsistently.
The header is now stacked into two rows below the sm breakpoint:
title + chips on row 1, range tabs + cog right-aligned on row 2 with
flex-nowrap so the tabs always fit. The decorative bucket-aggregation
chip and comparison captions hide on mobile (the range tabs already
communicate the visible window). Above sm the original side-by-side
layout is preserved.
A new helper at src/lib/charts/x-axis-density.ts caps visible ticks
at 4 (≤360 px Fold), 6 (≤480 px Pixel-5 / iPhone-12), 8 (small tablet)
and 10 (desktop) and is wired into HealthChart, MoodChart,
MedicationComplianceChart and ComplianceLineChart so every chart row
reads the same density. A new useViewportWidth hook keeps it reactive
without an SSR penalty (desktop default during server render, real
measurement after mount).
The scatter-correlation chart was deliberately left alone — its
x-axis is numeric (BP / weight values, not a time series) and already
supplies an explicit `ticks` array.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3048dd6637 |
style(prettier): sweep formatting across the tree
Pre-existing format drift accumulated across v1.4.x; running prettier brings the whole tree back to baseline so future diffs read clean. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
720e6c89b9 |
refactor(v1.4.18): apply simplify-review safe suggestions
Seven mechanical cleanups, no behaviour changes: - Drop the duplicate ChartOverlayPrefs type + DEFAULT in chart-overlay-controls; re-export the canonical pair from dashboard-layout so a future fourth toggle can't drift between the UI and persistence layers. - useChartOverlayPrefs accepts null/undefined chartKey and short- circuits the dashboard-layout fetch when none is supplied. Drops the chartKey ?? "bp" fallback and the three discard-the-result ternaries in health-chart; mini-mode renders no longer subscribe to the BP chart's persisted prefs. - /api/analytics no longer pair-and-counts BP twice — the windowed helper already produces last30Days, so the headline reads it back instead of running computeBpInTargetPct on the same input. - Drop the redundant moodEntries.map shape in the achievements route; the prisma select already returns the matching shape. - Merge the five hidden-metric cases into the no-precondition block of isEarnable; the defensive comment admitted they couldn't fire. - Remove the deprecated getAchievementCategory wrapper (zero callers). - Strip six "v1.4.18 gradient removed" what-comments scattered across the chart wrappers; the polish tests pin the absence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d70a9b1ee9 |
feat(charts): persist per-chart overlay prefs (default off; clean line)
Hook the overlay-controls dropdown into a TanStack Query mutation so
each chart's three toggle states (7-day trend / Trend arrow / Target
range) save per user and survive reloads. The prefs piggy-back on the
existing User.dashboardWidgetsJson blob with a `chartOverlayPrefs`
keyed by chart id, mirroring the v1.4.16 phase B8 comparison-baseline
pattern — no Prisma migration needed.
Wires up:
- GET /api/dashboard/widgets surfaces the resolved prefs in its
response so a fresh dashboard load knows which overlays to paint.
- PUT /api/dashboard/chart-overlay-prefs replaces a single chart's
prefs without round-tripping the whole layout.
- Existing PUT /api/dashboard/widgets preserves any chartOverlayPrefs
the client doesn't send, so saving widget visibility doesn't wipe
chart prefs.
- useChartOverlayPrefs hook reads + optimistically updates the cache.
- HealthChart, MoodChart, MedicationComplianceChart all consume the
hook, render the controls dropdown next to the range tabs, and gate
their three overlays behind the persisted toggles.
Default state for every chart, every flag: false. The clean line is
the new default; users opt into overlays they care about per chart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8215e25876 |
feat(onboarding): auto-launch tour for new users on first dashboard load
v1.4.15 Phase B5 — third commit. Wires the spotlight tour
(`<OnboardingTour>` from previous commit) to auto-fire for new users
the first time they land on the dashboard, persists completion to
the DB, and exposes a window-event hook for replay.
Pieces:
- `<TourLauncher>` (`src/components/onboarding/tour-launcher.tsx`)
— gates and mounts the tour. Self-decides via:
* `user.onboardingTourCompleted` (DB flag from migration 0030)
* sessionStorage dismiss guard (defensive against in-flight
`/api/auth/me` refetches re-opening the tour after Skip)
* post-wizard 1500ms grace window (avoids the wizard's
`doneToast` and the spotlight stacking on the same z-layer)
* a `ready` prop the dashboard sets once analytics has
resolved (so the spotlight cutout doesn't snap to a 0×0
placeholder before tiles render)
Listens for `healthlog:tour-restart` to force-reopen.
- `POST /api/onboarding/tour` — single endpoint with body
`{ completed: boolean, outcome?: "completed" | "skipped" }`.
Flips `users.onboarding_tour_completed`. Dispatches a Wide
Event annotation with the outcome so Loki can split completion
from dismiss without a second column.
- `/api/auth/me` now returns `onboardingTourCompleted`. The
`AuthUser` type in `src/hooks/use-auth.ts` gains the field;
no consumer outside the launcher reads it today.
- `data-tour-id` attributes added to:
* dashboard tile-strip wrapper (`dashboard-tile-strip`)
* dashboard "Add" dropdown trigger (`dashboard-quick-add`)
* sidebar nav items (`nav-insights`, `nav-settings`,
`nav-achievements`, plus the rest for symmetry —
targeted attribute, not class names, so renames in
Tailwind classes don't silently break the spotlight)
- The `/onboarding` wizard's `persistAndExit()` writes
`sessionStorage["healthlog-tour-referrer"] = "1"` immediately
before navigating to `/`. The launcher consumes-and-clears that
on first mount via a `useState` lazy initialiser (presence-only
flag — no `Date.now()` so the render-phase decision stays pure
under `react-hooks/purity`).
ESLint compliance:
- `react-hooks/set-state-in-effect` — no synchronous setState in
a useEffect body. The launch decision happens in the render
phase using the React-recommended "previous input id" guard
(mirrors `account-section.tsx`'s `seededUserId`), and the
post-wizard timer's setState lives inside a setTimeout
callback (an event handler, not the effect body).
- `react-hooks/purity` — render-phase setState only branches on
prop values + sessionStorage-presence helpers. No `Date.now()`
/ `Math.random()` / fetch in render.
Tests: my 39 unit tests pass (was 27 — +12 across tour-state and
tour SSR shape). Pre-existing test failures in
`doctor-report-data.test.ts` are uncommitted B6 work in the
working tree, not B5. Lint clean for B5 files; one pre-existing
B6 lint error in `doctor-report-dialog.tsx` is out of scope.
Typecheck has the same pre-existing `dashboard-layout.test.ts`
errors documented in STATE.md (A4 / phase-D scope).
Restart UX (Settings → Account → "Restart tour") lands in the next
commit (B5-4) so this commit stays orthogonal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8533755d8d |
release(v1.2): i18n, thresholds, glucose, dashboard customization, feedback, multi-provider AI (#77)
* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX Addresses bugs surfaced in issues #73 and #74 plus drift found during audit. - i18n: add 21 missing translation keys (targets.* namespace was only partially populated after the English-first migration; common.reset and settings.configured were referenced but absent), plus common helpers (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError. - charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P" to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE. - queries: add src/lib/query-keys.ts as a single source of truth with measurement/mood/medication-dependent key sets. Wire them into measurement-form/list, mood-form/list, medication-form/card, intake-history-list, intake-timeline, medications page, and Withings sync in settings. Fixes dashboard not auto-refreshing after adding a measurement (old code only invalidated ["measurements"], not ["analytics"] / ["insights"] / ["insights","targets"]). - admin: useUpdateSettings + updateUser mutations now surface real error messages via sonner toasts and invoke getApiErrorMessage() so saves fail loudly instead of silently. Success also gets a toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(i18n): package B — locale-aware formatters throughout Removes the hard-coded de-DE locale that lingered from the German-first era and makes every number, date, and time locale-aware. English stays the default per user preference; German remains selectable. New: - src/lib/format-locale.ts exposes makeFormatters(locale) + a cached Europe/Berlin timezone. Covers number, integer, percent, date, dateShort, dateTime, time, dateWithWeekday, monthShort. Also exports parseLocaleFromAcceptLanguage() for server code. - useFormatters() hook in @/lib/i18n/context resolves from the active locale, memoized per render. Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") / Intl.DateTimeFormat("de-DE") call sites): - src/app/page.tsx (dashboard) - src/app/insights/page.tsx - src/app/admin/page.tsx - src/app/settings/page.tsx (Withings last-sync, doctor-report caller) - src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels) - src/components/charts/health-chart.tsx - src/components/medications/medication-card.tsx - src/components/medications/medication-form.tsx - src/components/measurements/measurement-list.tsx - src/components/mood/mood-list.tsx - src/components/insights/insight-status-card.tsx (also removed 5 German hard-coded strings with TODO: i18n markers — now translated) - src/components/insights/insight-advisor-card.tsx (same — 10+ strings) Server: reminder-check / reminder-worker / umami-test switched from "de-DE" to neutral en-GB/en-US where the string is machine-compared rather than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now — the repo is English-speaking. Next.js metadata: - OpenGraph locale from "de_DE" → "en_US" with "de_DE" as alternateLocale. - Root layout now reads healthlog-locale cookie (falls back to Accept-Language) and sets <html lang> server-side, avoiding an EN-flashes-then-DE hydration gap and fixing SEO/a11y. - I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next SSR pass is correct. Doctor-report PDF: - generateDoctorReportPDF(data, { t, locale }) now accepts injected translator + locale. All section titles, column headers, BMI / BP classifications, mood labels, and the footer disclaimer are i18n-driven. File name slug switches between gesundheitsbericht / health-report. - 40+ new doctorReport.* keys in EN + DE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ux): package C — typed passkey errors, bug-report gating, error boundary Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste error reporting requested during review. Passkey registration / login (#73.1): - src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code to specific i18n keys instead of swallowing every failure as "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/ Security/Abort errors and falls back to a diagnostic message. - settings passkey + login passkey flows both use describePasskeyError now, so origin mismatches and previous-registration collisions show actionable copy. - 5 new passkey.* i18n keys in EN + DE. Bug report (#73.5): - New GET /api/bugreport/status reports {configured, hasToken, hasRepo, isAdmin}. - /bugreport page now queries status first. When not configured it shows a notice (different copy for admins vs. regular users) with a direct link to the admin panel. Form is hidden, not silently broken. - When POST /api/bugreport can't create issues it returns 503 instead of 500 with the same user-facing message. - 4 new bugreport.* i18n keys. Global error UX: - src/components/error-details.tsx — reusable panel with retry + copy diagnostics (message, digest, stack (trimmed), URL, user-agent, request-id, locale, timestamp) + report-issue link. Clipboard fallback prompt covers insecure-context cases. - src/app/error.tsx uses it. - src/app/global-error.tsx is provider-free (no i18n dependency) since it replaces the root layout on catastrophic failures — hardcoded English + Dracula palette so it still looks like HealthLog. - 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails, retry, reportIssue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))` verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and adopts it everywhere, including replacing the local `invalidateMedicationRelated` helper that medication-form.tsx had grown. Also fixes the import-sandwich that helper introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: small simplifications in i18n/UX changes - passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow - doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention - dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place - health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): capture upstream response body in OpenAI errors A production insights burst at 12:19 UTC failed with "OpenAI request failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed via logs), but we had no way to tell from logs whether it was an OpenAI incident, a model-not-found, or a response_format rejection because the response body was thrown away. Now the error message includes the first 500 chars of the response body and structured fields (httpStatus, upstream, model) that Wide Event logging can attach. No behavior change for the happy path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address code-review + security findings Review round produced five real issues — fix them all, plus three nits that were cheap enough to bundle. Must-fix: - security: error-details.tsx + global-error.tsx were copying window.location.href into the clipboard, which on OAuth callback routes (/api/withings/*, password reset) would leak code/state tokens into GitHub issues. Strip to pathname + searchParamCount. - fe: query-keys.invalidateKeys now uses Promise.allSettled, so a transient invalidate failure doesn't abort the chain and no unhandled rejection leaks from void invalidateKeys(...) call sites. - fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers() in try/catch — a dynamic-server error in the root layout now degrades to the "en" default instead of crashing into global-error. - be: reminder-worker + reminder-check parseTimeToMinutes normalize "24:00" → "00:00" (some ICU builds emit that for midnight under en-GB). Prevents a 1440-minute comparison against schedule windows. - security: openai-client error body excerpt redacts sk-* / Bearer tokens before logging, and moves the excerpt off Error.message into a structured field (bodyExcerpt) so log transports can filter it. - i18n: /api/withings/connect German error message → English. - i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...) Hardening: - /api/bugreport/status collapses to {configured, isAdmin} and adds a 30/min rate limit per user. Client interface updated. - i18n/context.tsx: I18nProvider's initializer now reads the cookie (matches SSR) before falling back to localStorage or browser detection — eliminates the locale-flash hydration class. - format.ts legacy helpers also read cookie first, matching SSR. Tests: - new src/lib/__tests__/format-locale.test.ts — 9 cases covering makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale - new passkey-errors.test.ts — 8 cases covering every DOMException name and SimpleWebAuthn code we map - new query-keys.test.ts — bundles + invalidateKeys allSettled semantics (incl. partial-failure test) 118 → 142 tests. All pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(v1.2): thresholds, glucose, dashboard customization, built-in feedback, multi-provider AI (#76) * build(deps): bump @prisma/adapter-pg from 7.6.0 to 7.7.0 (#60) Bumps [@prisma/adapter-pg](https://github.com/prisma/prisma/tree/HEAD/packages/adapter-pg) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/adapter-pg) --- updated-dependencies: - dependency-name: "@prisma/adapter-pg" dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump prisma from 7.6.0 to 7.7.0 (#61) Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/cli) --- updated-dependencies: - dependency-name: prisma dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump eslint-config-next from 16.2.2 to 16.2.3 (#62) Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.2.2 to 16.2.3. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v16.2.3/packages/eslint-config-next) --- updated-dependencies: - dependency-name: eslint-config-next dependency-version: 16.2.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump next from 16.2.2 to 16.2.3 (#65) Bumps [next](https://github.com/vercel/next.js) from 16.2.2 to 16.2.3. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v16.2.2...v16.2.3) --- updated-dependencies: - dependency-name: next dependency-version: 16.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @types/node from 25.5.2 to 25.6.0 (#66) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 25.6.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.6.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump dotenv from 17.4.0 to 17.4.1 (#67) Bumps [dotenv](https://github.com/motdotla/dotenv) from 17.4.0 to 17.4.1. - [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/motdotla/dotenv/compare/v17.4.0...v17.4.1) --- updated-dependencies: - dependency-name: dotenv dependency-version: 17.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @tanstack/react-query from 5.96.2 to 5.99.0 (#70) Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.2 to 5.99.0. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.99.0/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.99.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump typescript from 5.9.3 to 6.0.2 (#64) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2) --- updated-dependencies: - dependency-name: typescript dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump prettier from 3.8.1 to 3.8.2 (#68) Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2) --- updated-dependencies: - dependency-name: prettier dependency-version: 3.8.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump react from 19.2.4 to 19.2.5 (#63) Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.4 to 19.2.5. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react) --- updated-dependencies: - dependency-name: react dependency-version: 19.2.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump vitest from 4.1.2 to 4.1.4 (#69) Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.2 to 4.1.4. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @prisma/client from 7.6.0 to 7.7.0 (#71) Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 7.6.0 to 7.7.0. - [Release notes](https://github.com/prisma/prisma/releases) - [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/client) --- updated-dependencies: - dependency-name: "@prisma/client" dependency-version: 7.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump shadcn from 4.1.2 to 4.2.0 (#72) Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.1.2 to 4.2.0. - [Release notes](https://github.com/shadcn-ui/ui/releases) - [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md) - [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.2.0/packages/shadcn) --- updated-dependencies: - dependency-name: shadcn dependency-version: 4.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(thresholds): foundation + settings UI (v1.2) Introduces per-user health threshold overrides with an auto-default / user-override pattern. Default computation stays evidence-based (WHO, ESC/ESH 2018, AHA, AASM, ADA 2024, DGIM/DDG S3). Overrides live in User.thresholdsJson; any missing metric falls through to the default so new users need zero configuration. Schema (migration 0021): - New enums: glucose_context (FASTING/POSTPRANDIAL/RANDOM/BEDTIME), feedback_category, feedback_status. - measurement_type gains BLOOD_GLUCOSE; Measurement gets optional glucose_context (enforced via CHECK constraint — only set for glucose). - User gains thresholds_json, dashboard_widgets_json, glucose_unit, ai_provider, ai_model, ai_base_url, ai_anthropic_key_encrypted, ai_local_key_encrypted (stubs for the upcoming features). - New Feedback table for the built-in feedback system (next commit). Backend: - src/lib/analytics/effective-range.ts is the single source of truth: getEffectiveRange(metric, profile, overrides) returns the resolved range + the pristine default + physiological bounds. Every chart, band, insight prompt, and doctor-report section should call this. - Supports 11 metrics incl. four glucose contexts with ADA/DGIM ranges. - src/lib/validations/thresholds.ts — Zod schema with per-metric bounds and min<max refinement. - GET/PUT/DELETE /api/user/thresholds with full audit logging (before/ after) so medical-ish data mutations are traceable. UI: - src/components/settings/thresholds-section.tsx — clean per-metric row with Auto/Override Switch, inline min/max, reset-per-row + reset-all, and a "this looks outside guideline ranges" warning banner that only shows when the override is >30% off the default. - New Personalization section in /settings. - 25 new i18n keys (EN + DE) in a `thresholds.*` namespace. Tests: 8 new cases covering default computation, override application, missing profile gracefully returning null, glucose-context defaults, and per-metric bounds. 150/150 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(glucose): wire BLOOD_GLUCOSE + context through the write path Extends measurement validation and the POST /api/measurements route to accept blood-glucose samples with a required `glucoseContext` (FASTING / POSTPRANDIAL / RANDOM / BEDTIME). - Zod schema adds BLOOD_GLUCOSE to the type enum and glucoseContextEnum. - refine(): BLOOD_GLUCOSE requires a context; every other type must not set one — mirrored by the Postgres CHECK constraint from migration 0021 so callers bypassing validation still can't write inconsistent rows. - VALUE_RANGES gains BLOOD_GLUCOSE (20-800 mg/dL) — covers severe hypo through severe hyperglycemia. - getUnitForType(BLOOD_GLUCOSE) returns "mg/dL" (canonical storage). Display in mmol/L is a UI-level conversion per user preference. - Error messages translated to English (removed two lingering German refine messages while here). Measurement form + charts + targets UI follow in the next commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(i18n): localize AI insight prompts (en/de) Each prompt builder (base-system, blood-pressure, bmi, general-status, medication-compliance, mood, pulse, weight) now accepts a Locale and returns the system + user prompt either fully in English or fully in German. Stable contract keys (classification enum optimal/gut/grenzwertig /erhoht/kritisch, confidence niedrig/mittel/hoch) are preserved verbatim so the response parser keeps working — only the natural-language sections are translated. The prompt explicitly tells the model to emit classificationLabel / summary / recommendations in the user's language. Wires the locale through every status generator (general / blood-pressure / bmi / weight / pulse / mood / medication-compliance) into the prompt calls and adds a small server-side resolver (src/lib/i18n/server-locale.ts: explicit override > healthlog-locale or userLocale cookie > User.locale > Accept-Language header > default English) used by all /api/insights/*-status routes plus /api/insights/generate. The auditLog cache key already contains the locale suffix, so cached entries stay disjoint per language. The reminder worker continues to fall back to "de" when User.locale is null, matching its prior behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(glucose): UI + unit conversion helpers - src/lib/glucose.ts — mg/dL ↔ mmol/L conversion (factor 18.0182 per DGIM/DDG), resolveGlucoseUnit() defaulting to mg/dL, thresholdMetricForContext() mapping each context to the matching effective-range key. - Measurement form gains a "Blood glucose" option (mg/dL) with a context selector (fasting / postprandial / random / bedtime). The glucoseContext field is only sent for BLOOD_GLUCOSE — the Zod refinement and the Postgres CHECK constraint both enforce that. - 10 new i18n keys per locale (measurements.typeBloodGlucose, glucoseContext*, glucoseUnit, glucoseUnitHint). - 6 new tests covering conversion roundtrip, unit resolution defaults, and context → threshold-metric mapping. Charts, target cards, and doctor-PDF row for glucose follow in the next commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(feedback): built-in feedback system (no GitHub required) Implements the feedback-system design from the v1.2 spec. The submit button on /bugreport now always works — even when no admin has configured a GitHub repository. Feedback is stored in the Feedback table (migration 0021) with an admin inbox for triage. Endpoints: - POST /api/feedback — user submits. Auth-gated, rate-limited 5/hour. Category (BUG / FEATURE_REQUEST / QUESTION / OTHER), subject, description, optional screenshot (≤5 MB, base64), optional metadata (locale, URL path, userAgent). - GET /api/feedback — user's own history (paginated). - GET /api/admin/feedback — admin inbox with status filter and counts-by-status for tab badges. - PATCH /api/admin/feedback/[id] — update status / admin note. - DELETE /api/admin/feedback/[id] — soft-archive. Every mutation writes to AuditLog with before/after details. UI: - /bugreport rewritten: category dropdown, optional subject, always submits. Informational banner explains whether the server has GitHub escalation available or keeps feedback internal-only. - Legacy "not configured" blocker is gone — the form is never hidden. - 8 new bugreport.* i18n keys per locale. The old POST /api/bugreport route and /api/bugreport/status endpoint are left intact for backwards compatibility; they'll be removed in v1.3 once no one depends on them. Admin inbox UI lands in the next commit alongside the dashboard and multi-provider work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(telegram): localize bot UI per User.locale The Telegram webhook now resolves the user's locale (User.locale, falling back to "de" so existing bots stay German) and runs every reply, inline- keyboard label and callback toast through a small server-side translator. Adds src/lib/i18n/server-translator.ts (getServerTranslator) — reads the same messages/{en,de}.json bundles the client uses, with the same "missing key falls back to English, then to the raw key" semantics, and the same {param} interpolation. Importing the client useTranslations() hook into a server route would have pulled React in, so this mirrors just the parts the bot needs. Adds the telegram.* namespace to messages/en.json and messages/de.json covering: button labels, callback toasts, the /help body, /add prompt, medication-not-found / inactive errors, snooze/skip/confirm confirmations and the cancel button. The "genommen" / "taken" intake keyword and the "hi/hallo/hey/moin/hello" greeting list are accepted regardless of UI locale to keep backwards compatibility for existing users. Also adds 6 vitest cases for getServerTranslator (basic resolution, locale switching, parameter interpolation, English fallback, raw-key fallback, exposed locale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashboard): user-customizable widget layout Implements the dashboard customization spec: users choose which widgets appear on / and in what order. Auto-default keeps the current v1.1 behavior so nothing changes without opt-in. Backend: - src/lib/dashboard-layout.ts — single source of truth with DEFAULT_DASHBOARD_LAYOUT (10 widgets incl. the new sleep/steps/glucose ones that were previously hard-conditional), a version field for future schema evolution, and resolveDashboardLayout() that merges persisted state with defaults so new widgets auto-appear (invisible by default) when the schema bumps. - GET / PUT / DELETE /api/dashboard/widgets with strict Zod validation (version literal, widget-id enum, order range). - Persisted in User.dashboardWidgetsJson (column added in 0021). UI: - src/components/settings/dashboard-layout-section.tsx — mounted under the new Personalization section alongside Thresholds. Local edit buffer so toggling/reordering doesn't fire a network call per click; single "Save" commits. Each widget has Up/Down buttons + visibility toggle, plus a reset-to-defaults button at the section header. - 11 new dashboard.* i18n keys per locale. Dashboard page integration is staged — widgets currently still render per the old data-conditional rules. Wiring the saved layout through /app/page.tsx is a follow-up so this commit stays focused on the plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(auth): locale-aware password-strength feedback Completes the zxcvbn i18n migration by wiring the new getZxcvbnTranslations(locale) helper into the client PasswordStrength component and removing the obsolete zxcvbn-de helper. - src/components/ui/password-strength.tsx reads the active locale from useTranslations() and memoizes the translator per locale — so changing language instantly retranslates open warnings/suggestions. - src/lib/zxcvbn-de.ts deleted. Every consumer now routes through zxcvbn-i18n.ts which carries both EN and DE mappings. - server-side checkPasswordStrength (already refactored earlier in this session) continues to accept locale; clients can pass it from the session. No test impact — 162 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dashboard): replace setState-in-effect with draft-overlay pattern The dashboard-layout-section.tsx initial implementation synced server state into local state via useEffect, which trips the project's strict react-hooks/set-state-in-effect rule. Rewrote using a nullable draft pattern: `layout` is derived as `draft ?? remote`, and only user edits set the draft. Save/Reset clear the draft so the server copy wins again. Cancel also clears instead of re-seeding from server. No behavior change, no new tests — the component contract stays the same. Lint back to baseline (2 errors / 7 warnings, both pre-existing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: v1.2 README + CHANGELOG (thresholds, glucose, dashboard, feedback, multi-provider AI) - CHANGELOG 1.2.0 entry covering 5 new features + locale polish - README Key Features expanded: glucose, custom thresholds, dashboard customization, multi-provider AI, built-in feedback - Doctor Report description updated for locale-aware output Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(targets): glucose cards per context with effective range + unit conversion Render one target card per logged glucose context (FASTING, POSTPRANDIAL, RANDOM, BEDTIME) on /targets, sourcing the threshold band from getEffectiveRange() so user overrides apply consistently. The targets API returns glucose values canonical mg/dL; the page converts to the user's glucoseUnit preference via convertGlucose() before display. - Extend /api/insights/targets to include per-context glucose summaries (latest, 30-day average, range, classification) and surface glucoseUnit in the profile envelope. - Expose glucoseUnit on /api/auth/me and AuthUser type. - Targets page resolves the i18n label key, applies unit conversion to current/avg/range, and uses Droplet iconography. - Add targets.glucoseFasting/Postprandial/Random/Bedtime keys to en + de. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): address H1 (CSP nonce), H3 (SSRF), M1+M2 (XSS) From the v1.2 full-repo security audit: - **H1 / CWE-79** src/proxy.ts: CSP nonce was base64 of the UUID *string* form, which gives ~122 bits and a structured pattern (hyphens at fixed offsets). Replaced with 16 random bytes → proper 128-bit nonce. - **H3 / CWE-918** src/app/api/admin/ai-settings/route.ts: the admin AI base URL only required "https://". A compromised admin cookie could point it at attacker.example and exfiltrate the bearer key on the next insight call. Now validates with URL parser, requires https://, and only accepts hosts on a small allowlist (OpenAI, Anthropic, Google AI, Mistral, Groq, OpenRouter). Ops can extend via `ADMIN_AI_BASE_URL_ALLOWLIST` env var. - **M1 / CWE-79 (stored, off-platform)** src/app/api/bugreport/route.ts: bug-report description flowed to GitHub with only backtick/asterisk stripping. Now strips HTML tags entirely, neutralises dangerous markdown link schemes (javascript:/data:/vbscript:/file:), and wraps the whole body in a fenced code block so residual markdown renders literally. - **M2 / CWE-79** Zod schemas for feedback + bugreport screenshot now require a base64 PNG/JPEG/WEBP/GIF data URL via regex. SVG deliberately excluded — it can carry inline JS that would execute when the admin inbox renders the `<img>` preview. Medium rate-limiter semantics (audit M3) left as-is: the fixed-window behaviour is acceptable for a single-tenant PWA and matches what ops actually want. Remaining M4–M6 + L1–L7 are tracked for v1.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dashboard): glucose TrendCards + chart label support Render one TrendCard per logged glucose context on the dashboard when the user has the "glucose" widget enabled in their layout. Cards respect the user's glucoseUnit preference via convertGlucose(); data sources from the existing /api/analytics endpoint, which now emits a glucoseByContext map alongside the summaries object. - /api/analytics: include BLOOD_GLUCOSE in the type whitelist (so total counts include glucose) and add glucoseByContext containing a summarize() result per context. - HealthChart: wire BLOOD_GLUCOSE into BASE_TYPE_LABEL_KEYS so the legend shows the localized "Blood glucose" label when the chart is invoked for that type. - Dashboard page: fetch the dashboard layout, gate glucose cards on widget visibility AND data presence, render Droplet-icon TrendCards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(i18n): bugreport.subtitle no longer promises a GitHub issue The visual QA pass flagged that bugreport.subtitle still said "a GitHub issue will be created automatically" even though the banner below the heading correctly explains the submission is stored locally (built-in feedback system, GitHub is only an optional escalation). Neutral copy now that matches the actual behaviour in both locales. * feat(ai): user-level provider selection + test endpoint + UI Adds the missing pieces for the multi-provider AI MVP that pair with the AnthropicClient and LocalOpenAICompatibleClient already on disk: * /api/user/ai-provider (GET + PATCH): user can pick OPENAI / ANTHROPIC / LOCAL / CHATGPT_OAUTH and store provider, model, baseUrl and per-provider API keys. Anthropic + local keys go through the existing AES-256-GCM crypto helper. Provider value is whitelisted. * /api/ai/test (POST): rate-limited (5/min/user) round-trip that resolves the user's provider and asks for a tiny JSON ack so the UI surfaces real "does this config work?" feedback instead of a blind Save. * settings/page.tsx: appends a per-user override card under the existing AI Insights section. Provider dropdown drives conditional fields (Anthropic key for ANTHROPIC, baseUrl + optional bearer for LOCAL, model for all), then Save / Test buttons. * provider.test.ts: extended to cover the four new cases (ANTHROPIC with key, ANTHROPIC fallback, LOCAL, OPENAI routes through admin key, CHATGPT_OAUTH routes through Codex). Switched to resetAllMocks() in beforeEach to flush mockResolvedValueOnce queues between cases — the prior clearAllMocks() leaked them. * local-client.test.ts: full happy / no-auth / bearer-auth / structured error / empty-content coverage mirroring the openai-client suite. typecheck + tests green; lint stays at the pre-existing 2 errors / 7 warnings baseline (no new findings introduced). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: integrate late agent additions (glucose PDF row + i18n) * test(glucose): cover display-unit conversion edge cases Lock in the precision contract for the targets/dashboard/PDF surfaces: canonical mg/dL stays as integers, mmol/L collapses to 1-decimal, and the ADA diabetes thresholds (70/99/126) round-trip to their published SI values (3.9 / 5.5 / 7.0 mmol/L). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(i18n): eliminate hydration flash by threading SSR-resolved locale to I18nProvider Layout already resolves the initial locale from cookie + Accept-Language and sets <html lang>. Until now the client I18nProvider re-detected locale at mount via getSavedLocale() / detectSystemLocale(), causing a flash where "Loading…" (EN) rendered first, then flipped to "Laden…" (DE) after hydration. Pass initialLocale down layout -> Providers -> I18nProvider so useState seeds the same locale the server used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin/feedback): return 409 when GitHub issue already published Spec requires 409 Conflict (not 200 with flag) when the feedback row already has gitHubIssueUrl set. Drops the alreadyPublished response field in favour of a clear error status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(crypto,dashboard): L4 hard-fail short ENCRYPTION_KEY in prod + dashboard WIP from parallel agent * refactor(effective-range): collapse duplicated glucose case arms Extract glucoseRange() helper and toDate() normalizer. Use METRIC_BOUNDS keys as the source of truth for getAllEffectiveRanges() metric list so additions stay in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashboard-layout-section): drop redundant setLayout alias Remove the trivial setLayout wrapper and hoist the per-row label key lookup so it runs once per widget instead of twice (span + aria-label). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): MED-1 SSRF on user AI baseUrl + MED-2 publish-to-GitHub race Final-audit blockers before v1.2 merge. MED-1 — SSRF via user-level LOCAL AI baseUrl Authenticated user could set aiBaseUrl to http://169.254.169.254/, http://localhost:9090/metrics, or any internal host and get response bodies echoed back via the bodyExcerpt error field. Route the value through the existing isPublicUrl() helper (exported from lib/validations/notifications.ts). Ops can opt-in to private hosts on a specific instance via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true — that's how self-hosted Ollama / LM Studio stays usable without exposing the SSRF surface by default. MED-2 — Race in POST /api/admin/feedback/[id]/github Two concurrent admin clicks both saw gitHubIssueUrl=null and both created duplicate GitHub issues. Atomic claim now: a single updateMany({ where: { id, gitHubIssueUrl: null }, data: PENDING }) — only the caller whose count=1 proceeds. PENDING is released on failure so retries work. Tests + typecheck green. Baseline lint unchanged. * fix: i18n completeness + UX polish (closes #73, #74) (#75) * fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX Addresses bugs surfaced in issues #73 and #74 plus drift found during audit. - i18n: add 21 missing translation keys (targets.* namespace was only partially populated after the English-first migration; common.reset and settings.configured were referenced but absent), plus common helpers (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError. - charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P" to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE. - queries: add src/lib/query-keys.ts as a single source of truth with measurement/mood/medication-dependent key sets. Wire them into measurement-form/list, mood-form/list, medication-form/card, intake-history-list, intake-timeline, medications page, and Withings sync in settings. Fixes dashboard not auto-refreshing after adding a measurement (old code only invalidated ["measurements"], not ["analytics"] / ["insights"] / ["insights","targets"]). - admin: useUpdateSettings + updateUser mutations now surface real error messages via sonner toasts and invoke getApiErrorMessage() so saves fail loudly instead of silently. Success also gets a toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(i18n): package B — locale-aware formatters throughout Removes the hard-coded de-DE locale that lingered from the German-first era and makes every number, date, and time locale-aware. English stays the default per user preference; German remains selectable. New: - src/lib/format-locale.ts exposes makeFormatters(locale) + a cached Europe/Berlin timezone. Covers number, integer, percent, date, dateShort, dateTime, time, dateWithWeekday, monthShort. Also exports parseLocaleFromAcceptLanguage() for server code. - useFormatters() hook in @/lib/i18n/context resolves from the active locale, memoized per render. Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") / Intl.DateTimeFormat("de-DE") call sites): - src/app/page.tsx (dashboard) - src/app/insights/page.tsx - src/app/admin/page.tsx - src/app/settings/page.tsx (Withings last-sync, doctor-report caller) - src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels) - src/components/charts/health-chart.tsx - src/components/medications/medication-card.tsx - src/components/medications/medication-form.tsx - src/components/measurements/measurement-list.tsx - src/components/mood/mood-list.tsx - src/components/insights/insight-status-card.tsx (also removed 5 German hard-coded strings with TODO: i18n markers — now translated) - src/components/insights/insight-advisor-card.tsx (same — 10+ strings) Server: reminder-check / reminder-worker / umami-test switched from "de-DE" to neutral en-GB/en-US where the string is machine-compared rather than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now — the repo is English-speaking. Next.js metadata: - OpenGraph locale from "de_DE" → "en_US" with "de_DE" as alternateLocale. - Root layout now reads healthlog-locale cookie (falls back to Accept-Language) and sets <html lang> server-side, avoiding an EN-flashes-then-DE hydration gap and fixing SEO/a11y. - I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next SSR pass is correct. Doctor-report PDF: - generateDoctorReportPDF(data, { t, locale }) now accepts injected translator + locale. All section titles, column headers, BMI / BP classifications, mood labels, and the footer disclaimer are i18n-driven. File name slug switches between gesundheitsbericht / health-report. - 40+ new doctorReport.* keys in EN + DE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ux): package C — typed passkey errors, bug-report gating, error boundary Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste error reporting requested during review. Passkey registration / login (#73.1): - src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code to specific i18n keys instead of swallowing every failure as "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/ Security/Abort errors and falls back to a diagnostic message. - settings passkey + login passkey flows both use describePasskeyError now, so origin mismatches and previous-registration collisions show actionable copy. - 5 new passkey.* i18n keys in EN + DE. Bug report (#73.5): - New GET /api/bugreport/status reports {configured, hasToken, hasRepo, isAdmin}. - /bugreport page now queries status first. When not configured it shows a notice (different copy for admins vs. regular users) with a direct link to the admin panel. Form is hidden, not silently broken. - When POST /api/bugreport can't create issues it returns 503 instead of 500 with the same user-facing message. - 4 new bugreport.* i18n keys. Global error UX: - src/components/error-details.tsx — reusable panel with retry + copy diagnostics (message, digest, stack (trimmed), URL, user-agent, request-id, locale, timestamp) + report-issue link. Clipboard fallback prompt covers insecure-context cases. - src/app/error.tsx uses it. - src/app/global-error.tsx is provider-free (no i18n dependency) since it replaces the root layout on catastrophic failures — hardcoded English + Dracula palette so it still looks like HealthLog. - 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails, retry, reportIssue). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))` verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and adopts it everywhere, including replacing the local `invalidateMedicationRelated` helper that medication-form.tsx had grown. Also fixes the import-sandwich that helper introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: small simplifications in i18n/UX changes - passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow - doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention - dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place - health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): capture upstream response body in OpenAI errors A production insights burst at 12:19 UTC failed with "OpenAI request failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed via logs), but we had no way to tell from logs whether it was an OpenAI incident, a model-not-found, or a response_format rejection because the response body was thrown away. Now the error message includes the first 500 chars of the response body and structured fields (httpStatus, upstream, model) that Wide Event logging can attach. No behavior change for the happy path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): address code-review + security findings Review round produced five real issues — fix them all, plus three nits that were cheap enough to bundle. Must-fix: - security: error-details.tsx + global-error.tsx were copying window.location.href into the clipboard, which on OAuth callback routes (/api/withings/*, password reset) would leak code/state tokens into GitHub issues. Strip to pathname + searchParamCount. - fe: query-keys.invalidateKeys now uses Promise.allSettled, so a transient invalidate failure doesn't abort the chain and no unhandled rejection leaks from void invalidateKeys(...) call sites. - fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers() in try/catch — a dynamic-server error in the root layout now degrades to the "en" default instead of crashing into global-error. - be: reminder-worker + reminder-check parseTimeToMinutes normalize "24:00" → "00:00" (some ICU builds emit that for midnight under en-GB). Prevents a 1440-minute comparison against schedule windows. - security: openai-client error body excerpt redacts sk-* / Bearer tokens before logging, and moves the excerpt off Error.message into a structured field (bodyExcerpt) so log transports can filter it. - i18n: /api/withings/connect German error message → English. - i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...) Hardening: - /api/bugreport/status collapses to {configured, isAdmin} and adds a 30/min rate limit per user. Client interface updated. - i18n/context.tsx: I18nProvider's initializer now reads the cookie (matches SSR) before falling back to localStorage or browser detection — eliminates the locale-flash hydration class. - format.ts legacy helpers also read cookie first, matching SSR. Tests: - new src/lib/__tests__/format-locale.test.ts — 9 cases covering makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale - new passkey-errors.test.ts — 8 cases covering every DOMException name and SimpleWebAuthn code we map - new query-keys.test.ts — bundles + invalidateKeys allSettled semantics (incl. partial-failure test) 118 → 142 tests. All pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: restore Link import lost in merge * fix(bugreport): remove stale loading + not-configured blocks lost in merge The v1.2 built-in feedback refactor made status informational only — form always renders. Main's merge re-introduced the pre-refactor blocker blocks referencing undefined statusLoading/AlertCircle and broke the build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Marc-André Bombeck <marc@bombeck.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
38309932f3 |
Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure, pulse, mood tracking, medication compliance, Withings integration, doctor report PDF export, and Dracula-themed UI. |