Commit Graph
43 Commits
Author SHA1 Message Date
Marc-André Bombeck 41596d1cb8 docs: comments name what the code does, not how it was built
Thirty-one files carried internal build vocabulary and anchors pointing at
notes that are not in the repository: a reader of `checklist.ts` in a year
would find its rationale keyed to a research id they cannot open, inside a
comment that also names an internal build stage. Two test titles carried the
same anchors.

The version tags and the whole of the reasoning stay; only the vocabulary and
the dangling references go, with the few sentences that were built around an
anchor rewritten to stand on their own.
2026-09-11 11:03:04 +02:00
Marc-André Bombeck 4d7b326fda Merge wave B — the connect step reads liveness, not a latched field
A provider with no sync history reports "unknown" instead of "connected",
so a fresh account is no longer told a device it never linked is already
delivering; the connect step resolves the sync verdict into four honest
tiles, records the flow's result only for a connection that is actually
serving, points an unconfigured provider at its credentials, and steps
over a source the account already has.
2026-09-11 10:24:54 +02:00
Marc-André Bombeck e9e4a4b44d fix(onboarding): confirm names the modules leaving the nav
The screen listed what the answers switch on and said everything else
stays one click away under Settings, Modules. That is true of
reachability and false of the navigation entry: a module the answers do
not ask for is written off by the completion's derivation.

It now names those modules, read through the registry's own merge rule
and intersected with the map the navigation shows today, so nothing is
claimed about a module the account payload says nothing about.
2026-09-11 09:24:36 +02:00
Marc-André Bombeck 8d81ac0708 fix(onboarding): a connected source is acknowledged, not offered
`chooseFirstResultTask` read the answers alone, so an account whose WHOOP had
been feeding the dashboard for months was sent to go and connect WHOOP. It
takes the delivering sources now; the server still picks from the answers,
having no status in hand, and the screen re-picks once the envelope resolves.

The fall-through stops where there is nowhere to fall to: a person whose one
answer is an already-connected source keeps the step and is told what was
found, rather than ending the flow on no result at all.
2026-09-11 09:22:18 +02:00
Marc-André Bombeck da58d16642 fix(onboarding): the connect step reads the sync verdict
The step decided "connected" from the ledger's `state` and had one piece of
copy for every outcome: "the first sync is in progress". That sentence was
false for a connection delivering for months, and worse for one the person
has to repair — the screen said everything was fine while the grant was gone.

`connectSourceView` resolves the envelope entry into one of four tiles and
says whether the flow may record its result. Only `pending_first_sync` and
`fresh` may; `stale` is acknowledged without being claimed as new; `stalled`,
`failing`, `reauth_required` and `parked` name the state and link at the page
that can change it. An unresolved envelope paints no tile at all.

A source the instance cannot connect — a BYO provider with no client pair, a
shared-app provider with no app — now points at the credentials instead of
offering a "Connect" button onto a card that has none.
2026-09-11 09:15:32 +02:00
Marc-André Bombeck 77a09a5080 fix(onboarding): an empty multi-select is not an answer
Q2, Q4 and Q6 let "Weiter" write an answer with nothing ticked. On Q2
that answered "no areas", which is what switched every optional module
off for somebody whose click meant "no preference"; on Q6 it marked the
step done while recording no unit at all.

An empty selection now yields no body, so the primary button is dead
until something is chosen. Skip stays the way past, and Skip is the
conservative derivation the completion route already applies.
2026-09-11 09:14:34 +02:00
Marc-André Bombeck dec5732687 fix(onboarding): checklist queries follow its own setup rule
The card's visibility rule gained a "ran the setup flow" disjunct; the
component's query gate kept restating the older half of it. A wizard-run
account past five readings therefore saw a permanent checklist whose
medication, data-source, notification and insight rows were frozen at
"not done", because every supporting query was disabled.

