Commit Graph
24 Commits
Author SHA1 Message Date
Marc-André Bombeck 37f922af97 Fix settings field rows, defaults and narrow layouts
Module rows drop the field-label colon and inset, the reminder number
fields align, target defaults print at their own precision, the
appointment offer wraps instead of squeezing to one word per line, the
export format legend matches its neighbour label, and the profile-photo
explainer reads as body text.
2026-10-08 20:20:03 +02:00
Marc-André Bombeck d0a9086538 fix(targets): edit glucose thresholds in the reader's glucose unit
The settings threshold editor took the identity path for the glucose
thresholds, so an account on mmol/L was shown a mg/dL default and asked
for mg/dL figures against 40-400 bounds, while the insights sheet for
the same threshold already spoke mmol/L. The shared target adapter now
owns the glucose unit as it owns pounds: fields, inward-rounded bounds,
the default hint and the canonical write all go through it, and the
insights panel and its sheet use the same branch instead of their own
conversion.

Refs #1067
2026-10-01 08:58:24 +02:00
Marc-André Bombeck fffdc83ca4 Make the reset-every-threshold delete ask first
DELETE /api/user/thresholds without ?metric= erased the account's whole
override set on an omitted query parameter alone: no confirmation, no dry
run, and no rate limit of its own while the PUT beside it had one.

The wide form now wants { "confirm": "RESET_THRESHOLDS" } in the body — the
same typed-confirmation shape /api/settings/account and /api/settings/data
already use — and refuses 422 without writing anything. The ?metric= form
is untouched: it resets one band the person can retype, and the targets
sheet fires it in a loop. Both forms now run the PUT's 30 / 5 min limit.

The settings card's "reset all" control already opened a confirm dialog;
it now sends that answer to the server too. Its token is compared against
the route's declaration in the tests, so the two ends cannot drift apart
into a permanent 422.
2026-08-23 16:55:59 +02:00
Marc-André Bombeck f2f40ae8b2 Pair every daily-read invalidation with its inactive refetch
Ten client mutation sites forgot a consumer: a coach-accepted checkup
stayed invisible for up to five minutes, a threshold or target edit left
the dashboard bands stale, a score-config save marked the daily reads
stale but never refetched them, a booked visit missed the Today rail, a
source-priority reorder kept every chart on the old canonical picks, an
adopted self-context note never reached the Settings read. Each site now
invalidates what its write changed and forces the unmounted snapshot +
digest reads to refetch through refetchInactiveDailyReads.

New illnessDependentKeys bundle carries the daily reads (Rest Mode
frames both), dailyDigest joins encounterDependentKeys, and the
chart-data prefix gains a factory accessor for whole-cache flushes.

The daily-reads guard learns the cycle / encounter / vaccination /
illness bundle families and a second matcher for direct snapshot/digest
invalidateQueries without a forced refetch, each with a frozen reasoned
allowlist. Both matchers were proven live by reverting a fix and
watching the guard go red (see the test doc comment).
2026-08-13 18:48:10 +02:00
Marc-André Bombeck b2e1aa0cde refactor(settings): one header, one action row, one integration card
The sweep that follows from the primitives. Nothing here is new behaviour;
every change removes a second way of saying something the surface already
said one way somewhere else.

Headers. The two collapsible security cards stop rebuilding
`SettingsCardHeader` by hand to carry a chevron — the chevron rides inside
the title button, and the description moves out of the collapsed region, so
the first paint is no longer a bare title. The import card shell, the export
and import section headings, and the four free-standing group headers stop
inventing their own type scale: a heading above a GROUP of cards is
`SectionHeading` (`text-base`), a card title is `SettingsCardHeader`
(`text-lg`), and the step between them is what says which is which. The
targets card gets a title instead of a naked icon.

Actions. Every card-level row is `SettingsCardActions`. The primary action
comes out of the header's status slot in six cards (password, web push,
pillar order, coach memory, and the two danger-zone triggers) and out of the
middle of the card in three more; the reset that hung in the dashboard-layout
header joins save as a secondary in the same row. Result and status lines
move ABOVE the row — sources' cross-link, the invite form's two status lines
— because nothing follows a card's actions. Revoking a share link is
`variant="destructive"` rather than an `outline` hand-tinted red in two
different ways. The import row had four variants for four buttons; it has
two. Export downloads are the card's primary, not an outline link.

