Commit Graph
66 Commits
Author SHA1 Message Date
Marc-André Bombeck d33be187e6 Copy polish: one pause dash, du-imperative errors, bare-question confirms, tokened micro-text
The German bundle's 109 spaced en-dash pauses join the em-dash norm (the
two date-range placeholders stay ranges); the three straight-quote
stragglers and three mixed-quote strings take German quotes; the eleven
Bitte-Infinitiv validation errors speak du-imperative like the rest of
the app; the two wirklich confirms become bare questions; the
notification-status description trades its semicolon telegram style for
two sentences in every locale; the Withings setup action shortens to the
sibling single word. Twelve arbitrary text-[10px]/[11px] sites move onto
the text-2xs/text-xs tokens; chart tooltips and the vendor calendar stay
as they are.
2026-08-14 10:08:49 +02:00
Marc-André Bombeck a3561b4600 Tell a first-run what Apple Health is actually doing
Refs #778. Two honesty gaps from the first-run report: the web onboarding
presented Apple Health like a web connector although the connection only
completes in the iPhone app, and the initial backfill gave no sign of
progress, so a working first sync looked broken.

The onboarding source step now says plainly that Apple Health is set up in
the iPhone app after onboarding, without dead-ending the flow. The
integration card gains a server-authoritative progress summary — rows
accepted, oldest reading reached, and an honest flowing/waiting line
derived from the last accepted batch. Nothing the server does not know is
shown: no percentage, no ETA, no invented throttle state; a quiet stretch
reads as waiting for the phone, not as an error.
2026-08-14 09:46:24 +02:00
Marc-André Bombeck 3b1314d1b0 Enforce the reminder nag cap, compare the disclaimer version, drop the unread analytics columns
- The daily Coach sweep now charges each ignored day against
  surfaceCount and auto-dismisses at three — the cap the schema comment
  promised since v1.22 but nothing enforced.
- The welcome gate compares the stored disclaimer acknowledgment
  version against the current one, so revised disclaimer text
  re-prompts; /api/auth/me carries the version for the client.
- slope90 and anomalyCount leave the analytics wire: slope90 had no
  reader anywhere (computed on every request, in SQL twice), and the
  slim path could never compute anomalyCount (it always reported 0).
  anomalyCount stays as an in-process field on summarize() for the
  insights feature builder, which is its only real consumer.
2026-08-14 05:19:05 +02:00
Marc-André Bombeck 7f6eae8425 style(ui): pull off-scale spacing and micro-labels onto the scale
Sweep the spacing rests that sat off the 1/1.5/2/3/4/6/8/10 scale: the
health-score panels and their skeleton, the recommendation ask-Coach
gutter, the onboarding tour footer, the vaccination row, the danger-zone
alert margins, the recovery-codes action wrapper, and the chart-tooltip
tile. Move the last hand-written micro-sizes (coach counters, the
personal-record badge, the score-ring label, the dose-history and
cycle-calendar headers, the mood tag chips) onto the text-2xs token, and
give the documents upload buttons the mobile tap floor their peers set.
2026-08-09 11:27:05 +02:00
Marc-André Bombeck b6332a96d9 refactor(ui): route hand-rolled card headers through the shared header primitives
Move the last hand-rolled tile and section headers onto TileHeader and
SectionHeading so every card speaks one header language: the vorsorge
dashboard tile, the three illness cards, the metric-correlation section,
and the custom-metric list group. The mood-chart title drops to the
primitive semibold weight and its bucket label becomes a TagChip.

Align the in-card sub-headings of the integration cards to the one
sanctioned section-label tier (muted uppercase eyebrow) instead of a
second text-sm font-semibold dialect.

Keep the onboarding and mental-health wizard hero H1s at their
deliberate semibold weight, now marked inline as an intentional
exception so a future heading sweep leaves them alone.
2026-08-09 11:03:43 +02:00
Marc-André Bombeck 675c70eba8 Keep the dashboard the server streamed
`/` runs its reads server-side and hands them to TanStack through a
HydrationBoundary, so the first HTML already carries real tiles. React threw
that away on every cold load. The app shell hydrates first and its mount
answers `/api/auth/me`; the dashboard sits below `loading.tsx`, hydrates later,
and its first client render therefore read an account the server never had.
That disagreed with the streamed HTML, React reported #418 and re-rendered the
whole page. The prefetch paid for its queries and delivered none of its paint.