Both places now read one exported predicate, isStillInSetup, which also
keeps the rows live for a restarted run nobody finished.
2026-09-11 09:07:41 +02:00
Marc-André Bombeck a2e407909c Never derive answers given for somebody else onto the guardian
"Finish without the profile" completed the flow with no record id, and
the route then derived the child's answers onto the guardian's own
record. The server now decides: answers given for somebody else with no
record to apply them to complete and latch the flow without deriving or
seeding anything, whatever the client sent. The checklist carries the
profile instead and reads none of those answers as the guardian's own.

A record id the caller does not guard answers 404 like every
id-addressed managed-profile route, one answer for missing, not managed
and not theirs alike; the contract documents the field, the 404 and the
422 text. Pinned by two integration cases and a second child journey.
2026-09-10 18:58:21 +02:00
Marc-André Bombeck 6adbee581e Apply the review of the setup flow: the child's answers reach the child
"Someone I look after" derived the child's answers onto the guardian's
own record. The completion now takes the managed record the profile
step just created, checks the same access rule the guardian's modules
route applies, and lands the same derivation and dashboard seed on that
record through the same record-keyed write; the guardian's own flow is
stamped complete and latched without touching its map. Three
integration cases pin it, and the child journey reads the child's map
through a real switch — it asserted the guardian's map before, which
pinned the wrong record.

Also from the review: the checklist's visit row read a meta the visits
list never publishes and now counts its body; the first-result tile
read the measurements list as an array and now shows the reading; the
demo allowlist admits the disclaimer and tour writes, with a structural
test over every onboarding route file; restart clears the old first
result, and the screen trusts the ledger before painting a result as
done; the headroom sweep's raw-key check matches the flow's own key
shapes; the demo migration script stops copying the dropped column; the
questions-to-pass rule and the shell's onboarding-path rule are pure and
pinned; the baseline form composes the shared action row; the schema
diff loses the unrelated formatting churn.
2026-09-10 18:19:34 +02:00
Marc-André Bombeck e98457151d Regenerate the OpenAPI document for the error-code catalogue
The catalogue lists the answers and restart codes and no longer the
retired step ones, and the generated document quotes that list. The
skippable-step id type nothing read goes with it.
2026-09-10 16:16:36 +02:00
Marc-André Bombeck 8c7cdcfc2b Tighten the setup flow after a review of the whole diff
The done screen moves focus to its heading like every other screen; the
confirm screen's preference list holds only dt/dd pairs and the settings
link stands beside it; Back from Q1 is not offered once the flow has
been entered, because the front door would resume past the welcome and
bounce straight back; the medication task records no target on the
ledger, as the contract says, and keeps the id on the screen; and the
two success affordances go through the outcome module rather than a
hand-rolled tick. The error-code catalogue lists the answers and restart
codes and no longer the retired step ones, and the session-surface guard
learns the one new deriving helper the onboarding pages read through.
2026-09-10 16:09:18 +02:00
Marc-André Bombeck ec82ad3a93 Carry the setup answers into the checklist, Settings and the module pages
The getting-started checklist reads the answers: a visit named within a
month adds a "prepare the visit" row that is done once one is on the
calendar, and the data-source row names the wearable that was chosen.
Its per-browser state moves to one module so the done screen, the skip
and Settings write the same keys.

Settings → Account gains the two ways back in: "Set up again" through
the restart route, which resets the ledger, keeps the answers as
prefill and touches no module; and "Show the checklist again", which
forgets what this browser hid.

A page whose module is off now answers with the shared notice and the
one action that helps, in one place for every module page: the shell
maps the route to the module through the navigation model's own gate,
so a page and its nav entry cannot disagree about which switch they
follow. And the tour's two dead stops get their anchors on the
integrations and health-record group headings.
2026-09-10 15:41:06 +02:00
Marc-André Bombeck a062610895 Retire the five-step wizard's route, goals and columns
The needs-based flow is the one setup flow now. POST /api/onboarding/step
and the six goal slugs go, and with them the two account columns the
wizard kept — its checkpoint and its goal list — through a migration
that drops them; onboarding_completed_at stays as the first-run
redirect's gate. The one durable effect the goals had, the dashboard
tile order, moves to the completion route: the answers now carry both
halves of Q2, which modules are on and what sits on top, and the seed
keeps the wizard's one contract of writing only while the layout column
is still unset.

