Commit Graph
2167 Commits
Author SHA1 Message Date
Marc-André Bombeck 32eaf7a29e fix(admin): show each channel's cause in the notification test toast
The admin test response carried a code and status per channel that nothing rendered. The toast now lists each failing channel with the translated cause and the HTTP status or SMTP code. The relay reply is no longer part of that response, and the ntfy error no longer echoes the topic, which the sender treats as a secret.

Refs #947
2026-09-13 14:50:33 +02:00
Marc-André Bombeck a11e5991cf test(settings): read a real failure response through the test button
The fetch and envelope reading move into runConnectionTest, which the button calls. A test stubs the fetch with the 502 envelopes the notification test routes send and asserts the rendered code, HTTP status, SMTP code and relay reply, so dropping the meta extraction fails it.

Refs #947
2026-09-13 14:50:32 +02:00
Marc-André Bombeck fafb5cdc08 feat(notifications): add a Gotify payload format to the webhook
Gotify's POST /message types priority as an integer, so the generic body, which sends the word, was refused with a 400 before the token was ever the question. The webhook card now offers a payload format: generic, which stays byte for byte what it was, and gotify, which sends title, message, an integer priority and extras.

Priorities follow the Gotify Android client's channel thresholds: 5 for routine events (audible), 8 for medication reminders (the lowest heads-up value), 10 for urgent events. The extras declare plain text and carry the event name, masked to reminder in discreet mode as the generic body does.

The choice lives in the encrypted channel config, so no migration: an absent format is generic, only gotify is written, and a save that omits the field keeps the stored choice. GET returns the resolved value. The sender comment no longer suggests an Authorization Bearer header for Gotify, which takes X-Gotify-Key or a token query parameter.

Refs #947
2026-09-13 14:19:48 +02:00
Marc-André Bombeck 92606f6409 feat(settings): show the status and reply of a failed channel test
The test-connection callout now appends the HTTP status or SMTP reply code the route reports, and shows what the relay answered on a line beneath it. The reply is rendered as a React text child, so markup in it is shown as characters.

Adds the upstream_rejected message in all seven locales.

Refs #947
2026-09-13 14:13:55 +02:00
Marc-André Bombeck 4460047910 Merge the coach cap counting only the tokens the operator paid for
Background insights can no longer use up the day before somebody asks a question; a fifth of the daily limit is held back for what a person does by hand.
2026-09-11 14:18:21 +02:00
Marc-André Bombeck 82d4a32726 fix(onboarding): the path CTA breaks onto two lines at phone width
The connect step's new attention and credentials tiles link to the
integrations page with a label that names a path — "Open Settings →
Integrations". The button base is `whitespace-nowrap` with a fixed height and
`shrink-0`, so at 390 px the German and French renderings could neither shrink
nor wrap: the control ran 27 px past the viewport. The pre-existing connect
card was unaffected, its label being a short verb phrase; the path label and
the tiles that carry it are new, which is why the sweep passed before.

The CTA is bounded to its card, allowed to break between the words of the
path, and allowed to grow for the second line, with the 44 px tap target kept.
No copy moved, so the seven catalogues stay as they are.
2026-09-11 12:57:10 +02:00
Marc-André Bombeck b565edc48d docs(ai): drop planning vocabulary from the ledger comments
CLAUDE.md forbids planning shorthand in committed artefacts, and this branch
introduced 74 lines of it across source, tests, a schema field and two
migrations. Someone reading the migration SQL in a year has no way to resolve a
wave letter or a round number, and a comment that points at something outside
the repository names no reason at all.

Three passes. A version followed by a parenthetical keeps the version and loses
the parenthetical, because the version says since when something holds. A
sentence that leaned on the vocabulary to carry its meaning is rewritten to
state the reason directly: the arrival reservation now says it was refused over
money the operator never spent, rather than naming the defect a wave existed to
remove. Three test titles carried the marker in the name itself and are renamed
to what they pin.

Comments only. No behaviour, no renames beyond those three titles.
2026-09-11 12:37:54 +02:00
Marc-André Bombeck 45599c527e fix(demo): the profile route writes only the baseline fields in the demo
Allowlisting `PUT /api/auth/profile` so the setup flow's baseline step could
finish also handed every visitor the shared demo account's contact email, its
display name, its full name and its insurer fields: the allowlist admits a
path and a method, and every field the handler behind it writes comes with
them. The justification beside the entry named three fields; the entry
admitted eleven.

