71 Commits
Author SHA1 Message Date
Marc-André Bombeck bf77f5243d Fix cycle ring width, tab strip and calendar tap targets on narrow phones
- Cycle ring: the fixed 120 px shrink-0 ring overflowed grid cells on
  360 px Android widths. It now caps at its target size but is free to
  shrink to fill its cell, so it never pushes the page wider than the
  viewport.
- Cycle tab strip: the four long German labels overflowed the equal-width
  flex-1 cells and forced a horizontal scroll at 375 px. Below sm the
  triggers size to their content (text-xs, no flex-1) and scroll cleanly;
  from sm up they reclaim the equal-width layout.
- Calendar day cells: bump the 40 px min-height to 44 px to clear the
  mobile tap-target floor.
- Add a Pixel-5 spec asserting no horizontal scroll on /insights and
  /cycle plus the calendar tap-target floor.

The cycle view also now feeds the profile's typical lengths into the
wheel so a low-data tracker gets the canonical four-phase ring.
2026-06-07 14:15:47 +02:00
Marc-André Bombeck 4d4dcac505 docs(import): document the data-import paths, point Apple Health import at the new settings area
Add docs/integrations/data-import.md covering the Apple Health
export.zip path, the generic JSON import schema with the full
MeasurementType enum and canonical units, a worked example, and a
CSV-to-JSON conversion how-to. Fix the Apple Health doc to point at
Settings -> Export & Import instead of a non-existent import page. Extend
the export e2e spec with the import surfaces and the example-download
happy path.
2026-06-07 00:07:58 +02:00
Marc-André Bombeck ab1eea7f23 test(e2e): align export and integrations specs with removed surfaces
Drop the obsolete doctor-report dialog spec (the interactive export-page
dialog was removed; the report now lives under the health-record export),
remove the doctor-report testid from the export smoke, and update the
integrations layout spec to three cards after the moodLog removal.
2026-06-06 23:40:10 +02:00
Marc-André BombeckandClaude Opus 4.8 b3a626d83c docs: add navigational READMEs to key code directories
Add a README to src, src/lib, src/app, src/components, prisma, scripts,
and e2e so a technical visitor browsing the tree gets oriented: what
lives in each directory, the most important files, the local
conventions, and pointers to related dirs and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:26:34 +02:00
Marc-André Bombeck 2d6603cd54 test(e2e): align integration fixture with the consolidated status envelope
The settings cards now read connection state from the consolidated
/api/integrations/status envelope (the per-provider status routes were dropped
from the section), so the mobile-layout fixture must supply the per-integration
connected/configured fields the pills key on.
2026-06-05 04:08:01 +02:00
Marc-André Bombeck 3186ecbe67 fix(settings): raise the Google Health tag contrast and update the integration-count assertion
The Fitbit & Pixel tag chip paired muted text on a muted background (below
WCAG AA); use foreground text. Update the mobile integrations e2e to expect
the fourth integration card (Google Health).
2026-06-05 01:33:42 +02:00
Marc-André Bombeck 280bf42382 feat(nav): add the center capture action and More hub, move unit preference into the profile
Reshape the mobile bottom bar to the five-slot shape the native client
uses: Home · Meds · Log · Insights · More. The center "Log" slot is a
capture action rather than a destination — it opens a picker that routes
to the existing measurement, medication-intake and mood quick-entry
sheets (those forms already share a mount contract, so the picker reuses
them verbatim). Expand the former overflow into a real "More" hub that
holds the rest of the top-level destinations: Measurements, Mood,
Workouts, Achievements, Notifications and Settings.

The change is additive — Measurements and Mood leave the always-visible
strip but stay reachable through the More hub and the capture picker, so
nothing loses its route. On desktop the sidebar still lists every
destination.

Move the metric/imperial unit system from its standalone card into a
dropdown in the Profile form beside the timezone picker — it is a
display preference like language and timezone. Persistence is unchanged:
the dropdown PATCHes /api/auth/me/unit-preference on change and
invalidates the same query keys, so chart display transforms re-render
without a reload. Retire the now-unused card.
2026-06-05 00:08:07 +02:00
Marc-André Bombeck 9e6865841d feat(coach): stream replies incrementally, collapsible history and provenance, single disclaimer
Token-by-token streaming was buffering on the server. The chat route
enqueued every tokenised SSE frame synchronously inside the stream's
start() callback, so the runtime coalesced them into a single network
read and the client painted the full answer at once despite the
per-token render path already wired in use-coach. Make the emit callback
async and yield to the event loop between token frames so each one
flushes as its own chunk; the visible cadence now reads ChatGPT/Claude-
style. Refusal and error paths stay single-frame.

The conversation history was a permanent left column on lg+, eating chat
width on laptops and reading as a takeover behind the narrowed drawer
cap. It now lives behind an always-visible "Conversations" toggle on
every viewport and opens as the left-edge tray on demand; the thread
keeps the full width by default and never loses it on mobile.

The provenance block (source chips plus raw key-values) rendered fully
expanded under every answer and was often taller than the reply. Fold
the chips into the existing collapsed disclosure alongside the key-
values so the whole grounding is one click-to-expand surface, collapsed
by default, with the disclosure's aria-expanded/aria-controls intact.

The clinical-decisions disclaimer rendered both at the bottom of the
thread and in the sources-rail footer. Consolidate to a single line
directly above the composer, always visible on every viewport.

The auto-growing composer (single line growing to a capped multi-line
with internal scroll, Enter-to-send, Shift+Enter-newline) and the
aria-live streaming region are preserved. Plain-text React children
throughout; no new dependencies.
2026-06-04 22:17:55 +02:00
Marc-André BombeckandClaude Opus 4.8 d4801d4646 refactor(export): highlight the health-record export, demote the doctor report, collapse the included-data list
The Settings export page over-emphasised the doctor-report PDF: it owned
the page hero while the broader health-record export (PDF + FHIR R4 + zip
package) sat below it as a plain card. The relative prominence is now the
other way round.