The OpenAPI table loses the step path and the completion's description
says what the route does today; the demo-allowlist test names a
mutation that still exists.
2026-09-10 15:32:35 +02:00
Marc-André Bombeck 5a805e8d17 Add the setup flow's first-result screen
Exactly one task, by the step machine's fixed priority: connect the
wearable named in Q4, add the first medication, or log one reading for
the first area. The measurement form and the medication wizard render
inline; a connection goes through the integrations page, because the
OAuth handshake cannot run inside this screen, and the screen watches
the status envelope until it comes back connected. On completion the
screen shows what the task produced — the reading on its tile, the
medication with its next dose, the connection with its first sync — and
records the result on the ledger with the server's own instant.

The offer is not recorded on arrival: somebody who leaves mid-task
resumes here rather than on the done screen.
2026-09-10 15:24:32 +02:00
Marc-André Bombeck 58bc91a0a3 Add the setup flow's confirm screen
What the answers switch on is read off the registry's derivation and
never computed here, so the list the person confirms is the list the
completion route writes. Units, language and time zone are shown once
beside it, changeable in Settings, never asked again.

The profile half keeps the baseline form and its per-field refusals:
the fields are seeded from the account so a value it holds is confirmed
rather than typed again, and a refused field still stops the flow. For
"someone I look after" the managed-profile form takes the profile's
place, because that record is created here, after the questions; an
account without a second factor can finish without it and create it
later under Settings.

Confirming posts the completion, passes the first-result step outright
when there is no task to offer, and goes where the step machine says.
2026-09-10 15:19:48 +02:00
Marc-André Bombeck 7b279aec72 Add the six question screens of the setup flow
One question per screen, Q1 to Q6, each a chip set with a real radio or
checkbox inside every chip so the browser owns grouping and keyboard
movement, and a focus ring the five-step picker's hidden inputs never
had. The selection is pre-filled from the stored answers, "Next"
persists it through the answers route and navigates from the state the
server hands back, and Q1 is the one question without a skip.

The questions are data (which step, which answers, one or many, may it
be passed) with two pure translations beside them — stored answers into
a selection, a selection into the strict body — so the wire never sees
a chip this file did not offer. Q6 asks only about the units the ticked
areas need and sends nothing for a value the account already had.

The step page guards every URL with the step machine and stays under
/onboarding/, which is what the first-run redirect lets through.
2026-09-10 15:15:24 +02:00
Marc-André Bombeck 1c2e7c29de Add the setup flow's welcome screen and shell
The front door reads the same ledger every screen reads: a record that
never entered the flow sees the welcome, one mid-way is sent to the step
it owes, a finished one is offered the questions again. The shell prints
the counter and an ordered step list off the step machine, so it shows
exactly the screens this flow has, and every screen moves focus to its
heading on arrival.

"Skip for now" is a real exit — it stamps the account past the first-run
redirect and opens the checklist on the dashboard — and the one-time
medical disclaimer now gates it as well as "Set up", because a skip that
reached the dashboard without it would be a way round the acknowledgment.

The five-step wizard's welcome carousel, goal picker and source grid go
with it; the numbered step route follows in the next commit.
2026-09-10 15:10:50 +02:00
Marc-André Bombeck 13f11ed06c Add the pure step machine for the setup screens
Which screen is next, where a returning person resumes, whether a URL
may be visited, and what the counter says are all one function of the
published ledger and answers, so the page guard, the screens and the
shell cannot disagree. Q6 is in the order only while an area needs a
unit and the account does not already answer it; the first-result task
follows the spec's fixed priority (a connection the browser can finish,
then the first medication for a daily schedule, then one reading for
the first area) and is left out for a record somebody else will run,
because the three tasks write the caller's own record.

