Commit Graph
1714 Commits
Author SHA1 Message Date
Marc-André Bombeck b3d2753897 merge(import): CSV measurement import, backdate hint, JSON-import dedup + instant bounds 2026-06-14 23:29:24 +02:00
Marc-André Bombeck f0a14ac0b4 feat(measurements): surface that the entry timestamp is editable for backdating
The add form already defaults the timestamp to now and accepts any earlier
instant — the plausibility bound and the picker's max both block the future —
but many users never notice they can change it. Add a one-line hint under the
date/time field so logging a past reading is discoverable, and pin the
manual-create contract with a guard asserting the single POST schema strips an
externalId (the stats:-prefix overwrite stays batch-scoped).
2026-06-14 23:28:36 +02:00
Marc-André Bombeck fbdcb31202 feat(import): add CSV measurement import with per-row status and dry-run
A self-hoster migrating from a spreadsheet or a meter export overwhelmingly
has CSV, which neither the JSON nor the Apple Health import covers. Add
`POST /api/import/csv` reusing the existing write loop and rollup re-fold.

The documented header is `type,value,unit,measuredAt[,glucoseContext,notes,
externalId]`, order-independent. measuredAt must carry an explicit ISO-8601
offset — a row without one is skipped rather than guessed — and is bounded by
the shared entry-instant validator. Glucose accepts mmol/L (converted to
canonical mg/dL) and weight accepts lb (converted to kg); any other
non-canonical unit is skipped, never mis-stored. An optional externalId
column makes a re-upload idempotent; without it a re-upload duplicates.

The response carries a per-row status envelope so a 2000-row file with three
bad rows imports 1997 and names the three. `?dryRun=1` validates and previews
the outcome without writing. The parse, validation, and unit conversion live
in a pure module so they are unit-tested without a database.

Settings -> Export & Import gains a third CSV card mirroring the JSON and
Apple Health cards, with upload, paste, preview, and a downloadable example.
The new route is documented in the OpenAPI contract.
2026-06-14 23:28:22 +02:00
Marc-André Bombeck 1e28f9cfb8 merge(onboarding): persist goals to seed the dashboard, add anamnesis step 2026-06-14 23:22:42 +02:00
Marc-André Bombeck c8cd5aa226 feat(onboarding): add an optional anamnesis step on baseline
A collapsible "About your health" card on the baseline step captures
the genuinely-new baseline the app can act on — chronic conditions and
allergies — without re-asking the height/sex/birthdate already gathered
above. Both write through the existing encrypted self-context path
(PUT /api/coach/about-me), so there is no new model and no new
contract; the form preserves any existing free-text about-me on save
and only sends a field the user actually typed.

The card is collapsed by default with a clear optional framing, so the
step stays clear and the progress bar keeps its five dots; every field
is editable later in Settings. Copy is warm and medically grounded —
context, not a clinical form — with keys across all six locales. Also
re-truths the goals body copy now that the selection genuinely surfaces
on the dashboard.
2026-06-14 23:21:58 +02:00
Marc-André Bombeck 5f601426a9 feat(onboarding): persist goal selection and seed the dashboard
Wire the goals step to actually keep its promise. The picker now posts
the chosen slugs on the step-2 submit; the step endpoint validates them
against the closed slug enum (422 on an unknown slug), persists them to
User.onboardingGoals field-by-field, and on completion seeds
dashboardWidgetsJson from the selection — promoting the goal-mapped
tiles to the top and forcing them visible.

The seed is one-time, gated on a null dashboardWidgetsJson via the
updateMany WHERE clause, so a user who already arranged tiles is never
clobbered. An empty or general-wellness-only selection leaves the
column untouched (default layout). The resulting layout is what both
the web dashboard and the iOS widgets contract read — no client
recompute. Skip posts an explicit empty array (deliberate no
preference).
2026-06-14 23:21:48 +02:00
Marc-André Bombeck 38c7604ce4 feat(integrations): per-user Polar and Oura OAuth credentials
Polar and Oura resolved their OAuth client id/secret from the server
environment only, forcing every user on a deploy to share one app. Both
now read per-user BYO keys first, falling back to the shared env app when
unset, mirroring the WHOOP and Fitbit model.