- The health-record export panel takes the page hero, with the same
  hero-gradient + glow-purple treatment the Insights hero strip uses.
- The doctor-report card drops the hero styling and moves to the bottom
  of the page as a small secondary card. It stays fully functional — the
  same dialog + /api/doctor-report flow runs from there.
- The health-record "included data" checklist is now a disclosure,
  collapsed by default, so the panel opens compact instead of as a long
  always-expanded list. It uses the inline aria-expanded/aria-controls
  pattern already used elsewhere, with no new dependency.

Pure presentation/IA change: export routes, payloads, and the exported
data are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:43:02 +02:00
Marc-André Bombeck 98693cda6a test(e2e): expect three integration pills on mobile after WHOOP card
The WHOOP integration card adds a third status pill on
/settings/integrations. Update the Pixel-5 layout spec to expect three
pills and assert the two connected ones (Withings + moodLog) order-
independently, since WHOOP stays a dormant BYO-keys card until an
operator adds credentials.
2026-06-04 05:00:13 +02:00
Marc-André Bombeck e7f406ecc7 test(e2e): pin the clock so the medication next-due chip is deterministic
The recurring medication stub schedules an 08:00-09:00 window and the card
derives its header from the real clock. When CI ran inside that window the card
showed the in-window 'take now' pill and correctly suppressed the 'Nächste
Einnahme:' next-due line, so the chip assertion failed depending on wall-clock
time. Pin the clock to a fixed afternoon before rendering the list, putting the
morning window in the past so the next-due line always renders.
2026-06-02 21:02:42 +02:00
Marc-André Bombeck 0c8506d7d4 test(e2e): give the next-due chip assertion the 10s budget the name has
The medication-list next-due line is computed client-side from the schedule
after hydration, so on a loaded CI runner it can appear a beat after the
medication name. The chip assertion used the default 5s timeout while the name
assertion above it uses 10s; align them so the check isn't racing hydration.
2026-06-02 20:52:28 +02:00
Marc-André Bombeck 9b3cb5ec7e test(e2e): assert the wizard step via the dialog root data attributes 2026-06-01 23:56:19 +02:00
Marc-André Bombeck 6e61713fc6 fix(insights): move coach-ask to the header and de-emphasise the assessment border
The Coach launch sat at the foot of every category page; move it into the
sub-page header as an icon-only button at heading height, top-right, so
"ask the coach" reads as a header action and the page no longer ends on a
stray button. The shared shell mounts it (gated on the Coach flag plus the
per-user opt-out), so every category page picks it up and the foot
instance is dropped. Also drop the purple left accent on the assessment
("Einschätzung") card — the coloured rule made it read as restless against
the calmer surrounding cards.
2026-06-01 22:50:59 +02:00
Marc-André Bombeck 6ce09cc76d feat(insights): stat strip, range band, all-readings subpage, diversity nudge
Add the data-value features to the insights category pages on top of the
density pass:

- Stat strip: lead each metric page with min / max / median / mean,
  read from the already-fetched analytics summaries slice (no new
  network). Median is now computed in both summary producers — exact in
  the live path, a windowed PERCENTILE_CONT in the rollup-tier slim
  slice. The thin HealthKit pages mount it too for parity; no AI
  assessment is added there.
- Range band: shade an Apple-Health-style min-max area around the daily
  mean line where the data supports it (non-cumulative metrics). The
  daily-aggregate read now emits per-day min / max from the rollup
  buckets; the band rides the existing target-range toggle.
- All readings: a dedicated per-metric subpage at /insights/values/<type>
  reusing the measurement list pinned to one type (pagination + inline
  edit/delete). A clear entry button mounts at the foot of each
  category page.
- Diversity nudge: a gentle hint when a metric's readings cluster on one
  weekday or time of day, computed from a bounded, cached timestamp
  window — non-blocking, self-gating.

All new reads stay cache-friendly and off the navigation path.
2026-06-01 20:26:26 +02:00
Marc-André Bombeck a3e1d369e6 test(e2e): mock the dashboard snapshot endpoint for tile specs
The v1.7.2 default-ON snapshot flag makes the dashboard read every tile
from GET /api/dashboard/snapshot; the e2e build sets no opt-out var, so
the specs that only mocked the legacy cells hit the real route, got
empty summaries for the zero-measurement seed user, and timed out the
tile/chart assertions.

Add a shared, correctly-typed mock helper and wire it into the five
tile-asserting specs alongside the existing legacy mocks (kept to
protect the reversible path).
2026-05-31 20:00:16 +02:00
Marc-André BombeckandClaude Opus 4.7 33b912babd test(settings): exempt the hidden avatar file input from the 44px touch-target sweep
The avatar upload uses the accessible visually-hidden <input type="file">
pattern: the input is sr-only / zero-size and the visible 44px touch
target is the styled label/button that triggers it. The mobile
touch-target sweep now skips hidden/zero-size file inputs and keeps the
44px assertion for every genuinely visible input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:44:40 +02:00
Marc-André BombeckandClaude Opus 4.7 ac1d03f8ea test(meds): green the wizard e2e locale and one-shot timezone fixtures
The medication-wizard e2e specs assert the German step counter, but the
seeded e2e user falls back to the English default locale; pin the
`healthlog-locale=de` cookie for the wizard flow and correct the
storage-state cookie allowlist to the hyphenated cookie name the i18n
layer actually reads.

The one-shot cadence integration fixture derived `startsOn`/`endsOn` from
the UTC calendar date but checked emission against the Europe/Berlin
today-window, so the assertion flipped when the suite ran in the UTC
evening. Derive the fixture dates from the same Berlin calendar day the
window uses.