The write is narrowed on the server, in `applyProfileUpdate`, to height, date
of birth and sex while `DEMO_MODE` is set — not in the client, which is the
party the allowlist exists to distrust. It happens ahead of the email conflict
check, which would otherwise answer "is this address registered here?" to
anybody who can open the demo. The display-name box goes read-only there, as
the anamnesis card already is, so nothing accepts input it will discard.

The entry's comment now lists what the handler writes and what is admitted.
2026-09-11 12:22:47 +02:00
Marc-André Bombeck ceefbc5571 fix(onboarding): a receipt on file is not evidence the provider works
The consent branch of the done screen's AI panel painted "insights work for
you right now" on the strength of the receipt alone, reading no health at all.
A person who had consented — including one who had just tapped the offer — was
told a dead provider was serving them. That is the claim-nobody-measured
defect closed at the offer end of the same panel, still open at the other.

The positive sentence now needs a healthy verdict as well. A consented account
on a provider that failed reads the same "not answering" note as anybody else,
and on an unmeasured one the panel claims nothing. The panel's four variants
are pinned across all three health states in every arm.
2026-09-11 11:38:31 +02:00
Marc-André Bombeck 34caeeea3c test(onboarding): the AI panel names the variant it settled on
The panel paints before its status query answers, and every variant inside it
is conditional, so an assertion that a variant is absent could pass on a
question that had simply not been answered yet — which is how the journey for
the unmeasured-provider case passed on a retry against the very defect it was
written to catch.

The panel now carries the variant it settled on, absent until it has settled,
and the three journeys wait on that instead of racing it. The third journey
covers the state a freshly deployed instance is in before its first AI call:
no offer, and no claim of an outage either.
2026-09-11 11:15:39 +02:00
Marc-André Bombeck c66bcbc295 feat(admin): the AI card shows the day's spend split by who paid
The provider-health route answered a spendToday payload that nothing rendered,
which is the pattern the two-ended rule exists to stop. It is also the screen
the operator opens when a surface refuses on budget, and the one mixed number
could not answer the question that raises: a day at 1.24 million tokens is
alarming until the card says that 151 200 of them were the instance's own key
and the rest ran on the users' own plans. The operator ceiling is enforced
against the second figure. Copy lands in all seven locales.
2026-09-11 11:11:17 +02:00
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 97174f470d fix(demo): the shared demo account takes no free text and no new record
The demo allowlist had grown two entries it should not carry. The demo is one
published account every visitor signs into, so `PUT /api/coach/about-me` was
an anonymous write of arbitrary text into the Coach prompt, persisted for
every later visitor until the next reseed; it is gone, and the baseline step's
anamnesis card renders read-only under `DEMO_MODE` so no visitor meets the
refusal as a failed confirm. `POST /api/managed-profiles` bought nothing: the
route opens with a fresh-MFA requirement the demo account cannot satisfy, so
the guardian arm was limited to finishing without the profile either way.

The baseline profile write stays, and the block's preamble now describes what
is actually admitted and why the two refusals are refusals.
2026-09-11 10:57:42 +02:00
Marc-André Bombeck e991fdf65d fix(onboarding): a guardian is not told their own modules are leaving
The confirm screen derived "Out of the navigation: …" from the acting
account's module map and rendered it in every arm. In the "someone I look
after" arm the completion never touches that account: it applies the
derivation to the managed record, or, when none was created, to nothing at
all. So a parent answering for a child was told Workouts, Cycle and Documents
were leaving their own navigation when nothing was going to move.

The arm now derives the list from an empty map, which makes it empty, and the
sentence disappears with it.
2026-09-11 10:54:14 +02:00
Marc-André Bombeck 65a62350ea fix(onboarding): the AI panel claims an outage only when one was seen
The done screen folded the `unknown` provider verdict into `unhealthy` and
painted "This instance's provider is not answering right now." `unknown` is
an empty `provider_health` table, which is the state of every instance until
its first AI call — so the most common path through the screen made a claim
about something nobody had measured. It now falls to the neutral panel, which
says nothing either way; the offer is withheld in both cases as before.