Tested before the screens exist; the two rules a screen would most
easily get wrong were broken on purpose and the tests went red.
2026-09-10 15:02:17 +02:00
Marc-André Bombeck 44b6ea01c6 fix(onboarding): tidy the small edges around the setup routes
Four of them. The confirm re-stamped its completion instant on every call, so
a replay quietly moved the moment the setup finished at while the derivation
beside it was carefully latched. The two list answers were stored exactly as
sent, duplicates included, so the column could hold something the reader would
never return — the reader de-duplicates on the way out and now so does the
write. The wide-event name for saving an answer used a different noun from its
two siblings, which makes a dashboard filter awkward for one feature. And the
contract said the restart body was required while the route accepts none.
2026-09-10 12:14:32 +02:00
Marc-André Bombeck b17f18295e fix(onboarding): let the getting-started list go when its rows are done
Three things, all in the same pair. The reader that orders the list from the
answers shipped without the proof that it reads correctly: every existing case
took the early return, so `checklistOrderFromNeeds` and the two new arms were
never executed by a test. They are covered now — medication first for somebody
who takes one daily, the named connection ahead of the untouched rows, profile
always first, and no answer combination that drops or duplicates a row.

A first-result task that was offered and then passed left the setup reading as
permanently unfinished, because the check asked only whether the offered task
had produced anything. A deliberate pass is an answer. And the published state
is additive — every record carries the field — so nine pending steps and empty
answers must read as "never entered the flow" rather than as an unfinished
one, or every account that predates the flow would look half set up.

The list itself now follows the design: for a record that ran the flow it goes
when its rows are done or the person hides it, and the five-reading rule stays
only for records that never entered.
2026-09-10 12:11:17 +02:00
Marc-André Bombeck cc6e6ca221 fix(onboarding): derive the module map only from a questionnaire that was finished
The only gate on the derivation was Q1 being set, so one answer was enough to
turn the whole questionnaire into a module map. Two ways that landed wrong and
they compound. Somebody answers who the record is for, closes the tab, and
later finishes the legacy four-step wizard, which posts to this same endpoint
and knows nothing about these questions: the five unanswered ones each read as
their conservative default, eleven surfaces are written off, and the
once-only marker makes it unrepeatable. Or any client patches one answer and
posts the confirm straight back.

The gate now asks whether every question has an answer or a deliberate pass.
A half-answered flow derives nothing, is not stamped complete, and keeps its
confirm step pending, so finishing the questions later still does the work.
The legacy arm is untouched: a caller with no setup row still answers the
fixed acknowledgement it always did.
2026-09-10 11:42:38 +02:00
Marc-André Bombeck 7a1f3fe32d fix(onboarding): stop the setup flow asking for units the account already holds
The spec's sixth question is skipped when the account already carries the
preferences, and the flow had no way to know: `User.unitPreference` is
nullable with null meaning "default metric", and the account payload coerces
that away, so nothing downstream could tell a chosen metric from an unset
column. The two columns are read raw on the server now, into the same
vocabulary the wire publishes, and a record whose account holds both reads its
`units` step as done — the question answers itself and is never put again.

Read-side only: the stored ledger keeps saying pending, because the person
really has not answered it, and an answer given later still lands.

The two writes the units answer makes — the display columns and the record of
the answer — go in one transaction, so a failure between them can no longer
leave the columns changed with no record of what changed them.
2026-09-10 11:39:32 +02:00
Marc-André Bombeck dbfad0f28e feat(api): publish the setup state on the account payload, and read it
Additive: `onboarding` carries the nine steps with their status, the answers as
given, the flow's own completion stamp and the one task it offered. Always
present — a record that never entered the flow reads as empty answers with nine
pending steps, so a client branches on the statuses rather than on whether the
field exists, and an unknown step id is skipped rather than refused.

It is resolved for the CALLER's own record rather than for the active one,
unlike the two module fields beside it. The three write routes are actor-only,
and publishing a setup state the browser cannot write would be a read it could
act on and never complete.

Both ends land here, because a field with no reader is the failure this
repository keeps rediscovering. The getting-started checklist orders its rows
from the answers — medication first for somebody who takes one daily, the
data-source row first for somebody who named a wearable — and it stays on screen
while the flow is unfinished. That is the second half: five readings were never
evidence that the setup finished, and now a pending step, an unconfirmed set of
answers or an offered task that produced nothing keeps the list where it is.