- Add DB-first-then-env resolvers in src/lib/{polar,oura}/credentials.ts,
  encrypting the client id/secret at rest on User.
- Repoint the connect, callback, status routes and the Oura refresh path
  to the user-scoped resolver.
- Add /api/{polar,oura}/credentials (GET/PUT/DELETE) with Zod-validated
  input and userId narrowed from the session.
- Render an optional 'your OAuth app credentials' form on the Polar and
  Oura cards; the status read exposes hasOwnCredentials for the saved
  placeholder. Existing env-only deploys keep working unchanged.
- Cover both resolvers (DB hit, env fallback, no creds) and the card form
  with unit tests; add the new strings across all six locales.
2026-06-14 23:19:47 +02:00
Marc-André Bombeck 3594b549d9 feat(sleep): place sleep-debt and rhythm cards side by side when both present
When both the sleep-debt and chronotype cards carry a settled readout, render
them two-up on large screens instead of stacked; they are compact and read
better paired. A lone data-bearing card — or either card in its learning /
partial state — keeps full width so it never leaves a half-width orphan. The
grid collapses to one column below the lg breakpoint and uses items-start so a
toggled-open chronotype card does not stretch its neighbour.
2026-06-14 23:02:37 +02:00
Marc-André Bombeck a198ce0958 fix(onboarding): show the real integrations and set honest expectations
Bring the source step up to date — present the six shipped integrations instead
of a single vendor and a stale coming-soon badge, and signpost the import path
for existing history. Stop promising that the goals and source choices configure
the dashboard, which they do not yet; frame them honestly as helping the user
decide what to track. Set the calibrating expectation on the done screen so empty
tiles read as learning, not broken, and lead forward with connect and log CTAs.
Count any connected integration toward the setup checklist, not Withings alone.
2026-06-14 21:26:41 +02:00
Marc-André Bombeck 1ddc8a0c47 Surface shipped data sources in onboarding and tell the truth
Rewrite the source step to list the live integrations (Apple Health,
WHOOP, Oura, Polar, Nightscout, Fitbit) alongside Withings and manual
entry, each linking to where it is connected; drop the stale Apple
Health coming-soon badge. Add an import affordance for existing history.

Reword the goals and source copy so it no longer promises automatic
dashboard or reminder configuration that does not happen yet. Add a
calibration line to the done screen setting the expectation that some
insights sharpen over the first weeks, and lead the success screen
forward with connect-a-device and log-first-measurement actions.

