Class-order + line-length normalisation on tour.tsx, mock-client.ts,
and codex-slug-fallback.test.ts after the focus-trap / strict-schema /
inspect-helper-removal commits. No functional changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase D design review HIGH cluster (H1, H2, H3) on the v1.4.15 B5
onboarding spotlight tour:
- H1: tab cycles inside the tooltip's three actions (Skip/Back/Next),
shift+tab honoured; on close the previously-focused element regains
focus so Settings -> Account "Restart" returns the caret correctly
- H2: tooltip card gains `max-h-[80vh] overflow-y-auto` so DE copy
with longer translations can scroll inside the card without ejecting
the Next/Done button below the fold on small viewports
- H3: backdrop button gets `focus-visible:ring-2 ring-primary` so
keyboard users see a visible focus signal when the tour is active
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.15 Phase B5 — third commit. Wires the spotlight tour
(`<OnboardingTour>` from previous commit) to auto-fire for new users
the first time they land on the dashboard, persists completion to
the DB, and exposes a window-event hook for replay.
Pieces:
- `<TourLauncher>` (`src/components/onboarding/tour-launcher.tsx`)
— gates and mounts the tour. Self-decides via:
* `user.onboardingTourCompleted` (DB flag from migration 0030)
* sessionStorage dismiss guard (defensive against in-flight
`/api/auth/me` refetches re-opening the tour after Skip)
* post-wizard 1500ms grace window (avoids the wizard's
`doneToast` and the spotlight stacking on the same z-layer)
* a `ready` prop the dashboard sets once analytics has
resolved (so the spotlight cutout doesn't snap to a 0×0
placeholder before tiles render)
Listens for `healthlog:tour-restart` to force-reopen.
- `POST /api/onboarding/tour` — single endpoint with body
`{ completed: boolean, outcome?: "completed" | "skipped" }`.
Flips `users.onboarding_tour_completed`. Dispatches a Wide
Event annotation with the outcome so Loki can split completion
from dismiss without a second column.
- `/api/auth/me` now returns `onboardingTourCompleted`. The
`AuthUser` type in `src/hooks/use-auth.ts` gains the field;
no consumer outside the launcher reads it today.
- `data-tour-id` attributes added to:
* dashboard tile-strip wrapper (`dashboard-tile-strip`)
* dashboard "Add" dropdown trigger (`dashboard-quick-add`)
* sidebar nav items (`nav-insights`, `nav-settings`,
`nav-achievements`, plus the rest for symmetry —
targeted attribute, not class names, so renames in
Tailwind classes don't silently break the spotlight)
- The `/onboarding` wizard's `persistAndExit()` writes
`sessionStorage["healthlog-tour-referrer"] = "1"` immediately
before navigating to `/`. The launcher consumes-and-clears that
on first mount via a `useState` lazy initialiser (presence-only
flag — no `Date.now()` so the render-phase decision stays pure
under `react-hooks/purity`).
ESLint compliance:
- `react-hooks/set-state-in-effect` — no synchronous setState in
a useEffect body. The launch decision happens in the render
phase using the React-recommended "previous input id" guard
(mirrors `account-section.tsx`'s `seededUserId`), and the
post-wizard timer's setState lives inside a setTimeout
callback (an event handler, not the effect body).
- `react-hooks/purity` — render-phase setState only branches on
prop values + sessionStorage-presence helpers. No `Date.now()`
/ `Math.random()` / fetch in render.
Tests: my 39 unit tests pass (was 27 — +12 across tour-state and
tour SSR shape). Pre-existing test failures in
`doctor-report-data.test.ts` are uncommitted B6 work in the
working tree, not B5. Lint clean for B5 files; one pre-existing
B6 lint error in `doctor-report-dialog.tsx` is out of scope.
Typecheck has the same pre-existing `dashboard-layout.test.ts`
errors documented in STATE.md (A4 / phase-D scope).
Restart UX (Settings → Account → "Restart tour") lands in the next
commit (B5-4) so this commit stays orthogonal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Two bugs fixed at once. The previous version of the dashboard onboarding
checklist (a) flashed for ~500 ms on every page-load even for fully
onboarded users, and (b) auto-rendered expanded, pushing the real
dashboard tiles below the fold each visit.
Flicker root cause: the component decided render/no-render off
`shouldShowChecklist({ measurementCount, ... })` while `measurementCount`
was still 0 because the analytics query was in flight. For a user with
30 measurements + a non-null `onboardingCompletedAt`, `stillInSetup`
flipped from `true` (load) to `false` (post-fetch) — exactly long
enough to be visible. Fix: refuse to render until
`analyticsQuery.data !== undefined`. Tanstack-query writes `data`
exactly once per fetch, and the dashboard's `/api/analytics` query
shares the cache key, so the checklist almost always reads cached data
synchronously and never blocks UI.
Auto-open: introduce an `expanded` state, default `false`, persisted
to `localStorage[healthlog-getting-started-expanded]` so a deliberate
expand survives reloads but new users see a collapsed shell. Header
becomes a chevron toggle (`aria-expanded` / `aria-controls`); the
progress meter stays visible in both states so a glance still shows
how much setup is left.
`e2e/onboarding-flicker.spec.ts` covers both contracts: a
sample-every-50ms loop during a deliberately-slowed analytics fetch
proves the card never paints for a complete-onboarding user, and a
second test mounts an incomplete user, asserts `aria-expanded=false`
and that `#getting-started-body` is NOT in the DOM until clicked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GettingStartedChecklist fired four useQuery calls on every dashboard
load even for users long past onboarding (Marc has
onboardingCompletedAt = 2026-02-20 and 1k+ measurements). The component
then short-circuits with `null` once shouldShowChecklist() reports
false, so the network round-trips are wasted.
The analytics + medications queries already share their cache with the
dashboard's own queryKeys, so they remain enabled. The other two —
withings/status (~303 ms) and notifications/preferences (~654 ms) —
are unique to the checklist and now gate on
`user.onboardingCompletedAt == null`. That removes ~950 ms of network
from /` for every established user.
Edge case (returning user with onboardingCompletedAt set but
measurementCount < 5 because they deleted readings post-onboarding) is
preserved by the inline comment + by the fact that the checklist
already renders unknown defaults gracefully.
v1.5 phase-4 perf audit, win #2 (docs/audit/v15-performance.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- P7: shrink the bottom-nav clearance from 5rem to 4rem on mobile (md:pb-0 unchanged) so content sits closer to the nav without overlap.
- P8: TrendHint title gets text-muted-foreground so it stops competing with chart titles after the T2 muted-foreground hierarchy fix.
- P9: drop font-mono from the onboarding "X%" progress label; tabular-nums alone is enough.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GettingStartedChecklist intentionally shares TanStack Query cache
with the dashboard via `queryKey: ["analytics"]` etc., but its
queryFns returned the raw `res.json()` (the full `{data, error}`
envelope) while the dashboard, medications page, notifications page
and withings integrations section all return `json.data` (the
unwrapped inner shape). When the checklist's queryFn populated the
cache first, the dashboard's `data?.summaries?.WEIGHT` ended up
reading a property of the envelope `{data, error}` instead of the
analytics body, came back undefined, and silently failed every
per-tile data gate (`hasWeight`, `hasBp`, `hasPulse`, `hasBodyFat`).
Marc saw only the mood tile because mood lives under a different
cache key (`["mood-analytics"]`).
Same envelope/shape mismatch existed for `["medications"]`,
`["withings", "status"]` and `["notifications", "preferences"]` —
fixed in one pass since the failure mode is identical (silent
undefined reads on shared keys, latent bug for any consumer that
mounts after the checklist).
Each queryFn now `await`s and returns `json.data`; the local
accessors collapse from `res?.data?.X` to `res?.X`. As a
side-effect this also fixes a long-standing checklist bug where
`medsRes.data.medications.length` was always 0 (the medications
API returns the array directly under `data:`, not nested under a
`medications` field — so the count silently coerced to zero and
the "log a medication" checklist item never auto-completed).
Tests + typecheck stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>