Commit Graph
547 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 4feacad088 fix(admin): drop sidebar reference from welcome subtitle
The admin overview welcome copy read "Pick a section from the
sidebar to manage HealthLog." but the mobile admin shell uses a
horizontal pill strip, not a sidebar — so the instruction was
literally wrong on the screen size where it mattered most. Drop
the surface reference and let the copy work on every layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:36:49 +02:00
Marc-André BombeckandClaude Opus 4.7 ff6e18473a fix(forms): hint browser locale on date and datetime inputs
Native `<input type="date">` and `<input type="datetime-local">`
were rendering `mm/dd/yyyy` even when the German locale was
active, because Chromium picks the input's locale from the
nearest `lang` attribute on the element rather than from
`<html lang>`. Pass `lang={locale}` on every date/datetime input
in the app — onboarding DOB, account DOB, export filters,
doctor-report range, measurement / medication / mood timestamp
fields — so DE users now see `TT.MM.JJJJ`. Display formatting
already routed through `useFormatters()`; this closes the form
half of the same gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:36:23 +02:00
Marc-André BombeckandClaude Opus 4.7 7a6bc812c9 i18n(achievements): replace insulting badge titles
The over-intake and skipped-intake achievements shipped with the
literal titles "Idiot" / "Lazy Boy" in both EN and DE bundles —
the German file never translated them. For a personal health app
the user gets called an idiot any time they double-dose because
they forgot they already took it. Replaced with neutral,
descriptive titles ("Double take" / "Stepped back" — "Doppelt
genommen" / "Pause eingelegt"). Locked in by a regression suite
that scans every badge title against a banned-word list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:32:19 +02:00
Marc-André BombeckandClaude Opus 4.7 dc75fe846a fix(admin): restrict login overview to auth events
The Login Overview page promised "authentication and admin events"
in its subtitle but the underlying audit-log query never set
`filter=auth`, so the table mixed insights.* / admin.* / data.*
rows in with passkey + password logins. Default the page request
to `filter=auth` (an explicit action picker still overrides), and
restrict the action dropdown to actions starting with `auth.` so
the filter UI stays in sync with what the page actually shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:30:51 +02:00
Marc-André BombeckandClaude Opus 4.7 180f46c24e fix(medications): localise time-window range string
`formatTimeWindowRange` used to hard-code the German "bis ... Uhr"
suffix, so an English-locale user saw "Today, 19:00 bis 23:00 Uhr"
on the medication card and the medication form. Pass the active
locale through and emit "19:00 – 23:00" for English while keeping
the existing German output for DE users (and unchanged for legacy
call sites that still don't pass a locale).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:29:07 +02:00
Marc-André BombeckandClaude Opus 4.7 79ad5c47a9 docs(planning): A7 report + STATE update for admin polish
Five atomic commits landed for A7:

- 088832a — feedback tab strip mini-scrollbar removed
- dd8212e — api-tokens table truncate-with-tooltip (4th attempt)
- 7a70db6 — api-tokens Einklappen button removed
- 6507646 — Zielwerte page whitespace tightened
- 90a109d — Zielwerte status labels translated to DE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:24:05 +02:00
Marc-André BombeckandClaude Opus 4.7 90a109d0b4 i18n(admin): translate Zielwerte status labels (Low/On Target/Stable/Moderate to DE)
Marc reported the Zielwerte page (`/targets`) still surfaces English
status pills even when the rest of the UI runs in German: "Low /
On Target / Stable / Moderate / Normal" stayed in source language
because the server emits stable English category strings and the
page rendered them verbatim. Card titles ("Resting pulse", "Mood
stability", "Medication compliance" …) had the same issue.

Add two i18n key blocks under `targets.*`:

- `targets.label.<TYPE>` — one entry per measurement type, used by
  `<TargetCard>` to localise the card title. The server keeps
  emitting English labels (so logs / external consumers don't
  churn); the page swaps them in via `t("targets.label." + target.type)`.
- `targets.status.<key>` — every classification category from
  `lib/analytics/classifications.ts`, `pulse-targets.ts`, and the
  inline maps in `targets/route.ts`. A small `STATUS_CATEGORY_KEY`
  map on the page normalises the human-readable English category
  to the i18n key.

Glucose targets keep their existing key path (the server already
emits `targets.glucoseFasting` etc. as the label, resolved at the
top of `<TargetsPage>` before cards mount).

Fall-through is graceful: if a future server adds a category we
have not translated yet, the verbatim English string still
renders — the page never blows up.

Also adds `targets.bmi`, `targets.label.BMI`, `targets.status.normal`,
`targets.status.optimal`, `targets.status.fitness` to the i18n
integrity test's PLACEHOLDER_ALLOWLIST: those terms are identical
in EN and DE (BMI is a global acronym; "Normal" / "Optimal" /
"Fitness" are technical terms German clinics use verbatim).

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 6507646a90 style(admin): reduce excessive vertical whitespace on Zielwerte page
Marc reported "relativ viel Platz" wasted between the overview
header and the first cards on `/targets` (Zielwerte). The page
used `space-y-8` (32 px) on the outer wrapper, which leaves an
oversized gap between the intro paragraph and the first card.

Tighten to `space-y-6` (24 px) — same rhythm the admin and
settings pages use, so the whole app shares one Dracula-spacing
beat.

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 7a70db6c86 fix(admin): hide collapse button when page has only one section
`/admin/api-tokens` carried a Collapse / Expand toggle inherited
from the v1.4 shared admin page where 13 sections lived together.
On the v1.5 dedicated route the toggle just hides the only card on
the page — Marc reported it as "sinnlos" (pointless): the page
title already says "API Tokens", the only thing below it is the
tokens card, and collapsing it leaves an empty surface with the
section heading staring back at you.

Drop the local `expanded` state and the toggle button from the
section. The card always renders, the query always fetches, and the
header is now a plain icon + title row. The shared
`settings.collapse` / `settings.expand` i18n keys stay in messages
because `<LoginOverviewSection>` still uses them.

The matching test asserts no Collapse / Expand / Einklappen /
Ausklappen string appears in either locale, and that no
`aria-expanded` button shows up on the section header.

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 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 088832aa39 fix(admin): remove spurious scrollbar from feedback tab strip
Marc reported a stray "mini scroll-button" on the right side of the
admin feedback inbox tab strip (Open / Acknowledged / Resolved /
Archived). The four short tabs do not horizontally overflow the
viewport, so the painted bar surprised him every time he opened the
inbox.

Root cause is a CSS overflow coupling: setting only `overflow-x: auto`
silently forces `overflow-y` to `auto` too, because a single axis
cannot be `visible` while the other clips. The strip is locked at
`h-9` (36 CSS px) but its badge children (`py-0.5` + text-xs digits)
ride 1-2 px taller than the strip on some glyph stacks, so the
implicit `overflow-y: auto` paints a tiny vertical scrollbar on the
right edge.

Fix is a single class on `tabsListVariants`: `overflow-y-hidden`
(plus `overflow-y-visible` in the vertical-orientation override so
vertical tab columns are not affected). Horizontal swipe and
keyboard-arrow scrolling still work — only the implicit y-axis bar
is suppressed.

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 ef67da5570 docs(planning): A3 report + STATE update for /insights polish
Marks Wave A bucket A3 complete. Six commits on origin/main
(60a91af → f2b21a4) covered all six concerns Marc raised about the
/insights page on 2026-05-10. Detailed walkthrough in
.planning/phase-A3-report.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:20:00 +02:00
Marc-André BombeckandClaude Opus 4.7 f2b21a47db test(insights): add insights-polish.test.ts (followup to 5360d3c)
Adds the actual test file that 5360d3c's message described — the
previous commit only landed the .planning/* files for A6 because a
parallel agent's `git add` interleaved with mine and reordered the
working-tree state mid-commit.

The test suite itself was already passing locally (9/9) and against
the 1646-test full suite; this commit lands the file so CI sees the
guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:16:57 +02:00
Marc-André BombeckandClaude Opus 4.7 5360d3c0ce 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>
2026-05-10 13:16:19 +02:00
Marc-André BombeckandClaude Opus 4.7 e0c007a8af docs(planning): A2 report + STATE update for charts mobile audit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:15:14 +02:00
Marc-André BombeckandClaude Opus 4.7 fa91a73aef fix(insights): strip lowercase chart tokens from AI prose
Marc reported a raw "metric: blood_pressure_sweet" string at the
bottom of /insights — looked like a debug code or unrendered
template. Root cause: the v1.4.16 strip regex
`/metric:[A-Z_]+/g` only matches uppercase letters and underscores,
but the model occasionally emits lowercase / snake_case forms like
`metric:blood_pressure` or `metric:weight_trend`. Those slipped
through `stripChartTokens()` unchanged and the literal token text
landed in the DOM.

Split the regex in two:
 - `STRIP_TOKEN_REGEX` (`[A-Za-z0-9_]+`) — permissive; cleaves any
   case from prose so unrenderable / hallucinated tokens never
   surface as text.
 - `PARSE_TOKEN_REGEX` (`[A-Z_]+`) — strict; the render path still
   only mounts charts for the uppercase allowlisted vocabulary.

Allowlist semantics are unchanged — `parseChartTokens()` still
returns only entries from `ALLOWED_CHART_TOKENS`. The change is
purely about defending the visible-prose surface from the wider
class of token shapes the LLM occasionally emits.

XSS-style trailing junk (`metric:WEIGHT' onclick='alert(1)'`)
still cleaves cleanly: the apostrophe terminates either class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:14:53 +02:00
Marc-André BombeckandClaude Opus 4.7 335f288d69 refactor(insights): drop duplicate metric tile strip
The /insights page used to lead with a 5-column grid of TrendCards
(weight, BP-sys, BP-dia, pulse, plus body-fat and mood when
present) — exact duplicates of the dashboard tile-strip. Marc said
the tiles "duplizieren Dashboard, nehmen nur Platz weg" and asked
for them gone.

Drop the strip, the helper functions that supported only it
(`getRangeColorClass`, `getRangeHint`, `RangeDisplayConfig`), and
the bookkeeping locals (`bf`, `showBodyFatCard`, `moodSummary`,
`showMoodCard`, `fmt`). Remaining mood references on the page read
`data?.moodSummary` inline; every other helper was used only by the
removed strip.

Net: -157 lines, no behaviour change anywhere except the missing
duplicate strip. Recommendations grid + per-section status / chart
cards below stay intact — those are the actual value of /insights.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:14:05 +02:00
Marc-André BombeckandClaude Opus 4.7 98a3d10705 refactor(insights): single page-level refresh and drop orphan subtitle
The /insights page used to surface the same regenerate handler three
times — the page hero's "Neu generieren" button, the advisor card
header's icon-only refresh button, and the empty-state "Analyse
starten" CTA inside the same card. Marc described the trio as
"gefuehlt drei Buttons, alle machen das gleiche".

Drop `onRegenerate` from the page's `<InsightAdvisorCard>` mount.
The hero remains the single page-level refresh affordance; the
card-level icon button and empty-state CTA disappear because they
short-circuit on `!onRegenerate`. The per-recommendation Regenerate
(inside `<RecommendationCard>`) is unrelated — that's per-card
context, not page-level — and stays as-is per the v1.4.16 spec.

Same patch drops the placeholder "Persoenlicher Berater" subtitle
(`insights.aiOverviewTitle`) the page passed in as the card's
secondary line. With no provider configured, the empty card
rendered just an icon and that subtitle above blank space, which
Marc described as "Titel da aber NICHTS PASSIERT". The CardTitle
("KI-Gesundheitsanalyse") is enough framing on its own; the
subtitle prop on `<InsightAdvisorCard>` becomes optional and
existing callers that still pass one keep their line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:12:24 +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 60a91afaee feat(insights): comparison toggle on-surface in hero meta band
Move the Vormonat / Vorjahr comparison toggle from a standalone row
below the page hero into the hero's own meta slot. The previous
layout read as three stacked cards with separate header / control
bands — hero, then comparison toggle, then advisor card — and the
toggle row had no other content, so it just hung there in the gap.

Folding the toggle into the hero clusters every page-level control
(personal-baseline indicator, Generated timestamp, Regenerate
button, comparison baseline) into one band. The hero now exposes a
generic `metaSlot` prop so future page-level controls can join the
band without another component refactor.

Settings → Dashboard remains the power-user backdoor for either
surface; the dashboard's on-surface toggle stays gone (Marc said it
crowded the tile strip too much).

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 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 717a025f0c style(settings): uniform vertical spacing across all sections
Pre-fix the per-card vertical rhythm was either `space-y-3` (AI
provider config forms), `space-y-5` (dashboard layout card) or
`space-y-4` (Account → Profile form). Three competing strides for
forms that visually live in the same shell, so the eye picks up the
discontinuity even though the section-level `space-y-6` outer rhythm
was already consistent.

Standardize on `space-y-4` between fields inside any single Settings
card:

- Dashboard "Customize layout" card: `space-y-5` -> `space-y-4`
- AI Codex / OpenAI / Anthropic / Local provider forms: `space-y-3` ->
  `space-y-4`

Account, Notifications and Export already used `space-y-4`. Section
roots and the AI parent card keep their `space-y-6` so distinct cards
still read as distinct surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:09:11 +02:00
Marc-André BombeckandClaude Opus 4.7 737b533089 style(settings): uniform vertical spacing across all sections
Pre-fix the per-card vertical rhythm was either `space-y-3` (AI
provider config forms), `space-y-5` (dashboard layout card) or
`space-y-4` (Account → Profile form). Three competing strides for
forms that visually live in the same shell, so the eye picks up the
discontinuity even though the section-level `space-y-6` outer rhythm
was already consistent.

Standardize on `space-y-4` between fields inside any single Settings
card:

- Dashboard "Customize layout" card: `space-y-5` -> `space-y-4`
- AI Codex / OpenAI / Anthropic / Local provider forms: `space-y-3` ->
  `space-y-4`

Account, Notifications and Export already used `space-y-4`. Section
roots and the AI parent card keep their `space-y-6` so distinct cards
still read as distinct surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:09:00 +02:00
Marc-André BombeckandClaude Opus 4.7 0cf23f34ca 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
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:07:25 +02:00
Marc-André BombeckandClaude Opus 4.7 77a3ad3bc8 fix(charts): mobile header layout + unified x-axis tick density
Pixel-5 audit against PROD (393 px) showed two issues with the
chart cards on /dashboard and /insights:

1. The bucket-aggregation chip ("Wochendurchschnitt" / "Weekly avg"),
   the optional comparison-period chip, the four range tabs and the
   cog dropdown all sat on a single justify-between row in the chart
   header. On viewports below the sm breakpoint the chips ate enough
   horizontal space that the tabs wrapped to a second row inside the
   same flex container, doubling the header height. On Galaxy Fold
   compact (280 px) the tabs split across three or four rows and the
   medication card overflowed horizontally.

2. The medication compliance chart drew one tick per data point on
   the x-axis. A 30-day Pixel-5 window painted thirty overlapping
   labels into a 393 px gutter while the weight / BMI charts looked
   sparser by accident, so the chart row read inconsistently.

The header is now stacked into two rows below the sm breakpoint:
title + chips on row 1, range tabs + cog right-aligned on row 2 with
flex-nowrap so the tabs always fit. The decorative bucket-aggregation
chip and comparison captions hide on mobile (the range tabs already
communicate the visible window). Above sm the original side-by-side
layout is preserved.

A new helper at src/lib/charts/x-axis-density.ts caps visible ticks
at 4 (≤360 px Fold), 6 (≤480 px Pixel-5 / iPhone-12), 8 (small tablet)
and 10 (desktop) and is wired into HealthChart, MoodChart,
MedicationComplianceChart and ComplianceLineChart so every chart row
reads the same density. A new useViewportWidth hook keeps it reactive
without an SSR penalty (desktop default during server render, real
measurement after mount).

The scatter-correlation chart was deliberately left alone — its
x-axis is numeric (BP / weight values, not a time series) and already
supplies an explicit `ticks` array.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:07:08 +02:00
Marc-André BombeckandClaude Opus 4.7 10757846bb fix(settings): Sprache select position + width consistent
The Sprache (language) select used to share a 2-column grid row with
"Geburtsdatum" inside the Profile card. That pairing was misleading on
two counts:

1. Language is a per-session UI preference (cookie-backed) — not a
   profile attribute stored on `users`. Sitting next to a column-
   stored field (date of birth) implied the wrong persistence model.
2. The two-column grid collapsed to single-column on `<sm`, leaving
   a four-line stack of dob -> dob-hint -> language -> language-hint
   inside a card that already had three other field rows. Marc
   flagged the visual stride as "awkward".

Lift Language out of the Profile-data block into its own row at the
bottom of the Profile card. On `>=sm` it uses `max-w-xs` so its width
matches the other half-width fields rather than stretching to the
full card width (which would make it the visually heaviest control on
the page). On `<sm` it stays full-width — same as every other field.

Drive-by: pull the duplicated native-select class string into a single
`NATIVE_SELECT_CLASS` constant so gender + language render identically
(previous gender select used `placeholder:text-muted-foreground` which
language did not — minor but visible at the focus-ring level).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:06:33 +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 9fda634369 fix(settings): action-button placement consistent across sections (right-side desktop, stacked mobile)
The Account → Password and Account → Restart-onboarding-tour cards
both used `flex items-center justify-between gap-2` to put the action
button right-aligned next to the card title. On a 393-px Pixel 5
viewport the tour button (216 px wide) overflowed the card's right
edge by ~48 px because nothing forced it to wrap.

Apply a single shared layout to every "title + action" row in
Settings: stacked column on `<sm` (button below title + description,
full-width), title + action on the same row on `>=sm`. Same pattern
applied to Dashboard's "Reset to defaults" so the long German copy
("Auf Standard zurücksetzen") cannot push past the card edge either.

Pulled the description paragraph inside the title cluster on the two
account cards so each row is now a clean `<title-block + button>`
flex pair — easier to reason about than the previous title-only flex
with the description orphaned outside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:05:11 +02:00
Marc-André BombeckandClaude Opus 4.7 0dcc91aae8 refactor(integrations): single status pill, drop redundant containers
Withings card had three status displays (top-right badge, mid-card
banner with the same state + two timestamps, and an inner badge).
Mood Log was even worse — four, including a "letzter Sync" line at
the very bottom. The pill in the card header is now the canonical
surface for both. Withings keeps its actionable error message under
the pill (the pill can't fit "Withings refresh error: 503 - upstream"),
the duplicate {n}/{threshold} badge is gone. Mood Log gains the visual
divider Withings always had — Marc called the asymmetry out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:04:27 +02:00
Marc-André BombeckandClaude Opus 4.7 957f8e9b93 fix(settings): equalize input heights across all settings sections
Settings inputs and selects rendered at four different heights on
Pixel 5: 32 px (AI add-provider), 36 px (account form, export, ntfy /
telegram), 40 px (AI active-provider) and 44 px (dashboard "Compare to"
trigger). The mismatch was visible whenever two cards lived next to
each other, e.g. the 40-px provider select sat directly above a 32-px
"Add provider" select inside the same column.

Standardize on the shared 36-px contract (`h-9`, matching `<Input>` and
the default `<SelectTrigger>` size) for every text-entry field across
Settings:

- AI active-provider native select: `h-10` -> `h-9`
- AI fallback-chain "Add provider" native select: `h-8` -> `h-9`
- Dashboard "Compare to" `<SelectTrigger>`: drop `min-h-11` so the
  default `h-9` applies

The 44-px tap-target floor (WCAG 2.5.5) still applies to mobile
nav-level controls; for inline form fields the 36-px height matches
the rest of the settings shell and the desktop sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:04:01 +02:00
Marc-André BombeckandClaude Opus 4.7 dcef89c59e docs(planning): A1 report + STATE update for BD-Zielbereich fix
Live-DB audit confirmed the headline was a literal copy of 30T
(`bpInTargetPct = windows.last30Days?.pct`); fix added an `allTime`
window so the three numbers compute independently. Marc's prod data
verifies the asymmetry: 7d=50%, 30d=50%, all-time≈11%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:02:39 +02:00
Marc-André BombeckandClaude Opus 4.7 a8562727a7 fix(dashboard): BD-Zielbereich headline shows all-time, not 30-day
Marc reported the BD-Zielbereich tile pinned at exactly 50/50/50 for
7T, 30T, and the headline ("total") on his real production data — the
fourth attempt at this surface (v1.4.15 → v1.4.16 → v1.4.18 → v1.4.19).

Root cause: the analytics route routed `bpInTargetPct` (the headline)
through `windows.last30Days?.pct` — a literal copy of the `30T`
sub-value. Since `computeBpInTargetWindows` only returned 7d + 30d
windows, the headline could never legitimately differ from `30T`. For
Marc's real data (572 paired readings since 2022, recent 30d = 50 %,
all-time ≈ 11 %) the tile read 50 % / 50 % / 50 % and looked
algorithmically pinned — exactly the symptom he reported.

The previous three passes treated this as either a predicate bug
(v1.4.16 ceiling-vs-narrow-band, real and merged) or a missing
`avg7`/`avg30` wiring (v1.4.18, also real and merged). Both fixes
landed correctly, but neither addressed the headline duplication.

Fix:
  - `computeBpInTargetWindows` now returns a third `allTime` window
    that aggregates EVERY paired reading in the input series.
  - The analytics route fetches all paired BP rows (no 30-day cutoff)
    and routes the headline through `windows.allTime?.pct`. The route
    only re-pairs once internally so the cost is paid once per request.
  - The 7T / 30T sub-values are unchanged (still windowed slices of
    the same input).

Failing test reproduces the bug with Marc's data shape (`allTime` MUST
not equal `last30Days` once older history is present); fix turns it
green. Integration test seeds 40 paired readings against real Postgres
and asserts all three windows compute independently.

Hand-counted against Marc's prod data:
  - 7T:        50 % (1/2) — matches what he sees, real coincidence
  - 30T:       50 % (5/10) — matches what he sees, real coincidence
  - Headline:  ~11 % (62/572) — was 50 %, now correct

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:01:08 +02:00
Marc-André BombeckandClaude Opus 4.7 ba0d6b87d1 feat(integrations): IntegrationStatusPill component (single tag, mobile-safe)
Settings → Integrations had three- to four-fold redundant status
displays per card. The new pill is the one place a connection status
appears: top-right, Dracula-tokenized chip, "Connected · 12 min ago"
relative-time pattern. Locale-aware (EN + DE), mobile-safe (whitespace-
nowrap, abbreviated "min" / "h" / "d" forms so it fits a 360px card).

Reusable for v1.4.20's Apple Health card on iOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:00:11 +02:00
Marc-André BombeckandClaude Opus 4.7 b5e9a95227 fix(ai): no default-positivity opener about data quality (mention only when limited)
Every insight summary opened with the same "Datengrundlage ist sehr
stark" / "Your data foundation is strong" filler — the user does
not see what fields the snapshot carries, so the opener reads as
awkward stock praise.

Add ground-rule 7 to both locales of the system prompt: don't open
with a compliment about the data quantity or quality, mention data
quality only when it materially limits the analysis (n<7 in the
analyzed window, recencyDays>14 since the last entry, or a coverage
gap). When data is fine, dive straight into the analysis.
Banned-opener phrases listed verbatim in both EN and DE so the
model does not paraphrase the same sentiment.

PROMPT_VERSION bumped 4.16.1 → 4.19.0 so feedback aggregation can
distinguish responses generated under the new rule.

New test file pins the rule, the trigger thresholds, the banned
phrases, and the dive-straight-in instruction in both locales.
Existing PROMPT_VERSION pins relaxed to allow any 4.x.x ratchet so
future minor bumps don't sweep up these assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:58:53 +02:00
Marc-André BombeckandClaude Opus 4.7 56729a7de3 chore(planning): bootstrap v1.4.19 marathon
Polish/bugfix release. Wave A spans eight buckets — BD-Zielbereich
constant 50% (4th attempt, live-DB audit), charts mobile axis-label
overflow + X-tick-density consistency, /insights polish with
comparison-toggle relocation and placeholder-leak cleanup, AI prompt
rework dropping the auto "Datengrundlage stark" first sentence,
Settings/Integrations status-UI consolidation, Settings mobile audit,
Admin polish (Feedback tabs, api-tokens 4th attempt via ellipsis +
tooltip, Zielwerte i18n + whitespace), and a write-only quality-of-
life audit feeding Wave B. Wave D ends on a Product-Lead pass briefed
for the next strategic milestone (Insights redesign + AI Coach).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:51:33 +02:00
Marc-André BombeckandClaude Opus 4.7 106ef2de5b docs(audit): v1.4.18 release summary
Marc-Brief + per-area landed/deferred breakdown for the v1.4.18
release. Charts walked back from gradients/emoji/auto-mean to clean
lines + opt-in per-chart overlay toggles, achievements roster grew
38 → 59 with 6 hidden Easter-eggs, BD-Zielbereich tile sub-values
wired, /admin/api-tokens scrollbar finally pinned to AdminShell.

STATE.md flipped to `finished`; final status block + Phase E3 block
appended; Phase E checklist all-ticked. Phase E1 and E3 reports
included with this commit (drift left from earlier marathon waves).

v1.4.18 LIVE — image digest sha256:c636fca7db66…, /api/version=1.4.18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:26:38 +02:00
Marc-André BombeckandClaude Opus 4.7 922413e27c docs(planning): phase-E2 v1.4.18 deploy-verify report
v1.4.18 live in production. GHCR both green, force-pulled on apps-01
(Coolify git-push trigger deferred). Image digest 936e9cf2 -> c636fca7,
/api/version 1.4.17 -> 1.4.18, smoke 14/14 200, GH release created.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:22:57 +02:00
Marc-André BombeckandClaude Opus 4.7 0243e20853 chore(release): v1.4.18
Per-chart overlay toggles, expanded achievements roster (incl.
hidden Easter-eggs), BD-Zielbereich 7T/30T sub-values wired,
chart visual revert to clean lines, and the AdminShell mobile
section-strip scrollbar fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.18
2026-05-10 11:14:30 +02:00
Marc-André BombeckandClaude Opus 4.7 c072ad8416 style(planning): prettier sweep on reconcile + v1.4.19 backlog
Whitespace + table alignment to keep the planning markdown
consistent with the rest of the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:10:59 +02:00
Marc-André BombeckandClaude Opus 4.7 481403dc3d style(docs): prettier sweep on audit summary tables
Whitespace-only changes from the same pretter pass that cleared
the rest of the tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:10:09 +02:00
Marc-André BombeckandClaude Opus 4.7 395129a18c docs(planning): file v1.4.18 reconcile + v1.4.19 backlog + v1.5 update
Wave-D reconcile report at .planning/phase-D-reconcile-report.md;
deferred items seeded to .planning/v1419-backlog.md; strategic
v1.5 items appended to .planning/v15-backlog.md from the v1.4.18
Product-Lead review. STATE.md marks Wave D done.

Prettier sweep also touched the planning markdown table widths;
no content change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:09:41 +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 c6e3ac69e8 fix(achievements): cap consistentMonthCount and clarify mood-improvement window
consistentMonthCount kept growing every calendar month with ≥25
active days even though the only achievement it backs unlocks at
target 1. The unbounded count served no UI purpose, churned the DB
on every poll, and inflated the wire payload over time. Cap at 1
once any month hits the threshold.

Also rewrites the moodImprovementHit doc-comment to accurately
describe the sliding-window semantics. The code slides over distinct
logged days, not 7 calendar days, so a user with sparse logging
compares the 7 most-recent logs against the 7 before that regardless
of calendar gap. The previous comment claimed calendar-day windowing
which the code never implemented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:03:52 +02:00
Marc-André BombeckandClaude Opus 4.7 cf75579470 fix(dashboard): wrap chart-overlay-prefs PUT in Serializable transaction
Two concurrent toggles on different charts (user opens two tabs,
flips BP "Target range" in one and Weight "Trend" in the other
within ~50ms) could drop one of the updates. Both reads landed on
the same dashboard-layout snapshot, then each wrote its merged blob
back, with the second write clobbering the first chart's change.

Wrap the read-modify-write in a $transaction with Serializable
isolation so the second writer either sees the first writer's
result or aborts. Same pattern any future widget-prefs route should
follow until we move the JSON to a jsonb_set-based atomic merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:03:03 +02:00
Marc-André BombeckandClaude Opus 4.7 194ec2f9da fix(chart-overlay-controls): bump cog to 44x44 tap target
The settings-cog trigger was 28x28 CSS px (h-7 w-7), below the
WCAG 2.1 AA 44x44 minimum. Sat in the same flex row as the 44 px
range tabs, so a Pixel-5 thumb missed the cog more reliably than
the surrounding tabs. Switch to min-h-11 min-w-11; icon stays at
h-3.5 w-3.5 so visual rhythm doesn't bloat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:02:35 +02:00
Marc-André BombeckandClaude Opus 4.7 fbf14fceb8 fix(mood-chart): make chartKey opt-in and don't leak overlay state across pages
The MoodChart component defaulted chartKey to "mood", which meant
every consumer that omitted the prop — including /insights and the
recommendation-card mini chart — picked up the dashboard's persisted
overlay prefs and rendered the settings cog. Toggling Trend on
/insights would silently flip the dashboard mood chart on next paint
because both reads shared the same persistence key.

Drop the default; the dashboard now passes chartKey="mood" explicitly
and ad-hoc surfaces stay clean (no cog, default overlays). Also wraps
the title row's right-side container with flex-wrap + justify-end so
the 4 range tabs + cog don't push the card horizontally on Pixel-5
when the comparison caption is also in the row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:02:27 +02:00
Marc-André BombeckandClaude Opus 4.7 720e6c89b9 refactor(v1.4.18): apply simplify-review safe suggestions
Seven mechanical cleanups, no behaviour changes:

- Drop the duplicate ChartOverlayPrefs type + DEFAULT in
  chart-overlay-controls; re-export the canonical pair from
  dashboard-layout so a future fourth toggle can't drift between
  the UI and persistence layers.
- useChartOverlayPrefs accepts null/undefined chartKey and short-
  circuits the dashboard-layout fetch when none is supplied. Drops
  the chartKey ?? "bp" fallback and the three discard-the-result
  ternaries in health-chart; mini-mode renders no longer subscribe
  to the BP chart's persisted prefs.
- /api/analytics no longer pair-and-counts BP twice — the windowed
  helper already produces last30Days, so the headline reads it back
  instead of running computeBpInTargetPct on the same input.
- Drop the redundant moodEntries.map shape in the achievements
  route; the prisma select already returns the matching shape.
- Merge the five hidden-metric cases into the no-precondition block
  of isEarnable; the defensive comment admitted they couldn't fire.
- Remove the deprecated getAchievementCategory wrapper (zero callers).
- Strip six "v1.4.18 gradient removed" what-comments scattered
  across the chart wrappers; the polish tests pin the absence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:01:01 +02:00
Marc-André BombeckandClaude Opus 4.7 545f44c074 fix(achievements): redact hidden-locked entries from API response
Hidden Easter-eggs leaked their trigger semantics through the JSON
response: a curious user with DevTools open could read metric names
(nightOwlCount, leapDayCount, etc.), i18n keys (hiddenNightOwl,
hiddenLeapDay, etc.), icon names, and target values without unlocking
anything. The opaque-card UI was already correct, but the wire shape
spoiled the surprise.

Project hidden+locked entries to a sentinel shape before serialising
(achievements.hiddenCard.title key, HelpCircle icon, target 0,
metric totalTakenIntakes), and drop hidden-only metric counters from
the metrics block unless the matching achievement is unlocked. The
iOS branch reuses the same redacted view so native clients can't
short-circuit the surprise via the resolved-strings shape.

Once an Easter-egg unlocks the real fields ship — the unlock IS the
reveal moment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:56:46 +02:00
Marc-André BombeckandClaude Opus 4.7 3d585571d8 docs(planning): chart visual revert + per-chart toggles done
Mark the A3 task list complete in STATE.md and ship the detailed
report covering the six atomic commits, the architectural deviations
(consolidated HealthChart wrapper, JSON-blob persistence instead of a
new column), and the test deltas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:38:17 +02:00
Marc-André BombeckandClaude Opus 4.7 178c6dc2c9 chore(planning): mark B1 achievements expansion done in STATE.md
All six checklist items closed. Roster 38 → 59 (+21 = 15 public +
6 hidden) with discovery-filter logic so locked public badges only
appear when the user has the underlying data, and hidden cards
paint opaque placeholders that never leak their triggers to the
DOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:36:57 +02:00