The account payload is now withheld from the hydration render wherever it is
only rendered. `useAccountOnceMounted()` answers null until the component has
mounted, which is what the server rendered, and hands the payload over on the
next render. The dashboard page, the caregiver card, the getting-started
checklist, the achievements tile and the unit labels read it that way. `useAuth`
itself still reads the cache, because the document vault redirects off
`user.modules` and a first render claiming "no account" bounces it to `/`.

Two tiles disagreed without any account involved. A query reports `isLoading`
true on the client's first render and false on the server, so both now gate on
whether the cell holds an answer.

Nothing in CI ran the shipped configuration: the suite's server turns the
prefetch off so route mocks keep governing the dashboard. A second server runs
it on, and one spec asserts that no hydration error is reported and that the
tile node the server streamed is still the node in the DOM once the page
settles.

Measured over nine cold loads with the CPU throttled 4x: seven bailouts before
and none after, median LCP 869 ms before and 145 ms after. FCP and the time to
the first tile in the DOM are unchanged, because the streamed paint was never
the problem. Keeping it was.
2026-08-06 17:28:48 +02:00
Marc-André Bombeck a640e84142 Exclude the worktrees the tooling was already meant to exclude
git, eslint and vitest each carry an exclusion for local development
worktrees, and all three name /.worktrees/ while the convention actually in
use is /.wt-*/. Every one of them was therefore walking eleven full copies of
the source tree.

The consequences differed by tool and none of them announced itself. pnpm lint
took ten minutes instead of one and reported warnings from whatever commits
those copies happened to sit on. vitest collected duplicate suites that drag in
a second React instance, which fails in ways that have nothing to do with the
working tree. git left them merely untracked, one git add -A away from
committing the repository eleven times over.

With the true warning count finally visible, teach no-unused-vars the
underscore convention this codebase already follows and drop the five
disable directives that convention made redundant. pnpm lint is clean.

A non-underscore unused argument still warns; that was checked rather than
assumed.
2026-08-02 16:36:25 +02:00
Marc-André Bombeck 8e6880b04c Say when the onboarding profile step could not save a field
The baseline step submitted what you filled in and ignored the answer.
The profile write is field-independent: it stores everything that
validated and reports back what it declined. Nobody read that report
here, so a value the server refused vanished between the input and the
next screen and you moved on believing it was saved.

It now says which field did not make it, using the name that sits next
to the input you typed into, and keeps going with the rest that did
save. When nothing could be saved at all, the step stays put and names
the field that blocked it instead of a shrug. The wording and the
field-naming come from the same place the account form in Settings
uses, so the same refusal reads the same wherever you hit it.
2026-07-31 07:58:45 +02:00
Marc-André Bombeck 349dac3a07 test(profile): cover the third gender value where it used to be lost
Adds the cases that were missing everywhere except the FHIR conformance
suite: an empty string clears without taking its siblings down, OTHER
round-trips through the profile and onboarding writes, an unsupported
value is still refused and says which field it is about, the Strain
model scores an unanswered account on neither side, the dashboard bands
give OTHER the neutral fallback, the snapshot payload carries the value
instead of flattening it, and the doctor-report cover omits the row
rather than claiming a value the account never chose.

Refs #645
2026-07-26 17:09:03 +02:00
Marc-André Bombeck f74efc44aa fix(profile): let the third gender value through every surface that handles it
The profile validator has accepted MALE, FEMALE and OTHER for a long
time, but the published contract documented two, and a client written
against that contract had no way to know the third existed. The profile
PATCH is one write, so a rejected gender took every sibling field with
it - a time-format change lost to a field the person never touched.

Gender now clears on an empty string, the same contract every other
profile field documents, and an unsupported value is refused with a
message that names the field and its accepted values instead of the
validator's own prose. Onboarding validated against a narrower enum
than the profile schema and 422'd the whole submission; its form could
not record the third value at all.

The rest is the same defect in other clothes. The Strain model scored
an unanswered or OTHER account on the male Banister coefficients; it
now uses the midpoint of both published models, matching every other
sex-split formula here. The dashboard snapshot emitted null for an
account that answered OTHER, which is indistinguishable from no answer.
Three casts asserted a two-value type over a three-value column. All of
them now carry the stored value and narrow once, at the boundary of a
reference table that genuinely has no third row, through one named
helper in src/lib/profile/sex.ts.

Refs #645
2026-07-26 17:09:03 +02:00
Marc-André Bombeck d0b45e36b3 feat(integrations): remove the moodLog integration
Mood has been a first-class module here for a long time, with entries,
structured tags, rated factors and notes. The bridge to the external moodLog
service had nothing left to add, and its Settings card was withdrawn in
a56e6fe61 on the promise of a dedicated removal. This delivers it.