Generalize the dashboard checklist data-source item so any connected
integration satisfies it, not Withings alone.
2026-06-14 21:26:09 +02:00
Marc-André Bombeck bd9f03805c feat(insights): surface ring/strap HRV and rank wearable activity sources 2026-06-14 21:22:41 +02:00
Marc-André Bombeck 46f9dfa47f fix(integrations): make the new cards match the existing ones and link to their data
Give the Polar, Oura and Nightscout cards the same parked-state resume banner
and test-connection button the other integrations carry, distinct brand icons,
a connected-state link to where their data appears, and the correct status-key
invalidation so the pill refreshes after a disconnect. Fix the sleep empty-state
link that pointed at a non-existent settings slug.
2026-06-14 21:15:53 +02:00
Marc-André Bombeck 633a451c16 feat(integrations): match parked, test, and data-link treatment on new cards 2026-06-14 21:13:09 +02:00
Marc-André Bombeck 1b1713dd12 fix(insights): align the sleep cards with the design system
Drop the bespoke vertical-rhythm override on the sleep-debt and chronotype
cards so they inherit the standard card spacing the glucose panel uses, give the
sleep-rhythm section a quiet error notice instead of an endless skeleton on a
failed read, and move the card accents onto the semantic info token.
2026-06-14 21:10:06 +02:00
Marc-André Bombeck 4807c3afd8 Align sleep rhythm cards with the house Card rhythm and states
Drop the compact gap/padding override on the sleep-debt and chronotype
cards so they inherit the standard Card rhythm, matching the glucose
panel. Switch the card icons to the semantic text-info token. Branch
SleepRhythmSection on a query error to a quiet inline notice instead of
an endless skeleton, keeping loading and settled states distinct.
2026-06-14 21:08:32 +02:00
Marc-André Bombeck 235e509333 fix(insights): point the briefing steps link at the dedicated page
Every other briefing destination links to its dedicated insight sub-page; the
step row alone pointed at the generic readings view. Route it to /insights/steps
so the briefing, the tab strip and the dashboard all open the same step page.
2026-06-14 21:05:37 +02:00
Marc-André Bombeck c268ace58c Derive glucose isSpotEstimate from reading density
Hard-coding isSpotEstimate: true mislabels a dense Nightscout CGM stream
(~288 readings/day) as a spot estimate and undersells the data in the
clinical panel and coach. Compute readings-per-day over the covered span
and treat the series as continuous below the spot caveat once it clears a
~hourly bar (24 readings/day); sparse spot fingersticks keep the caveat.
The panel only renders the spot-reading line when isSpotEstimate, and the
OpenAPI field relaxes from a literal-true to a boolean.
2026-06-14 20:54:32 +02:00
Marc-André Bombeck c5a7e06e35 feat(medications): warn on supply with reorder lead time in mind
Move the low-stock trigger from a bare runway threshold to the larger of that
threshold and the reorder lead time plus one dose interval, so a weekly
medication is flagged before its last dose rather than after — early enough to
reorder. The change only moves the alert earlier; no one's threshold shrinks.
Surface concrete dates (runs out on, reorder by) and a running-low vs last-dose
state through the existing badge and channel, never the card colour. A reorder
lead time is configurable globally with a per-medication override, and the glp1
card flag now runs the same runway decision as the daily check instead of a
stale dose count.
2026-06-14 20:40:21 +02:00
Marc-André Bombeck 08aa655be4 feat(medications): make low-stock alert reorder-lead-time aware
The low-stock alert fired when the projected runway dropped below the
user threshold (default 7 days). For a sparse cadence — a weekly
injection's 7-day floor is barely one dose-interval — the warning landed
with about one dose left, too late to reorder before the supply ran out.

Add an optional per-medication reorder lead time plus a user-level
default (0–60 days, default 10) and widen the alert trigger to
max(lowStockRunwayDays, leadDays + cadenceIntervalDays), so the warning
lands before the last dose for any cadence. Because the trigger is a
max() over the user floor it never shrinks anyone's current threshold; a
daily med with no lead keeps its prior boundary.

The push body and the card notice surface concrete dates — supply runs
out around <date>, reorder by <date> — with a last-dose informational
state and the days/units fallback when supply data is thin. Retire the
stale fixed 4-dose count threshold on the GLP-1 details endpoint and
route its card lowStock flag through the same runway evaluation so the
card and the daily cron agree. Surface the lead default in the low-stock
settings card with progressive disclosure; a single medication can
override it.

Additive schema: nullable Medication.reorderLeadDays (migration 0161, no
backfill) and notificationPrefs.medication.reorderLeadDays. OpenAPI
regenerated.
2026-06-14 20:39:06 +02:00
Marc-André Bombeck 4788c81689 merge: forward the v1.16.17 fixes into the v1.17.0 line
Carry the restored sleep timeline bar and the dashboard refresh-after-intake
fixes onto the release branch so the v1.17.0 image ships with them.
2026-06-14 20:38:23 +02:00
Marc-André Bombeck 59307760c5 fix(medications): refresh the dashboard the moment an intake is recorded
The intake write invalidates the dashboard snapshot, but the default active
refetch type only refetches mounted queries — and the snapshot is unmounted
while the user is on the medication card or detail page. On navigating back it
remounts under refetchOnMount:false, so the pre-write cache was served and the
due prompt lingered until a hard reload. Force the inactive snapshot to refetch
after every intake write so the dashboard clears as soon as the dose is taken.
2026-06-14 20:21:31 +02:00
Marc-André Bombeck 6a99f72384 fix(sleep): restore the last-night hypnogram timeline bar
The staged bar is gated on the night carrying more than one distinct stage
boundary. The guard read the segment END instants, so an ordinary multi-stage
night whose segments share a right edge — the shape a summary-derived writer
reconstructs to — collapsed to the breakdown footer with no bar. Gate on the
distinct START instants instead, so a night with timed stages keeps its
timeline and only a session recorded on a single instant falls back to the
breakdown. Covers the chart-vs-breakdown branch the test suite did not.
2026-06-14 20:21:19 +02:00
Marc-André Bombeck 88139fb981 feat(integrations): add Oura Cloud v2 OAuth sync
Connect an Oura account over the Cloud v2 API: an authorization-code OAuth
flow (shared app from OURA_CLIENT_ID / OURA_CLIENT_SECRET) on the same signed
state machinery as Polar. Unlike Polar, Oura issues refresh tokens; the sync
refreshes reactively on a 401, rotating and re-encrypting both tokens, and a
403 stays a hard reauth rather than a refresh.