The ordering only applies once the answers are confirmed. Re-ordering the
dashboard under somebody who is still mid-question would be movement they did
not ask for.
2026-09-10 11:03:47 +02:00
Marc-André Bombeck cc98c56172 feat(onboarding): store what a record needs, and map it to modules
The setup questionnaire the design spec asks for needs somewhere to put its
answers. `OnboardingRecord` is that place: one row per RECORD rather than per
person, so a guardian running a managed profile's setup writes the child's row
and not their own, and the three JSON columns hold the answers, the nine
ordered steps with their status, and the one task the flow ends on.

Nothing here is encrypted, and the schema comment says why: the columns hold
closed enum vocabularies, not free text, which is the same class of value as
the module map and the goal slugs sitting in plaintext beside them.

The needs to modules map lives next to the module keys rather than in the flow,
because a module renamed or retired has to break the map at compile time rather
than leave a chip that quietly changes nothing. The derivation is total — every
directly-owned key gets an explicit value — and the merge beside it is
one-directional: what the answers name is switched on, what they do not name is
switched off only where the record does not already carry a deliberate on. The
two delegated keys are absent by construction: `cycle` is reported separately
because its real column is on the cycle profile, and the Coach is not offered by
this flow at all.

Readers are fail-soft. They sit on the payload that rides every app boot, and a
value written by a release that named an area this one retired must not take the
shell down with it.

Nothing is retired: the four-step wizard, its goal slugs and the one-time tile
seed keep working and keep their meaning until the new surface replaces them.
The two states are disjoint — the slugs decide tile order, these answers decide
which modules are on, and neither reads the other.
2026-09-10 11:03:24 +02:00
Marc-André Bombeck 542ca91be5 Finish the wiring polish: verbosity gate, upload flash, /about links, dead settings slugs
- The Coach verbosity picker is disabled while the concise tone forces
  brief, with a hint saying so, instead of a live control that silently
  does nothing.
- A fresh document upload flashes its new row like the duplicate path
  always did; the settle decision is a pure exported helper with a test.
- /about gains its two inbound links: the /privacy footer and a
  Settings→About credits row (the GeoLite2 EULA wants the attribution
  reachable from the running app).
- The eight settings slugs shadowed by permanent redirects (the seven
  layout children + sharing) leave the slug registry: no more
  prerendering unreachable pages, no dead component wirings, and the
  SectionPlaceholder fallback that could never render is gone. The
  redirects, section components, and layout-hub subpages are unchanged.
2026-08-14 05:31:33 +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 0160052289 refactor(settings): retire Research Mode — the switch governed nothing
Research Mode was the opt-in behind the estimated GLP-1 drug-level curve:
turn it on, read a disclaimer, get the chart. The chart stopped consulting
the flag several releases ago and has painted for every account since, so
the switch on Settings → Advanced changed no screen in either position. An
inert control that promises to hide a chart is worse than no control.

The curve is simply part of the medication page now. Gone with the switch:
the acknowledgment dialog, `/api/auth/me/research-mode`, the shared status
DTO and its gate helper, the disclaimer-version stamp, the query key, and
the three user columns the acknowledgment wrote (migration 0321). The
Privacy page's second button pointed at the retired control and goes too.

Settings → Advanced is now what it reads as: reset the data, delete the
account.
2026-08-08 15:17:05 +02:00
Marc-André Bombeck 2a396dd23e fix(backup): carry the aggregation provenance through backup and restore
The disaster-recovery backup selected every Measurement scalar except the
aggregation fields, so a restore brought every `stats:` aggregate back with no
provenance and the Apple Health authority ladder came back flat: an export.xml
source-day estimate could then overwrite a native HealthKit statistic.

`aggregationProvenance` now rides the select, the disaster-recovery
serialization, the payload schema and the restore mapping, following
`externalSourceVersion` exactly — including staying out of the reduced
portable shape, which carries the human-readable subset only. The schema field
is optional, so every backup written before this parses unchanged.