Removed: the hourly `moodlog-sync` queue, cron and handler binding; the
inbound webhook and its shared-secret handling; the reverse push and its four
call sites; the pull-sync module; the per-user credential routes and the
instance-wide toggle; the entry on the `/api/integrations/status` envelope,
the `IntegrationKey` union, the cadence map and the display-name table; the
boot-time secret rotation pass; 22 message keys per locale.

Migration 0271 drops the five `users` credential columns and
`app_settings.mood_log_global`, clears the ledger row for the provider, and
deletes the retired pg-boss schedule so no cron keeps enqueuing work no
handler drains.

Mood data is untouched. `mood_entries` is not named anywhere in the migration
and cannot be reached from it: the dropped columns carry no index, constraint
or foreign key, and `mood_entries.user_id` references `users(id)`, not any of
them. Rows the bridge imported keep `source = 'MOODLOG'`, and every read path
- list filter, source badge, exports, backups - still resolves that value.

`src/lib/validations/moodlog.ts` held the native mood-entry schemas alongside
the bridge's own; it is now `validations/mood.ts` and carries only the former.
2026-07-25 11:36:18 +02:00
Marc-André Bombeck 1b41126fb3 feat(profile): enter and show height in the selected unit
Height is a profile column, not a MeasurementType, so the display
transform that carries the metric/imperial preference across every
reading surface never reached it. Add the adapter that closes the gap.

With imperial selected both entry surfaces render a feet + inches pair
instead of a centimetres box; with metric selected the field is what it
was. The adapter converts to canonical centimetres at the entry
boundary, so storage, the wire, the Zod bound, and every consumer of
the value stay unchanged.

Imperial entry quantises to whole inches, which map to centimetres
without a remainder, so save-then-reopen returns the typed pair. The
offered feet window rounds inward against the canonical 50-300 window
so a height the fields allow is never refused on save.

The unit select shares the Settings card with the height field, so a
flip re-expresses the in-flight draft through the new branch rather
than stranding an empty field.
2026-07-25 08:21:42 +02:00
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00
Marc-André Bombeck e1c6941b20 fix(ui): lift muted meta text off the sub-xs floor
Bump text-[10px]/text-[11px] to text-xs on muted description, hint, and
status-label surfaces that had drifted below the type-scale floor —
coach conversation captions, settings/admin status badges, mood
confidence pills, side-effect and cycle meta lines. Tabular-nums
badges, chart-plot legend/axis chrome, ring geometry, and
width-constrained tag tiles keep their compact size by design.
2026-07-18 10:27:55 +02:00
Marc-André Bombeck 8dd04e0170 fix(ux): close medium UX-audit findings across flows, IA, and onboarding
Coach deep-links now carry launch context in the URL instead of dropping it
at the route boundary: /coach reads ?scope= (validated against the closed
CoachScopeSource enum) and ?ask= to seed the drawer's own prefill/scope
props, the Today check-in's "Adjust" action seeds ?ask= with the plan's own
words instead of opening a blank chat, and the workout detail page scopes
its coach button to workouts.

IA: give the document vault a "View N lab values" link on LAB_RESULT-shaped
documents (the auto-stage/OCR-commit linkage existed server-side with no UI
surface); disambiguate the two "Recovery" pages (RECOVERY_SCORE anatomy vs
the composite wearable page) with a distinct label plus a two-way
cross-link; move the opt-in nutrients sync card from Source priority to
Integrations, where every other sync surface lives; rename the user-menu
theme switcher off "Appearance" (collides with the Settings hub) and the
Environment settings section to "Location & weather"; cross-link mood,
mental-wellbeing check-ins, and mood insights, which were three
unconnected islands.

Onboarding: the getting-started checklist's data-source/notifications rows
now gate their fetch on the same checklistRelevant condition the other rows
use, instead of only-while-onboarding — they used to be stuck undone
forever for anyone who finished the wizard with under 5 measurements; the
tour wrap-up no longer promises a replay affordance that was removed;
the wizard's Withings card shows a connected badge once the OAuth
round-trip lands instead of leaving the button looking untouched in the
tab it was opened from; the done screen and Modules hub get a quiet
cross-link; the sleep empty-state CTA reads "Connect a source" instead of
being pinned to one vendor.