The projection itself drew the same unearned conclusion from staleness alone:
a quiet instance whose last success was two days old read `unhealthy` without
any failure behind it. A verdict now needs an observed one — a row whose last
outcome was a failure — and a merely old success reads `unknown`.
2026-09-11 10:53:41 +02:00
Marc-André Bombeck 1885b7867d Merge wave D — the shared provider is offered only when it is known to work
The done screen's assistant panel stops claiming that the instance's
provider already serves the person. A tri-state read over the operator's
own provider ledger decides between a one-tap offer that mints the
consent receipt, a plain "not answering right now" note, and today's
setup link; anything unknown, any read error, a managed profile and an
operator who has switched the assistant off all fall to no offer.

# Conflicts:
#	e2e/setup/global-setup.ts
2026-09-11 10:25:19 +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 3642bbe4de Merge wave C — the flow cannot strand, mislead or silently switch off
The getting-started checklist's data queries follow the same setup rule as
its visibility, so a settled account stops being told to do what it has
done; a failed completion leaves the confirm screen usable; an empty
multi-select is no longer recorded as an answer; the confirm screen names
the modules a run would take out of the navigation; the settings links it
offers work before the first completion; and demo mode can fill the
baseline it is shown.
2026-09-11 10:24:48 +02:00
Marc-André Bombeck 0290554d36 test(e2e): an invite link registers from a cookie-less window
Every layer of the invite chain had unit coverage and the chain was broken
anyway: the proxy test proved the edge admits the landing, the page test proved
it redirects onto the registration form, and neither could see that the
redirect arrives in an RSC flight payload after the shell has already flushed.
The operator's path — mint an invitation, open the URL in a private window,
reach a usable form — was exercised by no spec at any level.

The new spec mints a real invitation in the admin UI and opens it twice: once
in a genuinely cookie-less context, asserting both where the visitor lands and
that they never passed through the sign-in page, and once in the admin's own
window, asserting the explaining panel and that the admin's session survives. A
third case pins the server half through the wire. Against `origin/main` all
three fail — the anonymous visitor lands on `/auth/login`, the panel does not
exist, and the registration POST answers 201 over a live session.

The minted URL gains a test id so the spec reads it from its own element
rather than from the first paragraph in the ticket.
2026-09-11 09:40:26 +02:00
Marc-André Bombeck a6dedffdb8 fix(onboarding): no dead settings links before completion
Both Settings links on the confirm screen are page routes, and the
onboarding cookie that sends every page route back into the flow is
cleared by the completion this very screen calls. On a first run they
could not go anywhere.

They now render as plain text until the flow has been completed once.
The words stay, because where a setting lives is true either way.
2026-09-11 09:25:28 +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 be7fbe37d5 docs(access): the grant card says the invitee needs an account
A sharing invitation carries no link and nothing is e-mailed — it is addressed
to an account that already exists on this instance, and the person accepts it
from inside their own session. The card's input hint already named the account
requirement and left the rest unsaid, so an inviter looking for a link to send
had nothing to find and no reason to stop looking. The card now names where the
invitation turns up, in all seven locales.
2026-09-11 09:16:24 +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 74ae672373 feat(onboarding): the done screen offers the provider only when it works
The card read one field and made two claims from it: that insights work,
and that no setup is needed. The first was a presence read - it knew a key
existed, never that it answered - and the second skipped the consent
receipt the egress actually needs, so the person who believed the note met
a consent.ai.required refusal on their first briefing.

Four branches now, one button. The offer appears only when the server says
the shared provider is known to work and nothing forbids it, and the tap
posts the affirmative web intent. An unhealthy or unknown verdict paints
'not answering' with no button. When the offer is refused for a reason that
is not health, the panel stays neutral rather than blaming the provider.
The note survives for the one state it was ever true of: consent on file.

The sample, the ladder, the 'fully useful without' line and the setup link
are identical in every branch. Copy in all seven locales.
2026-09-11 09:13:23 +02:00
Marc-André Bombeck 0e79b195b0 fix(onboarding): a failed completion leaves confirm usable
The baseline form raised its pending flag and lowered it on only two of
three exits; the success path went through the confirm screen's finish(),
which caught its own error and returned normally. A failed completion
therefore disabled both buttons until the page was reloaded.

The reset now lives in a finally, finish() rethrows so the form sees the
failure, and the two managed-arm buttons report it themselves. The step
passes before the completion run off a ledger ref, so a retry after a
failure midway no longer re-sends a step the ledger already carries.
2026-09-11 09:12:36 +02:00
Marc-André Bombeck 0bdbf009f6 fix(integrations): a provider with no history is "unknown"
The ledger's synthetic "no row" snapshot returned `state: "connected"`, so
the field's default meant the opposite of its name: every account that had
never connected a provider published `connected` on the status envelope.
Settings survived it by never reading `state`; the setup flow's connect step
did read it, and told brand-new accounts their wearable was connected.