An hourly poll-cohort job maps daily readiness to RECOVERY_SCORE, sleep to
per-stage durations plus efficiency / HRV / resting heart rate / respiratory
rate, and daily activity to steps and active energy — all tagged source OURA,
idempotent on (userId, type, source, externalId). Readiness flows through the
canonical recovery ladder above the computed proxy. Settings gains a connect /
disconnect card. Tokens are never logged or returned; every egress goes
through safeFetch.
2026-06-14 20:10:35 +02:00
Marc-André Bombeck 31abd307fd feat(integrations): add Polar AccessLink OAuth sync
Connect a Polar account over AccessLink v3: an authorization-code OAuth
flow (shared app from POLAR_CLIENT_ID / POLAR_CLIENT_SECRET) backed by a
stateless HMAC-signed state token doubling as an httpOnly cookie for CSRF.
The callback verifies the signature, expiry, provider binding, and the
byte-exact cookie match, then encrypts the access token plus member id onto
the user row.

An hourly poll-cohort job maps Nightly Recharge recovery, sleep stages,
and daily activity to canonical measurements tagged source POLAR, idempotent
on (userId, type, source, externalId). The recovery band feeds RECOVERY_SCORE
through the canonical ladder, which now ranks native readiness above the
computed proxy. Settings gains a connect / disconnect card. Tokens are never
logged or returned; every egress goes through safeFetch.
2026-06-14 20:08:50 +02:00
Marc-André Bombeck e561f8d893 feat(glucose): add the clinical glucose panel
Surface a clinical panel beneath the blood-glucose chart: time-in-range on the
consensus bands, the glucose management indicator and estimated A1C, and the
coefficient of variation with its instability flag by default, and the J-index
and the low and high blood-glucose risk indices with a per-context breakdown
behind an advanced disclosure. Below the calibration threshold it shows a calm
learning state, and it always carries the spot-reading caveat. The analytics
read, the dashboard snapshot, the coach and the doctor report all consume one
computed result over a fixed window, so the panel, the assistant and the
printed report cannot disagree.
2026-06-14 19:21:25 +02:00
Marc-André Bombeck a970ba8ab0 feat(sleep): surface sleep debt and chronotype
Assemble a sleep-rhythm read on the canonical per-night reconstruction — the
same midpoints and asleep totals the sleep score uses — that reports a rolling
sleep debt against the age-based need and an MCTQ chronotype with its mid-sleep,
social jetlag and band. A shared assembler caps the chronotype window so the
dashboard and the dedicated read return identical values, and both withhold a
confident reading behind a learning state until enough nights accrue. The sleep
page gains a debt headline and a chronotype card with the deeper detail behind
an advanced disclosure; the sleep score is left untouched so debt reframes the
sufficiency gap without double-counting it.
2026-06-14 19:12:01 +02:00
Marc-André Bombeck 4b9f31f053 Add glucose clinical panel with advanced-index disclosure 2026-06-14 19:11:57 +02:00
Marc-André Bombeck d221dc4da0 feat(nightscout): pull CGM glucose from a self-hosted instance
Add a Nightscout integration that pulls sensor glucose readings from a user's
own Nightscout instance and stores them as immutable canonical mg/dL blood
glucose, giving the clinical glucose panel the continuous density it needs.
Egress runs through the safe-fetch wrapper with the public-host pin on by
default and off only behind an explicit private-network opt-in, so a home or
LAN instance works without weakening the floor for everyone else. Readings are
first-write-wins by Nightscout entry id, an hourly job keeps recent history in
sync, and a settings card carries the URL, token and the private-network toggle.
2026-06-14 19:10:47 +02:00
Marc-André Bombeck 1ce1c786b6 Cap chronotype window in the shared assembler
Make computeSleepRhythmFromNights own the chronotype look-back so the dashboard
summary (a year of nights) and GET /api/sleep/rhythm (six weeks) return the
same band, mid-sleep, and social-jetlag values rather than averaging different
samples. Derive the free/work day-type from the local wake-day digits so the
classification holds in far-east timezones, and read the shared sleep-rhythm
DTO type into the client hook instead of a hand-mirrored copy.
2026-06-14 19:10:44 +02:00
Marc-André Bombeck f5d0b2f696 feat(integrations): add Nightscout CGM glucose sync
Pull continuous glucose readings from a self-hosted Nightscout instance
and store them as immutable BLOOD_GLUCOSE mg/dL measurements
(source=NIGHTSCOUT), giving the glucose panel real CGM density.