Also refresh the OpenAPI spec version to 1.6.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:30:27 +02:00
Marc-André BombeckandClaude Opus 4.7 dba844235c test(meds): coverage for compose-mode
Five new payload-helper cases pin the multi-schedule encoder, the
hydration that opens on the list view, the add and remove guards,
and the schedule-id preservation on edit. A new Playwright spec
walks a two-schedule create flow end-to-end and verifies both
summary cards land before save.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:52:39 +02:00
Marc-André BombeckandClaude Opus 4.7 320970c97d test(meds): coverage for the modal wizard
Component tests pin the path table, the step encoding, the
treatment-class to category mapping, and the create-vs-edit prop
branching. POST /api/medications gains pin-point coverage for the
new DIABETES and ANTIBIOTIC clinical-category values. Six Playwright
specs walk the new dialog end-to-end via the German label surface,
one per cadence shape; helpers consolidate the dialog-open + step
indicator assertions so each cadence spec only carries its own
sub-control selectors. The OpenAPI yaml refreshes to surface the
two new enum values to the iOS client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:33:11 +02:00
Marc-André BombeckandClaude Opus 4.7 b4b099003f test(meds): playwright e2e for every wizard cadence shape
Six specs walk the seven-step creation wizard end-to-end via the
German label surface, one per cadence kind, with shared step helpers
behind a single API stub. Verifies the medication lands in the list
view with the right next-due chip; one-shot suppresses the chip per
the design. Also closes the missing `wizard.page.backToList` key the
host route reads on /medications/new.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:42:10 +02:00
Marc-André Bombeck dbb5979a97 chore(release): v1.4.39.3 — dashboard slim/thick merge robustness and list-page precision
The post-deploy CI for v1.4.39.2 surfaced eight e2e failures across
chromium-desktop and chromium-mobile (chart-overlay-controls,
dashboard, charts-mobile, measurement-flow) that all routed through
two narrow regressions and one durable test-fixture gap.

### Fixed

- Dashboard slim/thick merge no longer blanks the tile strip when the
  slim slice resolves with an empty summaries record. The v1.4.39.2
  inline `slim?.summaries ?? thick?.summaries` short-circuited on
  JavaScript's truthy-empty-object even when thick carried the full
  payload. The merge moved to a pure `mergeSlimAndThickAnalytics`
  helper that uses object emptiness as the discriminator: populated
  slim still wins on overlapping fields (progressive-paint contract
  preserved), empty slim falls through to thick, both empty leaves
  the dashboard's data-floor gates to render the appropriate empty
  state. Eight new behaviour-level unit tests cover the edge cases.
- `MeasurementList` non-grouped rows now preserve decimal precision
  on both the desktop table and the mobile card grid. The v1.4.37 W7c
  collapsed-list view passed every value through `fmt.integer`, which
  kept the per-day step / activity aggregates correct but silently
  truncated single weight / body-fat / temperature readings — 78.4 kg
  rendered as "78 kg". Grouped rows (cumulative HK type day
  aggregates) stay on `fmt.integer` because their readings are
  integer-only; non-grouped rows now use `fmt.number` which honours
  up to three fraction digits without forcing a minimum.

### Changed

- Playwright route mocks for `/api/analytics` migrate from the
  literal `**/api/analytics` string glob to the regex form
  `/\/api\/analytics(\?|$)/` across every authenticated spec. The
  string glob does not match the sliced URL form
  `/api/analytics?slice=summaries` the v1.4.39.2 dashboard split
  fires alongside the thick request, so the slim request fell
  through to the real route on the seeded test user that returns
  empty summaries. Ten specs migrate: dashboard,
  chart-overlay-controls, measurement-flow, charts-mobile, a11y,
  v1427-responsive-sheet, v1427-coach-launch,
  v1427-measurements-add-param, v1427-insights-empty-state,
  onboarding-tour-passthrough. The regex form is the durable
  alignment with the v1.4.37 W-CI lesson that already swept
  `onboarding-flicker.spec.ts` and `mobile-viewport.spec.ts`.

### Notes

- No schema migration. Runtime + test-fixture-only changes.
- `pnpm test --run` green at 4662 / 4663 (1 long-standing skip).
- Full Playwright suite green at 116 passing / 36 skipped / 152 total
  on both `chromium-desktop` and `chromium-mobile` profiles, against
  the production Next.js build. Closes every failure from CI run
  26213226723.