The snapshot now reads `unknown`. `syncHealth.verdict` is unchanged and stays
the only liveness truth; the managed-record projection maps `unknown` onto
`disconnected` the way it already maps a missing connection.
2026-09-11 09:08:38 +02:00
Marc-André Bombeck 753438a329 fix(auth): the invite landing is public in the client shell too
`/invite/<token>` has been public in `proxy.ts` since v1.17.0 and was never
added to the client-side gate in `auth-shell.tsx`. Next streams the shell
before the landing page's RSC redirect resolves, so the shell classified the
route as protected, `/api/auth/me` answered 401, and `router.replace(
"/auth/login")` won the race — the operator's invite link landed on the sign-in
page it was meant to bypass.

The shell now lists `/invite/`, and lists the `/auth/` family as the prefix the
edge already admits rather than as two individual pages, so a future auth page
cannot inherit the same defect. `PUBLIC_PATHS` is exported from `proxy.ts` so a
new parameterised guard can render the shell at every page prefix the edge lets
through and fail when the two lists drift apart again.
2026-09-11 09:08:07 +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 677019b53b fix(onboarding): let the confirm screen's sex select shrink at 390 px
The string-headroom sweep went red on CI for French at phone width by
two pixels the Mac never showed: the runner's fallback fonts render a
touch wider. The box was the sex select on the confirm screen — its
value is whitespace-nowrap, and a grid item's automatic minimum is its
min-content, so the track was pinned at the padded option's width and
the shell scrolled sideways. Both cells of that row and the trigger
carry min-w-0 now: the track takes the free space and the value
ellipsises, which is the select primitive's own design. Reproduced and
verified locally by narrowing the viewport to 372 px, which stands in
for the wider fonts (336 px of scroll before, none after).

The sweep now names what overflowed: the boxes past the shell's edge,
with tag, slot, classes, text prefix and edges, ride the failure
message and the outside list, so the next red run says what rather
than only that.
2026-09-10 23:50:30 +02:00
Marc-André Bombeck 8ed46eef58 Merge remote-tracking branch 'origin/main' into feat/onboarding-wizard 2026-09-10 20:53:38 +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 9c306f04e7 fix(notifications): name the never-grantable refusal and redact the grant log
A loopback, link-local or metadata target answered
private_origin_not_approved with a message telling the user to have the
operator list it, and the parser refuses such an entry forever: a dead
end the route test even pinned. Those targets now answer
private_origin_not_grantable (catalogued, on all four routes, translated
in seven locales) with a message that says no grant can open them and
to use the relay's LAN address. Both senders carry the reason into the
outcome so the test button forwards the same code.

The malformed-entry warning printed the entry verbatim, which is where
a pasted token or userinfo sits; the parser now hands callers scheme
and host only, plus the reason in words. The Nightscout list throws a
named error with that redacted entry and reason, and the connect route
answers it as a 422 with the message instead of a bare 500, so a
self-host that listed http://localhost:1337 reads what to change.

The two settings cards translate a coded save refusal like the test
button does; the docs and the changelog name the sub-host and sibling-
port narrowing; the dispatcher floor tests assert the policy marker so
a real NXDOMAIN cannot pass for a refusal.

Refs #947
2026-09-10 18:45:57 +02:00
Marc-André Bombeck 55ff6df205 Merge remote-tracking branch 'origin/main' into feat/onboarding-wizard
# Conflicts:
#	docs/api/openapi.yaml
#	src/lib/openapi/error-codes.ts
2026-09-10 18:20:07 +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 5c5a592177 Let the setup flow's action rows wrap, and measure headroom per word
At 390 px with labels a third longer the action row pushed "Confirm and
continue" past the viewport; both rows now wrap. The string-headroom
sweep lengthens every word by 30% rather than hanging the whole surplus
on the last one — that manufactured an unbreakable run no locale
produces — leaves screen-reader-only boxes alone, and accepts a select
value's ellipsis, which is the primitive's design app-wide.
2026-09-10 16:49:18 +02:00
Marc-André Bombeck d91a8dfebe Make the setup flow pass its own journeys and axe sweep
Run against the standalone build, the four journeys and the axe sweep
surfaced five things the unit tier could not see. The completion route
derives only once every question is settled, and Q6 stays pending for
somebody with no unit-bearing area — the confirm screen now passes the
questions the flow never showed before it completes. The dashboard seed
matched a JSON null where the unset column is a SQL NULL, so it never
landed; the retired step route carried the same filter. The auth shell's
onboarding branch matched only the front door, so every question screen
rendered inside the full app chrome with the sidebar. Inline links
relied on colour alone, the step list sat outside any landmark, and the
baseline form brought a second h1 onto the confirm screen. The two
sweeps run serially, since each drives one account through the API.