- client over safeFetch: public-host pin by default, relaxed only on the
  per-user private-network opt-in; redirect pinned manual + timeout
- credentials encrypted at rest on User; URL is the connect marker
- sync maps SGV entries first-write-wins on (userId,type,source,externalId)
- connect/disconnect/status routes: apiHandler + requireAuth + rate limit;
  connect validates by a live test fetch with clear errors
- hourly poll-cohort job registered in the all-queues registry
- settings card with URL, token, and the private-network toggle
- i18n across all six locales
2026-06-14 19:06:56 +02:00
Marc-André Bombeck 3be7c3788b Serve glucose clinical panel from one engine to all surfaces 2026-06-14 19:03:34 +02:00
Marc-André Bombeck d1ba01548f Surface sleep-debt and chronotype on the sleep page
Compute cumulative sleep debt and the MCTQ chronotype server-side off the
canonical night reconstruction the Sleep Score reads, and render them on
the sleep page: a sleep-debt headline with a calm partial state under the
night threshold, and a chronotype card whose social-jetlag detail and
corrected mid-sleep sit behind an advanced disclosure. Both hold a learning
state on thin data and never assert a total or a band.

Serve the same values to the dashboard summary and a new GET /api/sleep/rhythm
read so the native client renders identical figures. Free vs work nights
default to weekend in the user's timezone until day-type tracking exists.
2026-06-14 19:03:11 +02:00
Marc-André Bombeck 5b67dc636d feat(cycle): sectioned log-day sheet with summary-badge disclosure 2026-06-14 18:57:38 +02:00
Marc-André Bombeck 9e1e285fe4 feat(mood): sectioned add sheet with quick-tag row and disclosure 2026-06-14 18:50:11 +02:00
Marc-André Bombeck 079be9d5af feat(ui): add collapsible SheetSection with summary badge slot 2026-06-14 18:44:51 +02:00
Marc-André Bombeck 9065092ba0 fix(insights): drop unhonored day-count from glucose median label 2026-06-14 17:48:18 +02:00
Marc-André Bombeck 555240cd28 fix(consent): close the receipt mint race, rate-limit, and share the back link
Mint the web AI-consent receipt inside a serializable transaction with an
in-transaction recheck, backed by a partial unique index over one active
receipt per user and kind, so two concurrent mounts can no longer leave two
active grants; a duplicate maps to a no-op mint. Supersede an active row
against the server clock rather than the client-supplied timestamp so a
backdated grant cannot invert the audit chain, and revoke through a single
atomic update. Rate-limit the consent endpoints per user. Extract the
hand-rolled sub-page back link into one component and adopt it across the
four pages that duplicated it.
2026-06-14 17:26:36 +02:00
Marc-André Bombeck e2f37bbe40 Extract shared BackLink and adopt on detail pages
The workout, values, score, and medication detail pages each hand-rolled
the same ghost Button + Link + ArrowLeft back-nav block. Fold it into a
single BackLink (href + label + optional data-slot) so the affordance
reads identically — same hit area, icon, and left-bleed alignment — on
every page and lives in one place. Each page passes its existing label
translation, so no new i18n keys are introduced and the markup stays
byte-identical.
2026-06-14 17:21:45 +02:00
Marc-André Bombeck 25944ed57a feat(glucose): read glucose in one unit across series, CSV, and detail page
Glucose is stored canonical mg/dL but the series DTO, CSV export, and detail
page emitted raw mg/dL while FHIR already converted to the user preference.