2026-05-21 10:31:18 +02:00
Marc-André Bombeck fa8a62513a test(e2e): include unit + source on the mocked measurement row
`measurement-flow.spec.ts` mocks `/api/measurements` and echoes the
just-posted body back as the list response. The `Measurement` shape
the list page consumes requires `unit` and `source`; the mock
omitted both, the list render crashed before painting the row, and
the test's `expect.poll(...)` for "78.4" timed out.
2026-05-17 23:55:55 +02:00
Marc-André Bombeck 6ea1c77532 test(e2e): point doctor-report specs at the v1.4.37 hero-card testids
v1.4.37 lifted the doctor-report card from the export-tile grid to a
hero block on the export page; the test-ids renamed from
`export-card-doctor-report` / `export-action-doctor-report` to
`export-hero-doctor-report` / `export-hero-doctor-report-action`.
The Playwright suite still targeted the old ids and timed out on
every run since. Updates the two affected specs.
2026-05-17 23:55:52 +02:00
Marc-André Bombeck 88028fa082 v1.4.37 — final web polish before iOS focus
Carries the v1.4.36 perf carry-over for the full /api/analytics
route (correlations + healthScore + bp_in_target on the rollup-
coverage probe, 111s cold worst-case down to ~1.5-3s on power-user
accounts), consolidates Apple Health step samples into a one-row-
per-day list view with chevron drill-down and a nightly drain
(~50x table compression once it converges), restores
IntakeHistoryListV2 to the pre-v1.4.28 contract, converges every
Coach affordance behind a single feature gate with an invariant
test pin, lifts the medication-detail Mounjaro card to byte-
equivalent parity with Ramipril, brings the Insights overview
hero row to height parity, hardens getClientIp behind a new
TRUST_CF_CONNECTING_IP env flag, ships the Arztbericht hero card
on Settings -> Export, adds the Medikamenteneinnahme quick-add
overlay to the dashboard, drops the timezone override toggle for
a silent browser-zone auto-seed, and fixes the v1.4.36 red CI
workflows. Documentation refreshed across all three repos with
five new architecture diagrams in the Dracula palette. Unit suite
4354 -> 4486 passing; typecheck + lint clean.
2026-05-17 15:36:38 +02:00
Marc-André Bombeck affede31a4 v1.4.34 — apple-health import + reliability + web-freeze
The final functional web release before the iOS native client lands.
Headline work: a streaming Apple Health `export.zip` importer for
prospective iOS-migrating accounts, a server-side aggregation cache
that collapses the three hottest dashboard reads onto in-process LRU
slots with single-flight coalescing and per-user invalidation, a
broader compliance classifier with a dedicated `early` bucket so
ahead-of-window doses count as compliant, the Coach launch surface
hoisted onto every authed page with a dashboard-hero CTA, and a
trimmer Settings shelf that folds Sources into Targets. The release
also lights up the `cache.<name>.outcome` annotation on the active
wide event so production logs carry the cache hit-ratio signal
without leaking userIds.

### Added

- **Server-side aggregation cache.** A new `ServerCache<T>` primitive
  (`src/lib/cache/server-cache.ts`) extends the v1.4.33 Coach snapshot
  LRU shape into a reusable per-route layer with TTL expiry,
  capacity-bounded LRU eviction, single-flight read coalescing, and
  per-instance hit / miss / eviction / stampede counters. Wires the
  three hottest dashboard reads — `/api/analytics` (slim + default),
  `/api/gamification/achievements`, `/api/medications/intake?scope=compliance`
  — and bolts per-user invalidation onto every measurement / mood /
  medication / workout / dashboard-widget write endpoint so the next
  read paints fresh data instead of waiting out the TTL. Every cache
  hit / miss surfaces on the active wide event as
  `cache.<name>.outcome` + `cache.<name>.key_hash` so production logs
  carry the hit-ratio signal without leaking userIds.
- **Apple Health `export.zip` import.** New endpoint
  `POST /api/import/apple-health-export` (synchronous multipart
  upload, asynchronous ingest via a dedicated pg-boss
  `apple-health-import` worker, per-`MeasurementType` ingestion
  stats). Streams the upload straight to disk so a multi-gigabyte
  export never lands in V8 heap, hashes the bytes inline for
  content-based idempotency, and unpacks `apple_health_export/export.xml`
  with a hand-rolled ZIP central-directory walker that handles Zip64
  for archives past the 4 GB barrier. The parser folds every
  `<Record>`, `<Workout>`, `<Correlation>`, and `<ClinicalRecord>`
  into the existing `Measurement` and `Workout` row shapes — spot
  rows keyed by `HKMetadataKeyExternalUUID` (or a deterministic
  `sample:<sha256>` fallback), cumulative HK types collapsed into
  one `stats:<HKType>:<YYYY-MM-DD>` row per user-local day to match
  the iOS daily-aggregation convention. Live per-stage progress
  surfaces through `GET /api/import/apple-health-export/{jobId}/status`.
  An admin variant at `POST /api/admin/import-apple-health-export`
  imports on behalf of a target user (cookie-only `requireAdmin()`
  gate; Bearer tokens never elevate).
- **`ImportJob` schema model + migration.** New Prisma model captures
  the per-upload state machine (`queued | unpacking | parsing |
  upserting | done | failed`), the content-hash for idempotency
  short-circuits, and the per-`MeasurementType` ingestion counters
  the status route surfaces back to the client.
- **`lastSeenByType` on `/api/analytics`.** Both the slim
  (`?slice=summaries`) and default slices return a per-type
  `{ lastSeenAt, daysAgo }` map. The dashboard trend tiles wire 12
  mounts to a new `tileStaleDays()` helper so an "X days / weeks /
  months ago" hint paints under any per-metric tile whose last
  sample crosses the 7-day floor. Six locales gained additive
  week / month plural keys; Polish uses the genitive-plural form
  that covers every non-1 count the bucket math can produce.
- **Dashboard "Ask the coach" hero CTA.** A new pill next to the
  existing "Hinzufügen" / "Add" launches the Coach drawer directly
  from the dashboard hero. The `<CoachLaunchProvider>` hoisted from
  the insights layout to the auth shell so the drawer is reachable
  from every authed route; the floating action button stays scoped
  to `/insights/**` so it cannot distract on the dashboard.
- **Shared achievements query hook.** `useAchievementsQuery()`
  centralises the three previous consumers — recent-achievements
  card, the `/achievements` mother page, and the unlock notifier —
  onto one TanStack queryKey + cache slot so dashboard cold mount
  fires the endpoint once instead of twice.
- **Typed authed Cache-Control presets.** New
  `src/lib/http/cache-headers.ts` module exports
  `NO_STORE_BUT_BFCACHE = "private, max-age=0, must-revalidate"`,
  `SHORT_LIVED_PUBLIC`, and an `applyAuthedHeaders()` helper. The
  presets land at the framework level via a `next.config.ts`
  `headers()` rule that stamps the bfcache-friendly directive on
  every authed HTML response, restoring Chromium bfcache eligibility
  for in-app back / forward navigation.