Integrations. One description sentence with the setup-guide link inline: the
second paragraph was byte-identical across six of eight cards, which is the
tell that it said nothing about the provider it sat under. The `<hr>` under
the header — which existed only in this folder — is gone. So is the hint that
pointed at a guide 40 px above it, and the second format for "not
configured". Connect is one button everywhere: primary, `sm`, in the action
row, instead of primary-and-small in three cards and outline-and-large in
four.

Plus the standards leftovers the audit listed: two `pl-5` list gutters, one
`scroll-mt-20` anchor, a purple header icon, `text-[10px]` badges, four
hand-rolled callout shells that `SettingsInfoTile` already paints, and the
two inline buttons that never had the mobile tap floor at all.
2026-08-07 19:39:11 +02:00
Marc-André Bombeck 5445bfc1f1 refactor(ui): give Settings and Admin one card rhythm and one page header
The two consoles used the same primitives and still read as two systems,
because everything without a primitive had drifted: the header-to-body
step existed in three competing techniques at once (`mt-4` on the first
body child, `space-y-4` on the card, `mb-4` on the header), the body
started either at the card edge or 28 px in, and both shells hand-rolled
the page title instead of calling `PageHeader`.

Move the decisions into the primitives so a call site cannot re-open them:

- `SettingsCard` is gap-based like `Card` (`flex flex-col gap-4`). The
  header-to-body step is the container's, not a margin somebody remembers.
  A dense card still overrides the gap on the card itself.
- The body starts at the card edge. `pl-7` is gone — off the spacing scale,
  and it mis-aligned action rows against their own forms wherever two
  neighbouring cards disagreed about it.
- The card header's description slot loses its `space-y-1`. That was the
  licence call sites used to stack explainer paragraphs into a muted
  `text-xs` slot; the slot holds one sentence.
- New `SettingsCardActions`: one action row, trailing edge, last thing in
  the card. `align="start"` spells the one exception, a card whose only
  actions are links out.
- New `StatTile` replaces four hand-rolled label/value boxes in Admin.
- `SectionHeading` moves from `insights/` to `ui/`. It is the heading above
  a group of cards, which is what Settings needs; Settings had been using a
  card header with no card, which reads as a card that failed to paint.

Both shells now render their heading through `PageHeader`, which gained
`headingAs="div"` for the second copy each shell paints (mobile above the
nav strip, desktop inside the grid) so only one is a real `<h1>`. The
page explainer is muted everywhere: the primitive said foreground, the
standards and every hand-rolled copy said muted, so pages disagreed with
each other. Medications, Vorsorge, the labs / custom-metric / illness
detail pages and the clinician view drop their hand-rolled title blocks
too, including the mid-word truncation on the medications subtitle.

Tests pin the parts that can be checked exactly: the card's gap, the
description slot's missing rhythm, the action row's two alignments, the
`<h1>`-free shells, and a sweep asserting `pl-7` stays gone.
2026-08-07 19:05:15 +02:00
Marc-André Bombeck 48ac8f7e2d fix(ui): a mis-tap must not be able to destroy an allergy list
Settings → AI "About me" had one Clear control wired to
`save.mutate({ aboutMe: "", allergies: "" })`. One tap, no dialog, and
the allergy line went with the note — while the toast it raised said
"About-me note cleared", which is what the control looks like it does.
The allergy line is the first thing a clinician reads and the column is
overwritten in place, so there is nothing to restore afterwards.

Scope the control rather than only gating it: it now writes `aboutMe`
alone and omits `allergies`, which the PUT leaves untouched by contract,
so the payload cannot carry the allergy line away even if the dialog is
ever dropped. Clearing allergies stays possible in the allergies field
itself, which is a deliberate act on the thing being emptied. The
confirmation on top guards the note.

The same one-tap shape ran through thirteen other controls that
overwrite a column or drop a credential with nothing to restore from.
They route through the shared confirmation now: both Codex disconnects
(personal and the instance-wide shared account), the AI server key, the
profile photo, the threshold reset — all, per metric, and the override
switch, whose off position performed the same irreversible delete from a
control that reads as a display toggle — the source-priority order, the
dashboard layout, the Insights overview, a single target range, a
remembered Coach fact, a past Coach plan, a logged cycle day and a custom
cycle symptom.

