test(insights): coverage for v1.4.19 polish changes

Source-grep guards for the four /insights polish concerns Marc
flagged on 2026-05-10 plus drift guards for the chart-token leak
fix:

  1. Dashboard does NOT mount <CompareToggle /> — the import +
     JSX use are both gone. Drift guard verifies
     `comparisonBaseline` / `compareBaseline` plumbing still
     reaches the charts (only the on-surface affordance moved).
  2. /insights renders exactly ONE `onRegenerate={advisor.regenerate}`
     prop — the page hero. The advisor card's duplicate handlers
     are gone.
  3. /insights does NOT mount <TrendCard> — the duplicate-of-
     dashboard tile strip. Verified via the import path being gone
     and any JSX-style mount being absent.
  4. /insights does NOT pass `t("insights.aiOverviewTitle")` as a
     subtitle — the orphan "Persoenlicher Berater" placeholder is
     gone.
  5. `stripChartTokens()` cleaves lowercase + mixed-case AI
     emissions; `parseChartTokens()` stays strict (uppercase
     allowlist only).

Source-grep tests rather than full React renders so they don't
pull in the dynamic-imported charts + advisor query stack the page
mounts. The cost is a slight false-positive risk if a future
refactor renames the JSX literal we're matching, but the comments
on each test point at the load-bearing assertion so a renamer can
update the guard without losing the contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 13:16:19 +02:00
1 parent e0c007a8af
commit 5360d3c0ce
2 files changed
+224

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
# Phase A6 — Settings mobile audit + consistency
**Owner:** A6 agent (Wave A, parallel with A1-A5, A7-A8)
**Date:** 2026-05-10
**Result:** done
Audit findings and the fix plan live in
[`.planning/phase-A6-settings-audit.md`](./phase-A6-settings-audit.md).
The audit is keyed to `before/` screenshots captured against
`https://healthlog.bombeck.io` at Pixel-5 (393×851) using Marc's
session cookie; raw geometry was extracted with
`scripts/audit-a6-v2.mjs` to spot the height mismatches the
human eye missed.
## What was wrong
- **Four input heights in the same shell.** The shared `<Input>`
primitive renders at 36 px (`h-9`) but Settings broke the contract
at three points: the AI active-provider native `<select>` was
`h-10` (40 px), the AI fallback-chain "Add provider" select was
`h-8` (32 px), and the Dashboard "Compare to" `<SelectTrigger>` was
`min-h-11` (44 px). Visible whenever two cards stacked on top of
each other.
- **Action-button overflow.** "Restart onboarding tour" lived in a
`flex justify-between` row next to its card title. On Pixel-5 the
button (216 px wide) overflowed the right edge of the parent card
by ~48 px because nothing forced a wrap. The "Change password"
button used the same pattern and was on the borderline.
- **Sprache buried.** The locale switch was paired with date-of-birth
inside the Profile card grid, implying it was a profile attribute
(it isn't — it's a cookie-backed UI preference).
- **Three competing `space-y-*` strides** inside Settings cards
(`space-y-3`, `space-y-4`, `space-y-5`) — outer rhythm was already
`space-y-6` consistently.
## What was fixed (5 commits)
1. `957f8e9` — `fix(settings): equalize input heights across all
settings sections`. AI active-provider select `h-10` → `h-9`, AI
add-provider select `h-8` → `h-9`, Dashboard "Compare to" trigger
loses `min-h-11` so the default `h-9` applies.
2. `9fda634` — `fix(settings): action-button placement consistent
across sections (right-side desktop, stacked mobile)`. Account →
Password, Account → Restart onboarding tour and Dashboard → Reset
to defaults all use `flex-col` on `<sm` and `flex-row` on `>=sm`,
so the button stacks below the title on mobile (full-width) and
right-aligns next to the title on desktop. Description paragraph
pulled into the title cluster on the two account cards so each
row is a clean `<title-block + button>` flex pair.
3. `1075784` — `fix(settings): Sprache select position + width
consistent`. Language lifted out of the dob/language pair into
its own row at the bottom of the Profile card. `max-w-xs` on
`>=sm` so it doesn't render heavier than other half-width fields.
Drive-by: pulled the duplicated native-select class string into a
single `NATIVE_SELECT_CLASS` constant so gender + language render
identically.
4. `737b533` — `style(settings): uniform vertical spacing across all
sections`. Dashboard layout card `space-y-5` → `space-y-4`. AI
provider config forms `space-y-3` → `space-y-4`. Section roots
keep `space-y-6` so distinct cards still read as distinct
surfaces. Note: `.planning/STATE.md` got bundled into this commit
accidentally via index race with A5; STATE content is correct,
no semantic harm.
5. `78f1f3f` — `test(settings): mobile e2e snapshot verifies
consistency`. Pixel-5 Playwright spec
(`e2e/settings-mobile-consistency.spec.ts`) locks in five
invariants: every form input on /settings/account is 36 px,
neither password nor tour buttons overflow the card right edge,
Sprache and dob no longer share a grid ancestor, the Dashboard
"Compare to" trigger is 36 px, every native select on
/settings/ai is 36 px. Mobile-only project — desktop skips it.
## Verification
- `pnpm test` — all 1637 tests pass
- `pnpm typecheck` — zero source-tree errors (the four `.next/types`
errors are stale build artefacts from previous milestone's untracked
dotted-segment route directories, documented in CLAUDE.md / STATE.md
as Phase 0 leftovers)
- `pnpm lint` — zero errors, 12 pre-existing warnings unrelated to A6
A post-fix Playwright re-screenshot pass against production is gated
on the v1.4.19 deploy (Phase E). The new e2e spec is the durable
artifact and runs on every PR.
## What I deliberately did NOT touch
- `src/components/settings/integrations-section.tsx` (A5 owned and
shipped during this phase as commits `0dcc91a` + `47a8fc7`).
A prettier autofix bundled two trivial reformats of A5's files
into my draft "uniform spacing" commit; those reformats were
reverted before the final commit landed.
- `src/components/admin/*` (A7).
- `src/lib/insights/`, `src/lib/ai/`, `src/components/charts/`,
`src/components/insights/` (A1-A4).
- Any settings copy or i18n keys — the focus stayed on layout, sizing,
and spacing per the brief.
## Files touched
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/dashboard-layout-section.tsx`
- `e2e/settings-mobile-consistency.spec.ts` (new)
- `.planning/phase-A6-settings-audit.md` (new — findings table)
- `.planning/phase-A6-report.md` (this file)
+118
View File
@@ -0,0 +1,118 @@
# Settings mobile audit (Pixel 5, 393×851)
Captured against production `https://healthlog.bombeck.io` on
2026-05-10 with Marc's session cookie. Screenshots in
`/tmp/healthlog-a6/before/<route>-tall.png`. Geometry tables produced
by `scripts/audit-a6-v2.mjs`.
## Routes covered
| Route | Purpose |
| ------------------------- | ----------------------------------- |
| `/settings/account` | profile + password + passkey + tour |
| `/settings/dashboard` | tile/chart layout + comparison |
| `/settings/ai` | provider config + fallback chain |
| `/settings/notifications` | telegram + ntfy + web-push + status |
| `/settings/export` | doctor report + CSV/JSON |
## Inconsistency table
### Input heights
| Section | Element | Actual | Expected |
| ------------- | ----------------------------------- | -----: | -------: |
| account | Username `<Input>` | 36 | 36 |
| account | Email `<Input>` | 36 | 36 |
| account | Gender native `<select>` | 36 | 36 |
| account | Height `<Input type=number>` | 36 | 36 |
| account | Date of birth `<Input type=date>` | 36 | 36 |
| account | Language native `<select>` | 36 | 36 |
| ai | "Active provider" native `<select>` | **40** | 36 |
| ai | "Add provider" native `<select>` | **32** | 36 |
| dashboard | "Compare to" `<SelectTrigger>` | **44** | 36 |
| export | All date `<Input>` fields | 36 | 36 |
| notifications | All telegram + ntfy `<Input>` | 36 | 36 |
The account section is internally consistent at 36 px. AI and Dashboard
break the 36-px contract: the AI active-provider select is 40 px, the
fallback "Add provider" select is 32 px, and Dashboard's "Compare to"
trigger uses `min-h-11` (44 px). All three are form fields the user
fills with their finger, so they should match the rest of the app.
### Action-button placement
| Section | Button | Placement on mobile | Overflow? |
| ------------- | ------------------------------- | ------------------------ | --------------------------------- |
| account | Save (profile) | bottom of form, right | no |
| account | Add passkey | bottom of card, right | no |
| account | **Change password** | inline next to title | borderline (336 / 361) |
| account | **Restart onboarding tour** | inline next to title | **YES — overflows card by 48 px** |
| ai | Disconnect | full block, sm | no |
| dashboard | Reset to defaults | inline next to title, sm | no |
| export | Configure & generate / Download | bottom of card, sm | no |
| notifications | Send test (status card) | inline | no |
Critical bug: on a 393-px viewport the "Restart onboarding tour" button
overflows the right edge of its parent card by ~48 px. The same flex
pattern (`flex items-center justify-between gap-2`) is used by the
password-reset card; with longer button copy it crosses the card edge.
### Spacing inconsistencies
| Section | Section-level spacing | Card-internal spacing |
| ------------- | --------------------- | --------------------------------------------- |
| account | `space-y-6` | `space-y-4` (form), `space-y-2` (field group) |
| dashboard | `space-y-6` | `space-y-5` (card), `space-y-2` (field group) |
| ai | `space-y-6` | `space-y-6` (card), `space-y-3` (sub-card) |
| notifications | `space-y-6` | per-card varies |
| export | `space-y-6` | `gap-3` grid + `mt-3 space-y-3` (card) |
The `space-y-6` outer rhythm is consistent; the card-internal rhythm
varies between `space-y-3` / `space-y-4` / `space-y-5`. Visible because
the AI card crams its sub-cards with `p-4` while the Account card uses
`p-6` — equivalent fields end up at different distances apart between
sections.
### Sprache-Menü specifics
The Sprache (language) `<select>` lives in the same row as the date-of-
birth field on the Account → Profile form. Both render at 36 px on
mobile, so technically consistent — but the Sprache field is paired
with a row that already carries a hint paragraph below the date field,
which makes the column heights asymmetric. On `<sm` the grid collapses
to a single column so the visual order becomes:
date-of-birth → date-hint → language → language-hint
That ordering is fine, but the Language field is the only locale switch
in the entire settings shell. It is buried halfway down the Account →
Profile card next to "date of birth", a profile attribute. Visually it
feels like part of the profile (it isn't — it's a per-session UI
preference stored in a cookie, not the `users.locale` column).
### Dashboard "Compare to"
Renders at 44 px (`min-h-11`) which mismatches the 36-px input contract
elsewhere. The 44-px tap-target floor matters where the only mobile-
nav action is a tap, not where the input lives inside a longer form
the user is already engaged with. Bring it down to 36 px on `>=sm`.
## Decisions
1. **Standardise input height to 36 px** (`h-9`). Replace the 40-px
ai-active-provider select, the 32-px add-provider select, and the
44-px comparison trigger with the shared 36-px contract.
2. **Mobile action-button rule**: when a card's title and an action
button live on the same row, the button stacks below the title on
`<sm` (full-width) and right-aligns next to the title on `>=sm`.
Prevents the "Restart onboarding tour" overflow.
3. **Card padding rule**: every settings card uses `p-6` (or `p-5` for
sub-cards inside a parent card) — pick one. Settled on `p-6` for
top-level cards. The AI sub-card uses `p-4` which matches its
nested role; left as-is.
4. **Sprache placement**: lift out of the dob/language pair into its
own row at the bottom of the Profile card. Keeps copy as-is, but
stops implying language is a profile attribute.
5. **Spacing**: top-level section uses `space-y-6`. Card-internal
spacing standardises on `space-y-4` between blocks; `space-y-2`
for label + input pairs.