- **`scripts/print-bundle-report.mjs`.** Restores the at-a-glance
  "top client chunks by parsed size" signal Turbopack dropped from
  `next build`; reads `.next/analyze/client.json` (written by
  `@next/bundle-analyzer` when `pnpm analyze` runs with `ANALYZE=1`)
  and prints a sorted table plus totals.

### Changed

- **Settings sidebar: "Sources" folded into "Targets & Sources".**
  Per-metric threshold ranges and per-metric source priority now
  share one `/settings/thresholds` page so the same metric's
  threshold and device-source preference sit together instead of
  one sidebar entry away. `/settings/sources` keeps a
  `permanentRedirect` so external bookmarks and docs links follow
  through unchanged. Section count: 11 → 10.
- **Insights tab strip: vital pills collapse under a "Vitals"
  parent.** Five vital pills (HRV, Resting HR, Oxygen, Body
  Temperature, Active Energy) hide behind one parent pill that
  opens a popover sub-list. Parent-pill active state mirrors the
  URL so spatial orientation survives the collapse; each sub-page
  keeps its own URL and bookmark resolves unchanged. Strip
  footprint: 14 → 10 entries when every metric has data.
- **Coolify env-var audit.** `mcp__coolify-apps01__env_vars`
  inspection captured the section-1 / section-2 duplicates that
  have accumulated under apps-01 since v1.3.1. Audit pinned at
  `.planning/round-v1434-iwa-coolify-env-audit.md`; no env-var
  deletes performed (operator action).

### Fixed

- **Compliance classifier: early intakes count as compliant.** The
  classifier picked up a dedicated `early` bucket (window-start
  minus three hours through window-start), the `on_time` post-window
  tolerance widened from one hour to three hours, and the `late`
  band sits between the new `on_time` ceiling and the configurable
  `lateMinutes` knob. Heatmap consumers route the new bucket
  through the compliant path; the dedicated `early` counter rides
  on `DailyComplianceEntry` for downstream consumers that want to
  distinguish ahead-of-window from on-window intakes.
- **Compliance heatmap fallback retired.** The v1.4.33 defensive
  `looksClassifierBug` fallthrough is gone now that the underlying
  classifier widening removes the every-dose-very-late mode the
  fallback covered.

### Performance

- **`/api/gamification/achievements` consumer collapse.** Two of
  three previous consumers shared the same TanStack queryKey
  literal; the third carried a per-user discriminator so the cache
  treated it as a fresh slot. Dashboard cold mount fired the
  endpoint twice. Collapsing onto the shared hook trims that to a
  single request; with the new server-side cache slot warm, the
  duplicate-mount worst case rides single-flight coalescing.
- **GHCR build fires once per release tag.** The
  `docker-publish.yml` workflow lost its `push.branches: [main]`
  trigger; the `:latest` raw-tag enable rule moved onto the tag
  ref so each release produces exactly one multi-arch build that
  refreshes both the semver tags and the `:latest` alias.
- **NFT-trace warnings silenced.** `next.config.ts` gained an
  `outputFileTracingExcludes` block that narrows the Turbopack
  tracer away from the `MAXMIND_LICENSE_KEY` env-access path so
  the trace-report no longer warns on every dependent route file.

### Refactor

- **Dashboard analytics consumers.** The two cards that previously
  decided their own freshness copy now lean on the typed
  `tileStaleDays()` helper and the additive `lastSeenByType` field
  on the analytics response. Mood and BD-Zielbereich tiles keep the
  default `null` because they have no underlying per-type freshness
  signal.

### Accessibility

- **Dashboard hero CTA hits the WCAG 2.5.5 touch-target floor.**
  The new "Ask the coach" pill carries `min-h-11` on mobile and
  the matching `sm:min-h-9` desktop floor so the touch target
  stays above 44 px on the Pixel 5 boundary.
- **Insights tab-strip Vitals parent reads correctly to assistive
  tech.** Parent pill carries `aria-current="page"` whenever the
  current URL matches one of its child vital sub-pages, plus a
  `data-slot="insights-tab-strip-group"` hook for visual-regression
  testing.

### Internal

- **Two e2e flake windows tightened.** `onboarding-flicker` swapped
  its 12-sample 50 ms poll loop for a single Playwright
  auto-retrying `toBeHidden({ timeout: 700 })` assertion so the
  1-2 ms race window between the analytics-pending shell and
  `useAuth().user` resolving collapses to a single retry slot.
  `mobile-viewport` dropped `nav a[href]` from the touch-target
  sweep (the bottom-nav owns its own WCAG spec) and gated the
  44-px floor on `matchMedia('(min-width: 640px)').matches === false`
  so the Pixel 5 viewport never tripped into the `sm:` tier during
  WebKit render commits.
- **Server-cache observability.** Every `cached()` invocation passes
  an `annotate` callback that lands two keys on the wide event:
  `cache.<name>.outcome` ∈ `{ hit | miss | stampede }` and
  `cache.<name>.key_hash` (non-reversible djb2 32-bit hash). Ops
  can grep `cache.analytics.outcome` over any time window to
  compute the hit ratio per deployment.
- **AASA followups verified.** The v1.4.33 `/.well-known/apple-app-site-association`
  handler serves a direct 200 on every fronting origin; Apple's
  CDN has ingested matching bodies for all three domains.

### Web freeze

v1.4.34 marks the last functional release of the web codebase before
the iOS native client launches. Subsequent v1.4.x tags carry security
fixes, dependency bumps, and hotfix-only corrections. New feature work
is paused until the iOS app clears Apple review, at which point a
v1.5.0 version-bump-only release tags the milestone. The Prisma
schema head comment and `.planning/v15-strategic-plan.md` §5
decision-log row pin the freeze trigger in-tree.
2026-05-16 21:25:26 +02:00
Marc-André Bombeck 7bef199039 v1.4.33 — polish and reliability
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path.

Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container.

Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app.