All copy changes land across every locale; de.json reviewed for register.
2026-07-18 05:06:24 +02:00
Marc-André Bombeck fb73c97125 fix(onboarding): stop the Withings connect dead-end and disclose BYO setup
The onboarding "Connect Withings" tap opened /api/withings/connect in a new
tab, which returned a raw JSON 400 for every brand-new account (Withings
credentials are per-user BYO with no env fallback, and the wizard never
collects them). WHOOP, Fitbit, Polar, Oura, and Strava shared the identical
raw-JSON-on-no-creds pattern on their own /connect routes, while every other
failure branch already redirected with a toast reason. All six now redirect
to /settings/integrations?<provider>=error&reason=nocreds instead, and the
wizard's Withings card checks the integrations-status envelope first: unless
credentials are already configured, the CTA reads "Set up in Settings" and
links straight to the credentials form rather than launching a doomed OAuth
handshake.

Also surfaces the BYO-developer-app requirement, which was invisible until a
user was deep inside a Settings card. The source-picker step now says
up front that device sources need a free developer app on the user's own
account; each of the six provider cards gets a compact 3-step guide with the
exact callback URL to register, shown until the user has saved their own
credentials.
2026-07-17 23:12:12 +02:00
Marc-André Bombeck 6d83115d16 chore(release): v1.28.43 — visual consistency polish
Verified UI-consistency findings from the campaign's design audit, each
pixel-faithful except the intended correction:

- insights hero band: off-scale py-5/gap-5 → the standard scale; H1
  font-semibold → font-bold; sm:text-[28px] → sm:text-3xl.
- medication-compliance-chart + recent-achievements-card: hand-rolled
  muted h-4 icon + text-sm headers → the standard TileHeader primitive so
  they match sibling tiles.
- achievements stat tiles + clinician-view shells: p-5 → p-4 md:p-6.
- two text-primary header glyphs (research-mode dialog, enroll-mfa) →
  text-foreground (never accent on a header glyph); the deliberate
  badge-boxed icons left untouched.
- SheetHeader now reserves the trailing close-X gutter like DialogHeader,
  closing the latent overlap class for future raw-Sheet consumers.
- five raw Dialogs on phone-reachable surfaces → ResponsiveSheet.
- new bg-card-scoped healthlog/spacing-scale lint (flags p-5/gap-5 etc.
  only on card shells; list-marker insets + half-steps excluded) at error,
  with the five offenders it flagged swept.
- dropped the stale CLAUDE.md '18 PascalCase outliers' line (0 remain).

Every data-slot preserved. No migration.
2026-07-16 03:44:00 +02:00
Marc-André Bombeck 0adba584a9 feat(onboarding): make AI insights reachable early without a gate
Close the gap between the welcome carousel's AI promise and the payoff:
a fresh keyless user used to meet "No AI provider connected" on every AI
surface, because the only setup prompt was a skippable card on the final
screen.

Add an "insights" row to the getting-started checklist, derived from real
provider state — any personal key, local model, OAuth sign-in, or the
operator's shared key flips it done via /api/user/ai-provider. It persists
past onboarding, self-satisfies on connect, and dismisses like the others.

Elevate the done-screen AI card into a value-first panel: it leads with a
static, clearly-labelled example briefing (no model call, no egress, no
consent), then the local-first ladder — local model, bring-your-own-key,
subscription sign-in with the training caveat — and the honest line that
HealthLog is fully useful without AI. All three exits stay, so the panel
gates nothing. On a deployment with a shared provider it says so plainly.