Restoring one of those older backups no longer recreates the ambiguity
migration 0263 repaired: an Apple Health row under a `stats:` identity with no
provenance comes back as LEGACY_UNKNOWN, the sentinel the cumulative drain
already writes in that situation, so a native sync or archive reprocess can
repair it. The gate is scoped as 0263 scoped its backfill — other providers
mint `stats:` identities for their cumulative dailies and have never carried
provenance, and ordinary point rows keep NULL.
2026-07-25 15:20:47 +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 a9892e9586 refactor(checkups): rename the preventive-care slug to /checkups
Move the preventive-care surface from the German-derived slug /vorsorge to
the English /checkups and make the feature read consistently across locales.
The route directory, the nav entry, the dashboard card link, and the
onboarding tour step now point at /checkups. A permanent redirect keeps old
bookmarks, the PWA's cached navigation, and push deep-links resolving.

Display label is now "Checkups" in English and the equivalent term in
Spanish (Revisiones), French (Examens), Italian (Controlli), and Polish
(Badania); German keeps "Vorsorge". The internal id stays "vorsorge" so
saved layouts, module-order preferences, and tour state keep resolving
without a data migration.
2026-06-27 12:48:04 +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 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 8ff3b51ba3 refactor: remove dead exports and an unused prop
Drop the health-score card's inert onAskCoach prop and its now-dead
pass-through from the hero strip, the buildDashboardBands re-export that no
caller reached through the snapshot module, an unused tour-progress status
type, and an orphan service-worker cache helper.
2026-06-18 22:42:27 +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 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 59e7b33285 feat(onboarding): add resumable module-tour progress field + API
Add nullable onboarding_tour_progress_json to users (migration 0173),
a Zod-validated progress shape, and extend POST /api/onboarding/tour to
accept a mid-tour resume checkpoint and clear it on replay. Surface the
resume point on GET /api/auth/me and register the contract in OpenAPI so
the native client mirrors it.
2026-06-18 19:27:28 +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 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.
2026-05-23 12:51:41 +02:00
Marc-André BombeckandClaude Opus 4.7 e57fc0a8da feat(onboarding): tour component with spotlight + keyboard navigation
v1.4.15 Phase B5 — second commit. Adds the spotlight-tour overlay
that walks new users through 5 dashboard features after they finish
the wizard at /onboarding:

  1. Tile strip            (BP / weight / pulse / mood)
  2. Quick-add menu
  3. Insights              (AI-assisted summaries)
  4. Settings → Integrations
  5. Achievements          (omittable via prop if B4 hasn't shipped)

Architecture:

  - `src/lib/onboarding/tour-state.ts` — pure state machine. Steps,
    navigation helpers (`nextStep`, `prevStep`, `skipTour`),
    completion outcome (`"completed" | "skipped"`). DOM-free so the
    Node-environment vitest setup can exercise it directly. 13 unit
    tests cover step list, navigation, idempotency once finished,
    and the 1-based step counter.

  - `src/components/onboarding/tour.tsx` — UI layer. In-house
    spotlight (no new dependency — package.json had no
    react-joyride / intro.js / shepherd already, and the surface
    area didn't justify a 30 KB add). Renders a fixed full-viewport
    overlay with a polygon `clip-path` cutout aligned to the live
    target, plus a tooltip card next to it. Targets are looked up
    via the stable `data-tour-id` attribute (NOT class names — the
    contract has to survive Tailwind churn). Re-measures on resize
    and scroll. Falls back to a centred-screen tooltip if the
    target is not in the DOM (mobile breakpoint, conditional
    render) so the tour never blocks indefinitely.

  - i18n: 19 new keys under `onboarding.tour.*` (EN + DE), one
    title + body per stop plus `next`/`back`/`skip`/`done`/
    `restart`/`stepOf` strings. The component test asserts the
    German strings resolve end-to-end via `<I18nProvider initialLocale="de">`.

Accessibility:

  - `role="dialog"` + `aria-modal="true"` + `aria-labelledby`,
    polite live region announces step transitions, focus moves to
    the primary action on every step.
  - Keyboard: Esc = skip, ArrowRight / Enter = next, ArrowLeft =
    back. Inputs / textareas / contenteditable nodes are
    explicitly NOT hijacked (defensive against future tour content
    that might embed an input).
  - `prefers-reduced-motion` disables the backdrop fade.

Tests: 917 / 917 unit pass (was 890 — +27 across 2 new files).
Lint clean (13 pre-existing warnings unchanged). Typecheck clean
for B5 files; dashboard-layout / sibling-agent typecheck issues
are pre-existing and out of scope.

The component is "dumb" — it takes `onClose(outcome)` and lets the
caller persist the flag. The launcher wiring lands in the next
commit (B5-3) so this commit is small and orthogonal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:19:18 +02:00
Marc-André BombeckandClaude Opus 4.7 3221f86eea feat(settings): introduce dynamic-route shell + redirect /settings → /settings/account (#139)
* feat(settings): introduce dynamic-route shell + redirect /settings → /settings/account

Splits the 3218-LOC /settings monolith into a routing surface only — no
section content moves in this PR. Each of the eight slugs (account,
integrations, notifications, dashboard, ai, api, advanced, about) now lives
at its own /settings/[section] route and renders inside a new
<SettingsShell> sidebar+main layout. Every section currently shows a
placeholder ("Moving here with the next release update") so reviewers can
validate the URL structure in isolation. The historic monolith is preserved
verbatim as page.legacy.tsx for section-by-section extraction by the
follow-up PRs (A2-account, A2-about, A2-ai, A2-integrations,
A2-notifications, A2-rest).

Behaviour change: existing entry points (sidebar nav, topbar dropdown,
onboarding hand-off, dashboard checklist, Withings callback, Codex
callback) now point at /settings/<slug> instead of /settings#anchor. The
/settings root is a 308 (permanentRedirect) to /settings/account so old
bookmarks and external links keep working.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): route every Codex OAuth callback redirect to /settings/integrations

Review-pass H1: only the late success/failure paths in the Codex OAuth
callback were updated to point at the new section route. The four early
branches — rate-limited, provider-supplied `?error=...`, missing params,
invalid state — still redirected to `/settings?codex_error=...`.

After the section split that path 308-redirects to `/settings/account`,
where the Codex error toast/banner doesn't live. Users hitting any of
those edge cases would land on the wrong screen with the error code
silently lost.

All six branches now go to `/settings/integrations?codex_error=...`,
matching the success and token-exchange-failure paths and the existing
Withings callback shape.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:15:15 +02:00
Marc-André BombeckandMarc-André Bombeck f4a2e13985 feat(v1.4): onboarding redesign — dashboard-first checklist, 3 focused steps, inline trend hints (#131)
Redesigns the first-run experience around three patterns we see in
Linear, Vercel and Notion: a short focused wizard, a persistent
getting-started checklist on the dashboard, and contextual hints
inside still-empty charts.

- Onboarding shrinks from 4 steps (one was marketing-only) to 3:
  About you (display name + locale + height + DOB + gender) → log
  your first measurement (real action via the existing form) → pick
  one notification channel (or skip).
- Every step except the first now has a Back button and exactly one
  Skip link with the honest "you can finish setup later from
  Settings" tail. The legacy duplicate Skip is removed.
- A new dashboard "Getting started" card lists the five quick wins
  (profile, first measurement, medication, Withings, notifications).
  Each row links to the right place. Items can be hidden one-by-one
  or the whole card can be dismissed; both states persist in
  localStorage. The card auto-hides once you finish onboarding and
  log your fifth measurement.
- Charts that have ≥1 but <5 readings now show a subtle inline hint
  ("First trend after 5 readings — N more to go") so the user knows
  the line will appear soon.
- Display name is captured during onboarding and saved to the
  profile.

All new strings are translated to English and German via the
existing i18n pipeline.

Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
2026-05-08 12:42:48 +02:00