The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces.
2026-05-16 19:46:43 +02:00
Marc-André Bombeck 2d111fcb24 chore(release): v1.4.28
Bug-fix and consistency follow-through after the v1.4.27 mobile sweep.
The headline is a tighter dashboard and insights surface: six widgets
retire from code entirely (GLP-1 tile, dashboard `<DrugLevelChart>`
mount, GLP-1 detail-page intake history + inventory, the
`<InsightAdvisorCard>` block and its regeneration affordance, the
weekly-report route), four broken edges close (workout-edit duplicate-
timestamp 409, BD-Zielbereich tile rebuilt on the shared
`<TrendCard>` primitive, `/insights/puls` chart timeout fallback,
sticky tab-strip scroll lock), and one consistency contract lands per
maintainer directive: every medication-list row, every medication-
detail section header, every trend tile, every Coach launch glyph,
every chart-height contract reads on one shape. The HealthScore card
grows to fill its column, gains an accessible `?` tooltip explaining
the delta, and drops the placeholder "Wochenbericht erstellen"
button. iOS contracts are intact; the only `/api/*` evolution is
additive (the `aggregate=monthly` grain on `/api/measurements` and an
internal-only `/api/internal/web-vitals` beacon route).
2026-05-16 03:17:00 +02:00
Marc-André Bombeck 4f09c3f862 chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
2026-05-15 22:59:10 +02:00
Marc-André Bombeck 17b8d8d2c2 chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
2026-05-14 23:49:32 +02:00
Marc-André BombeckandClaude Opus 4.7 15796c148b fix(auth): move onboarding redirect from AuthShell useEffect to proxy
The post-hydration redirect for users with `onboardingCompletedAt ===
null` caused a brief dashboard flash before the client-side effect
could fire. The redirect now lives in `src/proxy.ts` so it lands on the
first server response.

The proxy runs in the Edge runtime and can't reach Prisma, so the auth
routes (login / passkey-verify / register / password / /api/auth/me)
mirror the DB `onboardingCompletedAt` column into a non-httpOnly
`hl_onboarding` cookie. The cookie is a UX hint, not a security
signal — a user editing it locally only skips the dashboard flash;
they still can't bypass any server check. `/api/onboarding/complete`
clears the cookie so the next navigation drops the redirect
immediately, and `destroySession` + expired-session purge wipe it
alongside the session cookie.

The e2e onboarding-flicker spec can return to testing the actual
incomplete-onboarding case: the proxy reads the cookie (absent for
the fully-onboarded storage-state user), not the mocked `/api/auth/me`
response, so the test now sets `onboardingCompletedAt: null` honestly
instead of pinning it to a non-null value as a workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:29:17 +02:00
Marc-André BombeckandClaude Opus 4.7 3785fec2ac fix(e2e): scroll first chart card into view before counting axis ticks
The Pixel-5 tick-density spec waited on `.recharts-xAxis` to be visible
but Playwright kept resolving the SVG axis as `hidden`. On a 393×851
viewport the dashboard ships several stacked tiles above the first
chart card, so the bottom-positioned axis sits below the initial fold
when the test starts probing.

Locate the first chart card by its range-tab data-slot, scroll it into
view so Recharts' ResponsiveContainer measures a non-zero parent, then
wait for an axis tick label (not the wrapper `<g>`) to be visible
before counting ticks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:44 +02:00
Marc-André BombeckandClaude Opus 4.7 e8d0660198 fix(e2e): exempt truncate-clamped elements from per-element overflow walk
The api-tokens mobile no-overflow spec walks every descendant of the
mobile card-list and asserts `scrollWidth <= clientWidth + 1`. The
v1.4.19 fix wrapped long token names / usernames / permissions in a
`<TruncatedCell>` (Tailwind `truncate` ⇒ `overflow:hidden +
text-overflow:ellipsis + white-space:nowrap`), which by design keeps
`clientWidth` clamped to the parent column while `scrollWidth` reflects
the full text — exactly the case the assertion was tripping on.

Skip elements whose computed `text-overflow: ellipsis` and clamped
`overflow-x` show they are intentionally truncating. The page-level
`document.documentElement.scrollWidth <= innerWidth + 1` assertion
remains the authoritative no-scrollbar guard, so the regression intent
is preserved without false-positives on the truncate helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:32 +02:00
Marc-André BombeckandClaude Opus 4.7 812b68a99d fix(e2e): scroll trigger into view + force-click on portalled toggle
The chart-overlay-controls spec timed out clicking the target-range
Switch — Playwright resolved the locator but reported "element is
outside of the viewport" after auto-scroll. The Radix dropdown content
is portalled to `document.body`, so `scrollIntoViewIfNeeded()` on the
toggle is a no-op (it lives outside the page scroll container).

Scroll the cog trigger into view BEFORE opening the popover so the
portalled content paints inside the viewport, then force the click on
the toggle to bypass the off-viewport guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:20 +02:00
Marc-André BombeckandClaude Opus 4.7 f181ab71fe fix(e2e): pin onboardingCompletedAt so AuthShell does not redirect
The "incomplete-onboarding" flicker spec mocked `/api/auth/me` with a
null `onboardingCompletedAt`, which makes `<AuthShell>` redirect every
authenticated route to `/onboarding` before the dashboard hydrates —
so the checklist card the test asserts on never paints.