`ConfirmButton` moves to `components/ui`, since it is no longer a
settings-local helper, and gains `ConfirmDialog`: the same dialog without
a trigger, for destructive controls that are not buttons. A switch has to
stay on when the dialog its off position raised is cancelled, and a
control that could not reach for the primitive reached for nothing —
which is a fair part of why those were the ones that shipped unguarded.

Copy lands in all six locales.
2026-07-26 17:26:56 +02:00
Marc-André Bombeck bf73755899 fix(targets): follow the unit preference in target and threshold panels
Target ranges stayed canonical while every reading surface moved to the
metric/imperial preference in the previous release, so an imperial
account saw a weight chart in pounds above a target reference in
kilograms. The panel is a coupled display and edit unit: converting only
its display would have corrupted the edit seed and the save round-trip,
which is why it was held back.

Add one adapter that owns every direction of the conversion and route
all three surfaces through it. The reference panel converts the
canonical payload once and hands the converted pair to the range bar,
the status pill, and the editor seed. The editor seeds from the stored
value converted to the display unit, validates against guardrails
projected into that same unit, and inverts the typed number back to
canonical before the PUT.

Guardrails round inward, so the 30 kg floor projects to 66.2 lb rather
than 66.1 lb, which would have inverted below the floor the server
enforces. Absolute values carry the affine offset, differences take the
factor alone. Input step comes from the transform's own precision.

Storage is untouched: thresholds stay canonical SI, and for a metric
account the adapter is the exact identity in both directions, so a
stored value is unchanged bit for bit.
2026-07-25 03:24:10 +02:00
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00
Marc-André Bombeck c1c3aa9754 fix(settings): resync threshold rows after a reset
Each MetricRow is keyed on the stable metric name, so a per-row or
reset-all mutation that lands a new override (typically null) never
re-ran the useState initializers — the switch, min/max fields, and
override mode kept showing the just-cleared values while the
"Überschrieben" badge (driven straight off the prop) vanished, a
contradictory row until a reload. Re-derive an identity from the
override prop and resync local state during render when it changes;
typing in the fields never touches that identity, so this never
fights a live edit.
2026-07-18 03:52:32 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck d9393a2148 fix(settings): raise primary controls to the 44px touch floor
Apply the canonical min-h-11 sm:min-h-9 (and min-w-11 for icon-only
controls) to the primary Settings controls that sat at 32-40 px on
phones: the AI fallback-chain reorder/remove/save/add icons, provider
form save/test/connect actions, coach-memory forget, avatar replace and
remove, section save/cancel/reset CTAs, the export and import action
rows, the About external links, and the thresholds editor row. Collapse
the injection-sites checkbox grid to one column on phones and stack the
password-change grid below lg.
2026-06-19 21:27:05 +02:00
Marc-André Bombeck e31f9690e2 refactor(settings): compose single-card sections from SettingsCard
Swap the hand-rolled bg-card border-border rounded-xl border p-4 sm:p-6
container for SettingsCard across the single-card sections (integration
cards, notification channel cards, coach prefs, cycle, modules, mood
reminder, thresholds, dashboard-layout, about-me, low-stock, and the
health-record export panel). Drops the sm: padding split and the
primitive bypass; landmark sections keep their element via as="section".
2026-06-19 21:14:57 +02:00
Marc-André Bombeck 40f7196c20 feat(medications,coach,admin,perf): era corrections, a truthful weekly status and one card language everywhere
The dashboard snapshot fires the moment login succeeds instead of
waiting for the page chunk (start ~670 to ~200 ms throttled). Guided
follow-ups are personalised from the user's snapshot and an answer gets
the coach's streamed reaction before the next question. Archived
schedule eras can be corrected through a superseding revision that
keeps the original as an audit trail, era writes serialise per
medication, and duplicate dose times collapse in validation. A rolling
medication whose next dose is tomorrow can no longer read as heavily
overdue — the card status derives strictly from the served next-due —
and the adherence bars keep a fixed geometry in every state. The
server-wide assistant key gains its admin card, the one duplicated
coach preference collapses onto a single owner, twenty-seven admin and
settings card headers move onto the shared pattern with sixty
containers normalised, and the remaining hydration warnings are gone.
2026-06-11 13:57:15 +02:00
Marc-André Bombeck 3b480b9885 perf(i18n,shell): ship one locale and stop blocking the first paint
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00
Marc-André Bombeck 04765ea8ac style: one spacing, title and skeleton language across the app
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
2026-06-10 16:54:15 +02:00
Marc-André BombeckandClaude Opus 4.8 038be6ac57 chore(release): v1.12.11 — unify settings notice colours and callout style
Route every Settings callout, badge and status note through the semantic
colour tokens (success / warning / info / primary) instead of raw palette
classes, so the same meaning reads identically across cards and stays
legible in light mode. Add a single SettingsInfoTile primitive — icon +
tone + text on one fixed surface — and adopt it for the threshold-override
caution note, replacing a bespoke box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:42:18 +02:00
Marc-André Bombeck 9b6e237f5f chore(release): v1.4.42 — Knip enforcing, queryKey factory closed, iOS Workouts dedup, Withings off-response classification
Six touch-disjoint waves landed on develop before this release:

- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
  35 unused exports + 52 unused types triaged to 0/0; shadcn surface
  + zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
  `/api/dashboard/widgets` 422 now returns every Zod issue + worker
  audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
  migrated to the queryKeys factory; 9 new factory entries; ESLint
  rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
  sites (seven status helpers + bucket-series + bp-in-target);
  Suspense double-comment consolidated; doctor-report sanitiser regex
  rewritten to escape-form so diffs are reviewable; pr-detection-
  worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
  dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
  tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
  (transient/reauth/persistent) + WithingsApiError typed throw + the
  `pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
  silent-disable gap the wave was conceived to close.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.

pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
2026-05-21 19:33:07 +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 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é BombeckandClaude Opus 4.7 6b35cad079 refactor(v1.4.19): apply simplify-review safe suggestions
Mechanical clean-up sweep across five hot spots flagged by the simplify
audit:

- Lift the 11-key auth.* audit-action label map out of the two admin
  components that rebuilt it on every render. The shared
  useAuthActionLabels() hook in _shared.tsx is now the single home for
  new entries (login-overview + recent-audit-preview consume it).
- Drop the dead chipClass branches in IntegrationStatusPill: only the
  connected state actually paints a Dracula-green chip; error and
  disconnected fall back to the badge variant.
- Add thin DateInput / DateTimeInput wrappers under components/ui that
  bake in the lang={locale} hint so 14 callsites across 9 files no
  longer have to remember the prop. A future date input cannot
  silently regress.
- Remove the leading icon-only flex wrappers left behind when the
  duplicate card titles were dropped on /admin/feedback,
  /admin/danger-zone, and the thresholds editor.
- Strip the five "v1.4.19 X removed because…" narration comments from
  insights/page.tsx and the dashboard. git blame already answers what
  changed and the comments aged into history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:24:35 +02:00
Marc-André BombeckandClaude Opus 4.7 1f4d6a28a5 fix(settings): unify threshold page naming
The thresholds settings route was carrying three different
names: nav label "Personal targets", page header "Personal
targets", inner card header "Personal thresholds". The URL
slug is `/settings/thresholds`, so settled on "Personal
thresholds" everywhere in EN; DE keeps the established
"Persönliche Zielwerte". While here, dropped the duplicate
title + subtitle inside the editor card — the page-level
header already provides them and the second copy was just
visual noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:43:04 +02:00
Marc-André BombeckandClaude Opus 4.7 a432cb2e26 refactor(settings): consistent naming + i18n key namespace
Phase B6 commit 2 of 4.

* `thresholds-settings-section.tsx` (route wrapper) renamed to
  `thresholds-section.tsx` so the filename + default export match
  the slug — every other section in `src/components/settings/`
  follows the `<slug>-section.tsx` `<SlugSection>` convention.
* The historic inner editor at `thresholds-section.tsx`
  (`<ThresholdsSection>`) is moved out to
  `thresholds-editor-section.tsx` (`<ThresholdsEditorSection>`)
  so the names stop clashing.
* Rewrites three muddy section descriptions in EN + DE:
  - account: lists every concern the section actually covers
  - api: drops the misleading "headless clients" / "Drittanbieter"
    framing for "your own scripts and apps"
  - advanced: post-B7 there's no Import surface here — describes
    the danger zone honestly.

No user-facing setting removed, no slug renamed, no i18n key removed.
Settings smoke tests still green; the route mount goes through
`<ThresholdsSection>` exactly as before, just from the renamed file.

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