Convert at serialization through the single convertGlucose helper:
- series route sets unit from the user's glucoseUnit and converts each point
- CSV export converts BLOOD_GLUCOSE rows and emits the unit in the unit column
- the detail page + stat strip honour the preference via the chart value scale
- declare the trailing-90-day window in the glucose median label across locales

mmol/L-preference users now see the blood-glucose page, stat strip, iOS series,
and CSV in mmol/L; mg/dL users are unchanged.
2026-06-14 17:16:48 +02:00
Marc-André Bombeck a614ca78c6 feat(cycle): deepen phase-education copy with a second context line
Add a per-phase "context" line under cycle.phaseEducation.context.* that
expands the existing descriptive-only education with what someone might
notice — energy, sleep, basal temperature, mood — and a gentle framing of
normal variation. The original whatsHappening lines are kept verbatim; this
sits below them as a second, deeper paragraph in the same warm,
medically-grounded, non-prescriptive register.

Wire the new leaf through phase-copy.ts (PHASE_CONTEXT_KEY) and render it in
PhaseEducationCard, and add the copy across all six locales.
2026-06-14 15:42:58 +02:00
Marc-André Bombeck e832222402 feat(cycle): surface the cervix secondary-symptom choice in the UI
- Advanced cycle settings gains a "secondary fertility sign" selector
  (cervical mucus default | cervix observation). It sits below the priors
  in the advanced settings so it never forces itself on a casual user.
- The log-day sheet shows the cervix entry — position / firmness / opening
  chips — only when the user picked cervix observation; otherwise the
  cervical-mucus picker stays as before. Progressive disclosure: exactly
  one secondary sign is offered at a time so the sheet never overwhelms.
- Web client types, the cyclePrefs patch, and the day-log payload builders
  carry the new fields; copy added across all six locales.
2026-06-14 15:33:11 +02:00
Marc-André Bombeck 4bfe9f11d7 feat(cycle): exclude disturbed temperatures from the symptothermal engine
A fever, illness, alcohol or a late/short-sleep reading can fake or mask
a temperature rise. Add a per-day `temperatureExcluded` flag on
CycleDayLog (migration 0156): when set, the engine drops the reading from
the cover-line baseline AND the shift evaluation, so a spike neither
fabricates nor masks a rise (Sensiplan: the cover line is drawn over the
last six unbracketed values).

Wired end to end — schema + Prisma, the engine's `detectTempShift`
filter, the Zod input/patch schemas, the day-log upsert (partial merge +
change detection), the read DTO, the calendar/insights/coach selects, the
backup round-trip, and the OpenAPI contract (a new boolean on the day-log
and calendar-day DTOs for the iOS client). The log-day sheet gains a
"mark reading as disturbed" toggle that appears once a temperature is
entered, translated across all six locales.

Citation: de.wikipedia "Symptothermale Methode"; myNFP "Temperaturkurve
auswerten" — Störfaktor bracketing.
2026-06-14 15:04:25 +02:00
Marc-André Bombeck f5798e20d8 fix(cycle): gate the calendar grid to a learning state during cold start
The predictions panel suppressed its fertile-window and ovulation rows while
fewer than three cycles were observed, but the month calendar did not — so a
brand-new user who logged a single period saw the grid paint a confident
fertile-window shading, a predicted-ovulation dot, and full phase bands off a
population 28/14 prior at ~0.20 confidence. Calendar-based fertility estimation
is unreliable below several recorded cycles, so the grid was asserting days it
could not know.

Thread the same still-learning gate into the calendar build: while fewer than
three cycles are observed, suppress the fertile window, the ovulation dot, and
the phase band (the predicted-period bar stays, mirroring the panel). Surface a
top-level stillLearning flag on the calendar response so the client renders a
calm "learning your cycle" banner over the grid instead of a bare, falsely
confident calendar. The flag is additive and back-compatible; older clients
ignore it and degrade to the previous render.

Also pin the observed-cycle count to a single source — the engine's
post-exclusion value — so the response never carries two divergent counts.
2026-06-14 14:43:50 +02:00
Marc-André Bombeck 3e7aa49eb0 feat(validation): bound entry timestamps with a shared plausibility guard
Only medication intake bounded its capture instant — a hand-rolled POST or a
faulty import could backdate a measurement to epoch zero or forward-date a
mood entry into next year, and both sailed through validation.