Extract the worked example into a shared SampleBriefingCard. Add
onboarding.ai.* copy across all six locales and retire the old
single-card done-screen keys.
2026-07-09 20:32:33 +02:00
Marc-André Bombeck 9f21d9f20d fix(cards): retire banned p-5 shells and accent header icons
Bring the structural card-anatomy offenders onto the spacing scale. The
cycle wellness-tile family (ring tile, phase-education, insight summary,
cycle view) and the insights wellness-scores tile drop banned p-5/px-5
py-5 for p-4 md:p-6 and keep their premium hue surface; their header and
info glyphs move off text-primary to foreground, spinners to muted. The
onboarding hand-rolled shells (anamnesis card, baseline form,
getting-started checklist, tour tooltip) drop p-5/space-y-5/rounded-2xl
for p-4 md:p-6/space-y-4/rounded-xl. settings/anamnesis-section moves its
three raw Card+CardTitle shells onto SettingsCard + SettingsCardHeader.
2026-07-09 12:05:34 +02:00
Marc-André Bombeck e5ca125dfb fix(onboarding,integrations,cycle): deep-link source chips + cycle settings wrench
- Onboarding 'more sources' chips anchor to their provider card
  (/settings/integrations#oura etc.); connection cards gain scroll anchors.
  Apple Health (no web card) stays on the page root.
- Cycle module gains a header settings wrench for parity with labs /
  medications / illness; the tab strip is now controlled so the wrench jumps
  to the settings tab and ?tab= deep-links keep working.
2026-07-08 23:59:10 +02:00
Marc-André Bombeck 72ad228f3e fix(ui): lift the remaining explainer prose out of muted
Residual grey-on-content sweep after the page-description lift:
multi-sentence explainer paragraphs are content and read in the
regular foreground, matching the metric sub-page explainer that set
the pattern — the labs biomarker and custom-metric detail leads, the
recovery metric and resilience explainers, and the onboarding
carousel/done-screen body copy. Muted stays reserved for meta:
timestamps, units, captions under a stat, one-sentence description
slots.
2026-07-05 22:01:15 +02:00
Marc-André Bombeck a4b204ffe4 refactor(ui): retire the raw dracula utilities for semantic tokens
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:

- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
  badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
  light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
  update-available pill and offline-geo hint → warning; the admin
  helpful-rate ramp → success/warning/destructive; the confidence
  meter mirrors the coverage-meter precedent (medium + low share the
  caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
  classes (no generated utility) for border-border + bg-muted/50

Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
2026-07-05 21:28:09 +02:00
Marc-André Bombeck d0b4b1e4cd fix(ui): introduce FieldGroup and unify the add-form rhythm
The measurement add form opened with a horizontal type row (label beside
the select) while every field below it stacked label-above-control, so
the form read with two competing rhythms. Introduce a small FieldGroup
primitive — the long-standing space-y-2 label-above-control idiom in one
place — and route the measurement, mood and baseline forms through it so
they share one vertical rhythm. The type row now stacks like the rest.

Output stays byte-identical for the already-vertical groups; only the
type row changes, by design.
2026-06-30 12:35:36 +02:00
Marc-André Bombeck 0816b3b0c0 feat(date): migrate remaining date inputs to DateField
Swap the four DateInput callsites (filter bar, export range, schedule
history, onboarding baseline) to DateField so the user's date-order
preference paints every date input. Adapt the onChange from a native
event to the direct ISO value DateField emits, and add an optional
autoComplete prop so the onboarding date-of-birth field keeps its bday
autofill hint.
2026-06-26 13:14:19 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck 68033b2d66 refactor(onboarding): point consumers at the kebab-case component files
Update the onboarding step page, baseline form, and the anamnesis-card
spec to import from the renamed kebab-case files. Export identifiers stay
unchanged.
2026-06-19 22:17:34 +02:00
Marc-André Bombeck 922f770b41 refactor(medications): rename PascalCase component files to kebab-case
Bring the medication card, scheduling, wizard, and detail component
files in line with the kebab-case file-naming convention. Component
export identifiers are unchanged; only filenames and the import paths
that point at them move.
2026-06-19 22:17:13 +02:00
Marc-André Bombeck bda06c9ec4 refactor(query-keys): route remaining bare arrays through the factory
The target editor invalidated a bare ["insights"] array on save and reset.
That literal sat outside the centralised factory, so the broad insights
fan-out it intended never matched the reads, which key through
insightsTargets/insightsRoot. Route the editor's reads and invalidations
through userThresholds(), insightsRoot(), insightsTargets() and analytics()
so a saved target actually refreshes every insights surface.

Migrate the other call sites the lint rule does not reach onto the factory:
the Coach conversation rail + thread reads, the per-metric personal-records
badge, the per-day measurement drill-down, the dashboard intake
compliance-chart fan-out, the per-medication side-effects list, the
onboarding auth invalidations, the notification-preferences read, and the
PWA persister's auth-me lookup.

Add the matching factory keys: coachConversations, coachConversation,
personalRecordsByMetric, measurementDrilldown, complianceChartInline and
medicationSideEffects. Each new key reuses the prefix its reads already use
so no invalidation shape drifts.
2026-06-19 20:57:18 +02:00
Marc-André Bombeck 3ba542f797 fix(onboarding): make the guided tour first-time-only, drop every re-entry trigger
The guided tour now exists only as the first-time auto-start after onboarding.
Remove every in-app trigger that re-opened it: the per-module "show this tour"
button on the dashboard, measurements, mood, medications, labs, illness, cycle,
achievements, insights, and coach surfaces; and the Settings replay card.

Delete the now-orphaned ModuleTourTrigger component, the tour-restart worker,
the healthlog:module-tour and healthlog:tour-restart event listeners, the
single-stop overlay path, and the force-launch session marker. The first-time
auto-launch gate and the tour overlay itself are unchanged. Prune the orphaned
i18n keys across all locales and update the affected tests.
2026-06-18 23:58:27 +02:00
Marc-André Bombeck 22f588b34d refactor(ui): correct stale comments and steady the settings-nav spacer
Drop the overstated claim that the due-reminder green button mirrors the
medication card (the med take button is always the default tone), fix the
module-tour anchor attribute named in a doc comment, and feed the active
section title into the settings nav spacer so its heading line wraps the same
way a long localized title does.
2026-06-18 22:42:22 +02:00
Marc-André Bombeck 0a8ccaa30c feat(onboarding): one-time medical-disclaimer acknowledgment
Replace the per-page and per-chart disclaimer banners with a single
acknowledgment at the start of onboarding: HealthLog is a private tracking
tool, not a clinical assessment or diagnosis; talk to a doctor if worried.

- record the acknowledgment per user (disclaimer_acknowledged_at/_version,
  migration 0175) mirroring the research-mode acknowledgment precedent
- gate the welcome "Get started" on the checkbox; expose the timestamp on
  /api/auth/me so a returning user is never re-asked
- keep the full legal text reachable on the public privacy page
2026-06-18 21:37:13 +02:00
Marc-André Bombeck 1d83e10764 merge(W4): module-by-module guided tour
Extend the in-house spotlight into a cross-page tour: 15 module stops,
each gated on its module, walking the app and teaching what each module
does and how it feeds the others. Auto-starts after the anamnesis,
resumes from onboarding_tour_progress_json, re-enters per-module via a
header trigger, and replays from one Settings card. Adds the tour
progress pref + migration 0173 and the onboarding tour checkpoint route.

# Conflicts:
#	src/components/onboarding/DoneScreen.tsx
2026-06-18 19:55:00 +02:00
Marc-André Bombeck 91fd0b438e refactor(onboarding): stabilise the tour progress callback
Memoise the launcher's progress handler so the overlay checkpoint effect
fires on a real step transition rather than on every launcher re-render.
2026-06-18 19:52:40 +02:00
Marc-André Bombeck 33fec28b29 feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the
tour narrowed to that module's card in place, plus additive data-tour
anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/
insights/coach/integrations/export/achievements) so the cross-page walk
has a real target on each page. Consolidate the full-tour replay onto a
single Settings -> Advanced card, relabel it, and drop the duplicate from
About. The replay reset clears the stored progress so it restarts cleanly.
2026-06-18 19:49:37 +02:00
Marc-André Bombeck f0c25cc275 feat(onboarding): rebuild the tour as a 15-stop cross-page module walk
Replace the five chrome-pointer stops with a module-by-module guided tour
in the data->intelligence->export order. Each stop teaches the module's
value, one hidden capability, and how it feeds the others. Stops carry a
route + module gate; the overlay router.pushes to the route, waits for the
anchor to mount, then measures, so a single tour walks the whole product.

Move the launcher to the app shell so the overlay survives navigation,
auto-open immediately after anamnesis for fresh users (24h path stays the
fallback for wizard-skippers), resume from the persisted checkpoint, and
gate stops + the step counter on the resolved module map. Add tour copy
for all 15 stops across the six locales.
2026-06-18 19:39:10 +02:00
Marc-André Bombeck cd816607e4 feat(onboarding): surface AI-provider setup on the done step
AI Insights, Coach and the daily briefing are default-ON but need a BYOK
or local provider that onboarding never mentioned, so a fresh user met
the flagship feature as a cold error. Add an optional, skippable card to
the onboarding done step that explains what the AI does (plain-language
summaries of your readings, data stays with you) and deep-links to
Settings -> AI. Adds onboarding.done.aiTitle/aiBody/aiCta across all six
locales.
2026-06-18 19:09:12 +02:00
Marc-André Bombeck ca196f27a6 fix(onboarding): skip the achievements tour stop when the module is off
The dashboard tour launcher now derives includeAchievements from the
per-user module map (modules?.achievements !== false), so an account
that has the achievements module turned off no longer walks the user to
a page it has hidden.

Mention the module on/off concept in the integrations tour stop, where
the tour introduces Settings and customization, so a first-time user
learns whole areas can be turned on or off.
2026-06-16 02:04:20 +02:00
Marc-André Bombeck c8cd5aa226 feat(onboarding): add an optional anamnesis step on baseline
A collapsible "About your health" card on the baseline step captures
the genuinely-new baseline the app can act on — chronic conditions and
allergies — without re-asking the height/sex/birthdate already gathered
above. Both write through the existing encrypted self-context path
(PUT /api/coach/about-me), so there is no new model and no new
contract; the form preserves any existing free-text about-me on save
and only sends a field the user actually typed.

The card is collapsed by default with a clear optional framing, so the
step stays clear and the progress bar keeps its five dots; every field
is editable later in Settings. Copy is warm and medically grounded —
context, not a clinical form — with keys across all six locales. Also
re-truths the goals body copy now that the selection genuinely surfaces
on the dashboard.
2026-06-14 23:21:58 +02:00
Marc-André Bombeck 5f601426a9 feat(onboarding): persist goal selection and seed the dashboard
Wire the goals step to actually keep its promise. The picker now posts
the chosen slugs on the step-2 submit; the step endpoint validates them
against the closed slug enum (422 on an unknown slug), persists them to
User.onboardingGoals field-by-field, and on completion seeds
dashboardWidgetsJson from the selection — promoting the goal-mapped
tiles to the top and forcing them visible.

The seed is one-time, gated on a null dashboardWidgetsJson via the
updateMany WHERE clause, so a user who already arranged tiles is never
clobbered. An empty or general-wellness-only selection leaves the
column untouched (default layout). The resulting layout is what both
the web dashboard and the iOS widgets contract read — no client
recompute. Skip posts an explicit empty array (deliberate no
preference).
2026-06-14 23:21:48 +02:00
Marc-André Bombeck 1ddc8a0c47 Surface shipped data sources in onboarding and tell the truth
Rewrite the source step to list the live integrations (Apple Health,
WHOOP, Oura, Polar, Nightscout, Fitbit) alongside Withings and manual
entry, each linking to where it is connected; drop the stale Apple
Health coming-soon badge. Add an import affordance for existing history.

Reword the goals and source copy so it no longer promises automatic
dashboard or reminder configuration that does not happen yet. Add a
calibration line to the done screen setting the expectation that some
insights sharpen over the first weeks, and lead the success screen
forward with connect-a-device and log-first-measurement actions.

Generalize the dashboard checklist data-source item so any connected
integration satisfies it, not Withings alone.
2026-06-14 21:26:09 +02:00
Marc-André Bombeck 3b480b9885 perf(i18n,shell): ship one locale and stop blocking the first paint
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck bd78cd7f7a feat(medications): schedules remember their history
Editing a schedule's times no longer rewrites the past. Each material
change archives the outgoing cadence as a dated revision, and every
band consumer — history, compliance, write attribution, the auto-miss
cutoff — mints past periods from the schedule that was valid at the
time, so a dose era taken at its then-correct times stays on time
forever. The repair script can infer historical eras from consistent
old anchors behind an explicit flag. The welcome tour scrolls its
target into view, flips above it when space below runs out and falls
back to a centred sheet on any viewport, so no step can strand its
buttons below the fold. The avatar menu drops the about entry — that
lives in settings.
2026-06-10 22:21:44 +02:00
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00
Marc-André Bombeck 298e4c34b4 perf: collapse the medication compliance hot path and the dashboard fan-out
The medication list fired one compliance request per card every 30
seconds, and each request scanned the medication's full intake history
and expanded the recurrence calendar five to six times. The endpoint now
mints bands once per request, serves the sub-windows from that single
ledger, bounds the event read to a year of selected columns and sits
behind a write-invalidated 15-minute server cache; the cards poll at
five minutes. The dashboard drops two redundant checklist queries by
reading counts from the snapshot, the BMI chart derives from the weight
series instead of refetching it, the detail page no longer serialises
behind the auth probe and seeds its query from the list row. Expensive
analytics reads gain a shared per-user rate limit, and the service
worker stops serving cross-version shells, enables navigation preload
and trims caches in one pass.
2026-06-10 12:55:56 +02:00
Marc-André Bombeck 3ce0b69802 refactor(settings): unify card headers, integration verbs, test buttons, and tap targets
Holistic consistency pass over the settings and integration surfaces so the
same concept reads and behaves the same way everywhere.

- Card headers: migrate the four integration cards (Withings, WHOOP,
  Fitbit/Google Health, Mood Log) plus the profile, passkeys, avatar, about,
  api-endpoints, api-tokens, and sharing cards onto the shared
  SettingsCardHeader primitive. Extend the primitive with an inline title
  accessory slot (Fitbit tag + experimental badge) and a multi-line
  description slot (overlap / experimental / deprecation sub-notes), keeping
  the rendered result identical.
- Integration verbs: normalise Mood Log onto the canonical "Sync now" /
  "Sync all data" / "Synchronize" set used by the other integrations, across
  all six locales. Swap the full-sync icon from Download to RefreshCw and
  align its action-row icon metrics with the sibling cards.
- Test buttons: migrate Telegram and ntfy off their hand-rolled "Test
  message" buttons onto the shared TestConnectionButton, so every channel and
  integration test control shares one icon, label, latency readout, and error
  taxonomy. Retire the now-unused testMessage / testSent strings.
- ntfy auth-token placeholder no longer reuses the "Saved" status string;
  it now reads "Saved — enter new to replace" like the other secret fields.
- Save-success copy: collapse the moodLog and Telegram bespoke strings onto
  the shared settings.saved string.
- Tap targets: floor every size="sm" action-row button (sync / full-sync /
  test / disconnect / save / connect-secret copy) at min-h-11, fixed at the
  TestConnectionButton source and across the cards, so they clear the 44px
  touch minimum.
- Contrast: the onboarding source-card badge moves from muted-on-muted to
  text-foreground; web-push card body rhythm aligns on space-y-4; Mood Log's
  deprecation note drops its bespoke 11px-italic treatment for the shared
  sub-note style.

(cherry picked from commit 11931d14b7875b7e2184a3b373d7a34b842f8eae)
2026-06-05 03:45:04 +02:00
Marc-André Bombeck 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.
2026-05-22 09:18:52 +02:00
Marc-André Bombeck 6c7fc7dc84 chore(release): v1.4.45 — analytics 9 s perf fix + audit-marathon closure + v1.4.44 hotfix merged
Audit-marathon close: 14 waves + W10 QA + reconcile. Perf fix (analytics 9 s -> 2-3 s), Zod multi-issue rollout (41 routes), Withings parked-state automation, mobile-UI WCAG 2.5.5 close, QoL i18n + offline banner + account delete, security PII drop + replay-injection scan. Migration 0075 required. Tests 4815 -> 5076.
2026-05-21 23:13:21 +02:00
Marc-André Bombeck 67207d725e chore(release): v1.4.41 — iOS perf hotfix, soft-delete completeness, tree hygiene
Eight touch-disjoint waves landed on develop before this release:

- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
  persist helper and wire all seven *-status routes through it, closing
  the recurring ~14 s warm response the iOS client sees on dashboard
  open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
  (export bundle, gamification achievements, doctor-report aggregator)
  with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
  src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
  discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused tx?
  params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
  formalisation; knip CI gate staged for v1.4.42 flip.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.

pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
2026-05-21 18:36:17 +02:00
Marc-André Bombeck a550031a84 chore(release): v1.4.38
Robustness sweep, perf hotspots, full localization, coach gate
cascade closure.

- /api/dashboard/summary cold-mount 4.6 s → ~500 ms via DAY-bucket
  sparkline + 60 s response cache.
- Coach API gate inventory test surfaced and closed two orphan
  routes (chat[id] GET+DELETE, chat-message feedback POST).
- Cross-tz fast-path runtime guard falls back to live for users
  more than ±3 h from UTC; proper bucket minting stays v1.5.
- es / fr / it / pl coverage 27 % → 63 % (~3 400 new strings) plus
  three medication-cluster placeholder restorations.
- IANA timezone validation closes the User.timezone self-DoS
  regression the new dashboard SQL would have surfaced.
- Unit suite 4520 → 4524.

See CHANGELOG.md for the full set.
2026-05-17 19:07:03 +02:00
Marc-André Bombeck 88028fa082 v1.4.37 — final web polish before iOS focus
Carries the v1.4.36 perf carry-over for the full /api/analytics
route (correlations + healthScore + bp_in_target on the rollup-
coverage probe, 111s cold worst-case down to ~1.5-3s on power-user
accounts), consolidates Apple Health step samples into a one-row-
per-day list view with chevron drill-down and a nightly drain
(~50x table compression once it converges), restores
IntakeHistoryListV2 to the pre-v1.4.28 contract, converges every
Coach affordance behind a single feature gate with an invariant
test pin, lifts the medication-detail Mounjaro card to byte-
equivalent parity with Ramipril, brings the Insights overview
hero row to height parity, hardens getClientIp behind a new
TRUST_CF_CONNECTING_IP env flag, ships the Arztbericht hero card
on Settings -> Export, adds the Medikamenteneinnahme quick-add
overlay to the dashboard, drops the timezone override toggle for
a silent browser-zone auto-seed, and fixes the v1.4.36 red CI
workflows. Documentation refreshed across all three repos with
five new architecture diagrams in the Dracula palette. Unit suite
4354 -> 4486 passing; typecheck + lint clean.
2026-05-17 15:36:38 +02:00