The integration file gains the seed case that would have caught the
null filter.
2026-09-10 16:38:47 +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 d37993822e fix(admin): keep the off-host freshness badges readable in both themes
The destructive tone as 12px badge text on its own wash measures 4.08:1 in
the light theme and 3.99:1 in the dark one; the muted foreground on the
muted wash 3.6:1 in the dark one. The badge takes the shape the status rows
on the same console already use: the token paints a small indicator and the
wording stays in the foreground colour. The word states the condition, so
nothing hangs on the colour alone.
2026-09-10 16:00:38 +02:00
Marc-André Bombeck 2686da2803 Add the setup flow's browser journeys, axe sweep and string-headroom check
Four journeys, one account each, from the welcome screen to the
dashboard: blood pressure and weight typed in, a daily medication added
inline, a visit within a month with labs, and a child's managed profile
created on the confirm screen. Each asserts what the walk wrote — the
module map, the tile order, the checklist, the settled ledger — and the
first proves that leaving after Q3 resumes at Q4.

Two sweeps on accounts of their own drive the state through the answers
route and visit every screen by URL: axe at both widths plus one
keyboard-only answer, and every locale with every text node lengthened
by 30% before the page, the shell, every clipping box and every control
are measured. All six run in one project because each mutates the
account it walks.

The medication wizard learns to stay where it was mounted after a
create, which the first-result screen needs and the medications list
does not.
2026-09-10 15:57:41 +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 b446911d7d Add the setup flow's done screen
The dashboard is one click away, already ordered, and the checklist
opens on arrival. The tour is an offer here rather than an automatic
launch: pressing it sets the referrer the shell's launcher reads, and
nothing else does. Two record-shaped exits follow the answer to Q1: an
account that said "both" is offered the profile for the person it looks
after, and one that set up a record for somebody else is offered that
record through the same switch the account menu uses, so the first
entry lands where it belongs. The step is marked done on arrival, once.
2026-09-10 15:28:16 +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 12d6aa3078 Merge the scoped-grant fences, the delegate controls that ask their own section and the published switch refusals 2026-09-10 12:29:09 +02:00
Marc-André Bombeck 83b7cd223a docs(sharing): the booster needs two sections, and the guide promised it at one level
The write-level row listed "a vaccination dose and the booster reminder it
suggests" three paragraphs under a section table that puts the vaccination
record in Health background. Read together, that tells a self-hoster a
health-background WRITE grant can plan a booster — which the route beside it
now refuses, because the reminder it mints lives under Readings. The row lists
the dose alone, and the exceptions paragraph, which is where a reader looks for
exactly this, carries the two-section case and says the dose logs either way.

With it, the small things the same pass turned up:

The cycle-insights fixture asserted a property it could not show. Its docblock
claims the cycle-native answer survives the fence, and it seeded no symptom at
all, so `symptomPatterns` was empty in every leg and a regression that emptied
it along with the foreign columns would have passed green. One catalogue
symptom on the luteal days, asserted on the owner leg and on the cycle-only
one.

The immunization rows matched on a bare `role="button"`, which passes vacuously
the day the card becomes a real button and fails for the wrong reason if
anything else in the subtree grows one. They carry the same stable marker the
visit card took.

`/api/cycle/calendar` reads the same wrist-temperature channel the crosstab
does and is deliberately not fenced, and it was the only one of the pair with
nothing saying why: no value from it reaches the response.

The encrypted export's new 403 documented the switch refusal and not the
narrow-scope one it answers first, unlike the two settings operations beside
it. Both sheets say at the top that their Save and Delete are gated at the
caller, so a third mount reads the constraint before it violates it. And one
docblock line ran past the wrap.
2026-09-10 12:23:59 +02:00
Marc-André Bombeck 3017e24bb1 Merge the needs-based onboarding's server state and the derivation that never hides data 2026-09-10 12:23:34 +02:00