Pin the timestamp to a non-null value while keeping every other
checklist signal empty (profile fields null, `summaries: {}` so
`measurementCount = 0`). The `shouldShowChecklist` gate still returns
true via its `measurementCount < 5` branch, so the assertion still
exercises the collapsed-by-default rendering path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:12 +02:00
Marc-André BombeckandClaude Opus 4.7 32cd18566d fix(e2e): adopt HeroStrip data-slot on insights advisor spec
v1.4.20 B1 retired `<InsightsPageHero>` in favour of the condensed
`<HeroStrip>` component, which exposes `data-slot="insights-hero-strip"`
instead of the legacy `insights-page-hero` marker. Update the advisor-
card spec selector so the hero check hits the live element on /insights.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:48:59 +02:00
Marc-André BombeckandClaude Opus 4.7 ae2f671321 style: prettier on touched test + e2e files
Run prettier on the test, e2e, and component files this v1.4.19
A8 wave touched. The .planning/* and docs/audit/* prettier
warnings are pre-existing baseline noise — left alone to keep
this commit narrow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:58:30 +02:00
Marc-André BombeckandClaude Opus 4.7 dd8212e202 fix(admin): api-tokens table truncate-with-tooltip (no scrollbar 4th attempt)
Marc has now reported a residual scrollbar on `/admin/api-tokens`
four times running. Earlier fixes addressed the obvious overflow
culprits (column-hide in v1.4.15, mobile card-list in v1.4.16,
admin-shell mobile-strip `no-scrollbar` in v1.4.18), but the
section itself still leaked a few quirks:

- The mobile token-name span carried `truncate` and `break-all` on
  the same element. `truncate` sets `white-space: nowrap`, which
  beats `word-break: break-all` per CSS spec — `break-all` was dead
  code AND it was misleading. Remove it.
- The desktop table had no per-column upper bound. A long token
  name could expand the row past the card on 768-1024 px viewports.
  Lock the layout with `table-fixed` + `<colgroup>` widths.
- Permission badges had no `max-width` cap. A single long
  permission string (`measurements:write:audit:legacy:extended`)
  could push the cell past the card. Cap each badge with
  `max-w-full truncate` and a native `title` attribute.

Wrap every potentially-long string (token name, username, permission
badge) in a small `<TruncatedCell>` helper that pairs the visual
truncation with a radix tooltip. The tooltip is the discoverable
hover/long-press path so the full value is still readable; the
native `title` attribute is a graceful fallback for screen readers
and right-click → "view full name" on desktop.

Adds an e2e regression that walks every element inside the mobile
card-list and asserts none of them carry `scrollWidth > clientWidth`,
exercised against a worst-case payload (long token name + long
username + long permission string).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:21:36 +02:00
Marc-André BombeckandClaude Opus 4.7 a7390852f8 test(charts): mobile-viewport e2e spec for header layout + tick density
Adds a Playwright spec that runs only on the chromium-mobile project
(Pixel 5, 393 × 851) and gates the two regressions the previous
chart commit cleared:

- Every chart card on the dashboard keeps its range tabs on a single
  horizontal row (asserts that all `[data-slot=chart-range-tab]`
  inside a card share one rounded `top` coordinate). Mock data feeds
  every chart 30 days of measurements + 30 days of compliance points
  so the controls row actually paints with all chips and tabs.

- Every recharts x-axis paints at most seven visible tick labels at
  Pixel 5. The helper caps at six; the seven-tick allowance covers
  Recharts' preserveStartEnd interaction with rounding.

The PROD probe script that drove the audit is also committed at
.planning/v1419-a2-prod-probe.mjs for future regression runs, with
the findings recorded in .planning/phase-A2-mobile-findings.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:12:11 +02:00
Marc-André BombeckandClaude Opus 4.7 33d7a381f6 test(settings): mobile e2e snapshot verifies consistency
Pixel-5 (393 px) Playwright spec that locks in the v1.4.19 A6 fixes:

- /settings/account: every form `<input>` and native `<select>`
  renders at 36 px (the canonical Settings input height).
- /settings/account: the password + restart-onboarding-tour buttons
  stay within their parent card's right edge — pre-fix the tour
  button overflowed by ~48 px on Pixel 5.
- /settings/account: the Sprache select no longer shares a grid
  ancestor with the date-of-birth input (lifted into its own row).
- /settings/dashboard: the "Compare to" `<SelectTrigger>` renders at
  36 px, not 44 px.
- /settings/ai: every native `<select>` (active provider, model
  pickers, fallback chain "Add provider") renders at 36 px.

Mobile-only — the desktop project skips it. Re-uses the seeded e2e
user via the storage-state cookie jar, no per-spec login.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:11:39 +02:00
Marc-André BombeckandClaude Opus 4.7 47a8fc7311 test(integrations): Pixel-5 e2e for consolidated status pill
Pins three contracts at the Pixel-5 viewport that the Vitest specs
can't see: no horizontal page scroll, exactly one pill per card with
the connected `data-state` marker, and the v1.4.18 redundant banner
is gone from the rendered DOM. Stubs all three integration endpoints
so no real Withings/Mood Log credentials are needed in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:05:42 +02:00
Marc-André BombeckandClaude Opus 4.7 3048dd6637 style(prettier): sweep formatting across the tree
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:05:40 +02:00
Marc-André BombeckandClaude Opus 4.7 d6e87c3a12 test(charts): coverage for revert + overlay-controls + persistence
Adds a Playwright spec that drives the dashboard end-to-end — opens
the chart overlay-controls popover, toggles target-range on, and
verifies the PUT /api/dashboard/chart-overlay-prefs round-trip fires.

Adds a vitest SSR test that pins the medication-compliance chart's
prefs-gated rendering: with the trend toggle off the 7-day trend chip
stays hidden; flipping it on paints the chip in the header. Mocks the
useChartOverlayPrefs hook so each test can dial the persisted state
directly without round-tripping the layout API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:36:08 +02:00
Marc-André BombeckandClaude Opus 4.7 6912d4e2b6 test(achievements): coverage for evaluators + UI states
Three new layers of coverage for the v1.4.18 expansion set:

- Unit (expansion-metrics.test.ts) — drives synthetic data through
  every new evaluator (count by type, mood mean improvement, weekend
  warrior, hidden Easter-egg triggers including the Berlin TZ
  classifier for night-owl / early-bird) and the earnability flag
  builder. 13 new assertions.

- Integration (achievements-expansion.test.ts) — round-trips real
  prisma rows (MoodEntry's YYYY-MM-DD `date` string, Measurement
  type discrimination, audit log row) through
  `buildExpansionMetricValues` so a schema drift in any of the row
  shapes surfaces here rather than at runtime. We don't drive the
  full HTTP route in this spec because of pre-existing
  medication_schedules.days_of_week migration drift.

- E2E (achievements.spec.ts) — the existing sidebar smoke gets a
  sibling describe-block exercising all four card states (unlocked /
  locked-earnable / hidden-locked / hidden-unlocked) and asserting
  hidden-locked cards never leak their real strings to the rendered
  HTML, which is the entire point of the "hidden" state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:34:32 +02:00
Marc-André BombeckandClaude Opus 4.7 3e1607471b fix(admin): suppress painted scrollbar on mobile section strip
Production probe at /admin/api-tokens on a Pixel-5 viewport pinned the
scrollbar that kept showing up after two earlier fixes: it lives in
the AdminShell mobile section strip (13 entries, scrollWidth ~1700 vs
clientWidth ~360), not in the api-tokens table. The strip sits right
above the api-tokens card, which is why every previous attempt
targeted the table while the bar stayed put.

Adds a `no-scrollbar` utility in globals.css (scrollbar-width: none +
::-webkit-scrollbar { display: none }) and applies it to the strip in
both AdminShell and SettingsShell. Swipe and keyboard-arrow scrolling
keep working; the painted bar simply doesn't draw.

New AdminShell unit suite locks in the class so a future refactor
can't silently regress it; settings-shell test gets the matching
assertion. The api-tokens e2e spec gains a regression check that the
strip declares `no-scrollbar` and overflow-x-auto together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:16:56 +02:00
Marc-André BombeckandClaude Opus 4.7 c63cddc6dd style(insights): prettier sweep on C1 + C2 wire-up files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:34:09 +02:00
Marc-André BombeckandClaude Opus 4.7 8a5b6de27e fix(dashboard): mount InsightsCardPreview on root page (wire B1b dashboard preview to live route)
The polished InsightsCardPreview component shipped in B1b commit 5
(d2cdf9d) was an orphan with zero non-test imports. The dashboard read
no advisor payload, so the top severity-ordered AI recommendation +
ring confidence meter + "View all" CTA was unreachable from any live
route. Wave-D design review flagged this as CRITICAL C2.

Wires the preview into the root dashboard page above the chart row,
sharing the advisor cache key with /insights so a regenerate on either
surface refreshes the other without a second LLM call. The preview
self-hides when the user has no provider configured (route 422 → null)
or the advisor returns zero recommendations.

Adds insightsPreview to DASHBOARD_WIDGET_IDS so the layout toggle
persists through Settings → Dashboard. Pins the widget into
DEFAULT_DASHBOARD_LAYOUT (visible: true, tileVisible: false) so users
discover the feature without hunting through settings, and adds a
contract test mirroring the A5 widget-enum-drift guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:33:01 +02:00
Marc-André BombeckandClaude Opus 4.7 aae968a6d5 fix(insights): mount InsightAdvisorCard on /insights page (wire B5c/d/e + B1b polish to live route)
The /insights page rendered only the text-only InsightStatusCard per
section, hiding all of B5c (rec rationale + mini-chart), B5d (confidence
meter), B5e (thumbs feedback) and the broader B1b polish from users.
Wave-D design review flagged this as CRITICAL C1.

Wires the InsightAdvisorCard component into the live route, consuming
the existing /api/insights/generate POST. The route already returns the
24h-cached payload without burning a rate-limit token, so a single
POST-without-force is functionally a GET-or-generate — no new API
surface needed (the reconcile report deferred C1 because it assumed
otherwise).

The cache is shared with the dashboard preview via the new
queryKeys.insightsAdvisor() so a regenerate on either surface refreshes
the other without a second LLM call.

Per-section InsightStatusCards stay below as supplemental detail; the
hero now also wires its regenerate button to the same handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:28:52 +02:00
Marc-André BombeckandClaude Opus 4.7 3848b625af test(e2e): AI provider dropdown switches the rendered config form
Locks in the B2 contract: /settings/ai exposes the active-provider
Pulldown, switching the dropdown swaps the form below it (Codex →
OpenAI → Anthropic → Local), and the fallback chain card surfaces
all configured rows with their stable data-chain-row markers.

The spec mocks every backend endpoint the section reads so the run
needs no real Codex / OpenAI keys in CI. data-testid markers on each
config form keep the assertions independent of the locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:11:35 +02:00
Marc-André Bombeck 00848b7ed5 fix(e2e): skip desktop-sidebar specs on mobile + stub /api/dashboard/widgets
Three remaining e2e failures after the global-setup tour-completed fix
all came from cross-project sharing — specs that target desktop-only
DOM (the `aside[aria-label="Sidebar"]` logo + Achievements link)
were running in the chromium-mobile profile too, where the sidebar
is `hidden md:flex` by design. Added `test.skip(viewport.width < 768,
...)` guards so each spec only runs in the project that can satisfy
its contract.

The fourth failure was the `/api/dashboard/widgets` endpoint that the
A5 tile-strip work added since `onboarding-flicker.spec.ts` was
written. Without a stub the unauthenticated dashboard 500s on this
query and React Query's error boundary swallows the page render
before `<GettingStartedChecklist>` mounts. Added the stub to both
flicker specs (complete + incomplete-onboarding paths).
2026-05-10 00:50:00 +02:00