Extract the intake bound into a shared `validateEntryInstant` helper: reject
any instant more than a five-minute clock-skew tolerance in the future, and
reject anything before a 1900 far-past floor. Callers can clamp the past
window tighter — medication intake keeps its five-year floor — and override
the past-bound message to preserve an established contract.

Apply it to the measurement create/edit schema (`measuredAt`), the mood
schema (`moodLoggedAt`), and the cycle period + day-log (`loggedAt`);
medication intake now delegates to the same helper. The capture forms gain a
matching `max` on the datetime picker so a future instant can't be selected
in the UI either. The wire shape is unchanged, so the OpenAPI contract holds.
2026-06-14 13:51:51 +02:00
Marc-André Bombeck e9138bb4f6 feat(blood-pressure): label the in-target window dynamically and gate thin data
The BD-Zielbereich tile narrated a static "· 90 T" scope even for a user
with only a few weeks of history or a handful of readings — a headline that
reads like a stable quarter-long average when it is really a thin sample.

Add a shared, reusable window-confidence helper that, given a window length
and the readings observed inside it, returns the EFFECTIVE label span (the
real calendar span capped at the window, so "· 23 T" until ~90 days of
history exist) and a sufficiency flag (false below five readings). The helper
is metric-agnostic so the glucose time-in-range / GMI and sleep tiles can
reuse it.

Wire it into the in-target headline: below the confidence floor the tile
shows "sammelt noch Daten" instead of a hard percentage, with the dynamic
span label; above it the percentage shows with the real span. The
Health-Score BP pillar reads the same sufficiency notion through the shared
input builder, so a thin-data user no longer gets a confident pillar grading
off a few readings — the all-time rate still rescues a deep-history account
with a quiet quarter. The 90-day pair count and effective span thread from
the analytics route and the dashboard snapshot through to the tile.
2026-06-14 13:43:41 +02:00
Marc-André BombeckandClaude Opus 4.8 f846b0681d fix(demo): hide the Coach FAB in demo mode
The Coach floating button was mounted unconditionally, so a demo
visitor could tap it and send a message. /api/insights/chat is not in
the proxy's demo-mutation allowlist, so the send returned a raw 403.
Demo is read-only by design, so the FAB has no job there — gate its
mount on !demoMode and pin the contract in the hoist test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:44 +02:00
Marc-André BombeckandClaude Opus 4.8 b7b160d4fa fix(headers): keep list-page header actions on one row on mobile
The medications header used flex-wrap, so its action group (take-all
+ customize wrench + Add) dropped to a second row below the title on
narrow viewports. Drop the wrap and let the title block shrink: title
div gets min-w-0, subtitle truncates, action group keeps shrink-0 —
the wrench and Add stay top-right inline with the heading.

The mood, measurements and dashboard headers shared the same latent
root cause (title div missing min-w-0, subtitle/greeting not
truncating, action group not shrink-0). Apply the same minimal guard
so none of them can wrap or compress the actions either.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:39 +02:00
Marc-André BombeckandClaude Opus 4.8 4111124f24 fix(nav): keep long More-menu tile labels inside their tile
The More hub grid tile label was a flex child without min-w-0, so
long compound labels (e.g. "Benachrichtigungen") painted past the
tile's rounded border instead of wrapping. Give the span min-w-0
and tightened leading so it wraps cleanly within the tile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:33 +02:00
Marc-André BombeckandClaude Opus 4.8 3ad1a8ab25 fix(insights): right-align the hero Coach action on mobile
The overview hero's inline "Ask the Coach" button sat in a
flex-wrap row with no justify-end, so it rendered left under the
hero copy. Pin it to the right edge of the hero's left column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:28 +02:00
Marc-André Bombeck 083410473c refactor(pwa): defer push subscription to the app-wide registration
The Web-Push opt-in registered its own `/sw.js` before subscribing. With
the app-wide registrar now installing the worker for every user, that
second `register` call is a redundant source of truth at the same scope.
Replace it with `navigator.serviceWorker.ready`, which resolves with the
active registration once the global worker is controlling the page.
2026-06-14 10:46:23 +02:00