Commit Graph
2249 Commits
Author SHA1 Message Date
Marc-André Bombeck 9bd46ff25d refactor(jobs): decompose reminder-worker into domain queue registrars
Split the 2143-LOC startReminderWorker boot file into five domain
registrars under src/lib/jobs/reminder/ (integration-sync, status,
rollup, reminders, maintenance), each owning its queue-name constants,
its allQueues membership, its schedule tuples, and its boss.work
bindings. A shared createAndSchedule helper provisions every queue
before scheduling it. The boot file is now a 115-LOC registration table
that composes the registrars and runs the boot one-shots + discovery
enqueues.

Every queue declaration, schedule, boss.work binding, and boot-discovery
enqueue is preserved verbatim — verified by diffing the old monolith
against the union of the registrars (66/66 queues, 66/66 bindings,
identical schedule tuples and boot enqueues). The diff caught one
withings-oauth-state-cleanup binding that was dropped mid-refactor and
restored it.

Update the 18 queue-registration guard tests to follow the wiring into
its registrar module so the dead-queue protection moves with the code
rather than weakening: each grep guard now reads its queue's registrar,
the stress-strain-retention guard unions the allQueues blocks across the
status + rollup registrars, and the intake-slot-dedup schedules regex
tracks the new module-level indentation.
2026-06-16 23:28:48 +02:00
Marc-André Bombeck a3217ff767 fix(security): rotation covers all encrypted columns + CI guard; danger-zone typed confirm; admin rate-limit 2026-06-16 23:06:13 +02:00
Marc-André Bombeck 88c4a8428f fix(admin): harden the Danger-Zone wipe to match the Backups Restore gate
The global data wipe auto-supplied confirm:'DELETE ALL' from a single-click
dialog, leaving the server's typed-token defence dead and making the most
destructive action a weaker gate than the less-destructive restore. Mirror
RestoreRowDialog: require typing 'DELETE ALL' into an Input that arms the
destructive button, reset on close.

Also attach an Idempotency-Key header to the wipe so a double-submit cannot
re-run the transaction, add the documented post-requireAdmin()
'admin-data-delete:<userId>' rate-limit bucket alongside the anonymous IP
throttle, and switch BackupsSection off the non-reactive formatDateTime
onto the locale-reactive useFormatters() hook its siblings use.
2026-06-16 23:05:11 +02:00
Marc-André Bombeck e23f57448a fix(crypto): wire every encrypted column into key rotation behind a registry guard
The rotation script omitted the four v1.18.1 clinical-spine note columns
(LabResult.noteEncrypted, Biomarker.contextEncrypted,
IllnessEpisode.noteEncrypted, IllnessDayLog.noteEncrypted) plus a longer
pre-existing tail (Nightscout/Polar/Oura credentials, the KVNR field, the
health-profile and cycle-day-log payloads, InsightNarrative content).
Rotation reported zero legacy rows while these still held old-key
ciphertext, so dropping the legacy key left them permanently
undecryptable (decrypt is fail-closed).

Introduce src/lib/crypto/encrypted-columns.ts as the canonical registry of
every reversibly-encrypted column, drive the rotation script from it, and
add a guard test that cross-checks the registry against every encrypted
column in schema.prisma and asserts the script references each one. A new
*Encrypted column not wired into rotation now fails CI by construction.
2026-06-16 23:05:03 +02:00
Marc-André Bombeck b28fa4d4a5 merge: suppress-during-illness Rest Mode + FHIR Condition + doctor-report 2026-06-16 22:27:13 +02:00
Marc-André Bombeck c59cf0b652 feat(illness): Rest Mode — annotate score/recovery/streaks during an active episode + FHIR/doctor-report
An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.

- rest-mode.ts: one server-authoritative resolver (module-gated, fail-soft)
  for "is the account in Rest Mode + which episodes back it". iOS mirrors the
  resolved context, never recomputes the suppression.
- health-score: /api/analytics attaches a value-free Rest Mode annotation;
  computeHealthScore stays pure (score never adjusted).
- recovery-score: computeRecoveryScore carries the Rest Mode context as of the
  scored day; the persisted blend is unchanged.
- coach-nudge: pause cadence-nudges during an active episode (new
  skippedDuringIllness counter, gate sits before the trigger search).
- achievements: bridgeFrozenStreakGaps freezes (does not break) every
  day-streak across illness days; never invents a streak, a non-illness gap
  still breaks.
- coincident-deviation: reframe a fired flag as illness-explained; vital
  numbers untouched. Surfaced as a neutral line on the insights card.
- coach snapshot: illness history + active illness as load-bearing context so
  the coach knows about Rest Mode (no decrypted note reaches the prompt).
- FHIR: illness episodes map to Condition (generic disease root, label on
  code.text, patient-reported/unconfirmed) + a bounding Encounter.
- doctor-report: module-gated illness section in the aggregator + PDF table
  (retrospective, neutral palette); FHIR Condition section in the bundle.

i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
2026-06-16 22:25:50 +02:00
Marc-André Bombeck 6a74025b0a merge: illness correlation + recovery-gap engine with reliability battery 2026-06-16 22:17:29 +02:00
Marc-André Bombeck 745fa28c8b feat(illness): correlation + recovery-gap engine
Add the retrospective correlation engine for illness episodes: a pre-onset
anomaly scan (deviation in robust SDs from the user's own median±MAD
baseline), the nadir / what-dropped across the active span, and the
recovery-gap (physiological return-to-baseline vs the felt-better marker).
CHRONIC_ONGOING is excluded from the gap. Every derived number is gated by
the shared coverage/confidence model — a thin signal asserts nothing rather
than showing a wrong figure.

The baseline window ends strictly before the pre-onset lookback, so an
episode's own days can never contaminate the baseline it is measured against.
Reads are DAY-native (rollup tier with a live-SQL fallback), reusing the
baseline engine's read path so the rollup-vs-live parity carries over.

Surfaces:
- GET /api/illness/episodes/{id}/correlation — per-episode findings (gated
  Derived wire shape; red-flag escalation for sustained low SpO2 / fever).
- GET /api/illness/insights — cross-episode retrospective ("sick N times ·
  typical recovery gap X days"), with the typical-gap withheld below the
  min-sample floor. Retrospective only — never a predictor or diagnoser.

Reliability battery: golden-fixture recovery-gap math, rollup-vs-live parity,
baseline-contamination guard, thin-data / coverage-gate, red-flag escalation.
Born-gated + owner-scoped; server-authoritative DTO (iOS renders). i18n ×6.
2026-06-16 22:16:19 +02:00
Marc-André Bombeck 800e799d48 merge: extract worker lifecycle handlers + register illness CRUD in OpenAPI 2026-06-16 21:57:58 +02:00
Marc-André Bombeck c1d804809d docs(openapi): publish the illness episode + day-log CRUD contract
Register /api/illness/episodes (list + create), the {id} resource
(read + edit + soft-delete), the {id}/resolve verb, and the
{id}/day-logs upsert in the OpenAPI route table, and add the Illness
tag. Request bodies + queries reuse the runtime Zod schemas from
validations/illness so the wire contract stays single-source; the
response shapes mirror the server-authoritative episode + day-log DTOs.
Regenerated docs/api/openapi.yaml; openapi:check is in sync.
2026-06-16 21:56:54 +02:00
Marc-André Bombeck 042510d1d4 merge: flatten the dashboard hero to match the chart-card design 2026-06-16 21:55:34 +02:00
Marc-André Bombeck 4a10505500 merge: coach card redesign — wider rail, symmetric dividers, FAB inset 2026-06-16 21:54:58 +02:00
Marc-André Bombeck 651aa65c28 fix(dashboard): flatten the hero band to match the tile + chart surfaces
The daily-verdict band wore a gradient surface and a health-score ring
with a drop-shadow bloom, a green-band breathing pulse, a conic sheen,
and a sweep entrance. It read louder than the chart cards and tiles it
sits among.

Drop the gradient: the band now wears the same bg-card + border + radius
+ padding as <TrendCard>, and the dose chip sits on a neutral muted
surface (no card tint, blur, or shadow). Add a flat treatment to
<ScoreRing> that suppresses the bloom, pulse, sheen, and entrance sweep,
and pass it on the hero ring only. The wellness strip and anatomy view
keep the full reveal. Reduced-motion handling is unchanged.

Information is untouched — only the visual treatment changes.
2026-06-16 21:54:25 +02:00
Marc-André Bombeck e51fb49b89 refactor(coach): rework the coach card layout for balance and breathing room
Rebuild the coach conversation surface so it uses the full width without
feeling cramped:

- Widen the inline history rail (260px -> 300px on lg, 340px on xl) and
  bump rail typography + padding off the cramped 10/11px scale so the
  conversation list and source toggles read at a comfortable size.
- Align the dividers across columns: the header, the history-rail
  heading, and the centre rail-tray strip now share one fixed h-14 band
  with identical borders, so the lines land on the same row instead of
  the restless two-height split.
- Redesign the header leading cluster: the surface-toggle sits in its
  own group behind a hairline divider rather than crowding the avatar,
  which steps up to size-9.
- Give the full-page coach card more vertical room and a soft shadow so
  it fills the standalone route instead of leaving a wasted band below.
- Even the coach FAB corner inset so the right gap matches the bottom
  gap at each breakpoint.
2026-06-16 21:54:08 +02:00
Marc-André Bombeck 0338f18b50 refactor(worker): extract reminder-worker boot lifecycle into a focused module
Move the SIGTERM/SIGINT graceful-shutdown binding and the three
fire-and-forget boot maintenance passes (legacy mood-log secret
rotation, orphan ImportJob reconcile, integration-status null probe)
out of startReminderWorker into src/lib/jobs/reminder/worker-lifecycle.ts.

Behaviour-preserving: the relocated logic is unchanged, and none of the
pg-boss queue declarations, the allQueues array, the schedules table,
the boss.work bindings, or the boot-discovery enqueues moved. Those stay
co-located in reminder-worker.ts so the source-text registration guards
(the dead-queue catch) keep verifying every queue is registered and
bound. The full jobs guard suite stays green.
2026-06-16 21:53:16 +02:00
Marc-André Bombeck 8447ad90cc merge: settings IA round 2 — top-text removal, integration split, medications toggleable, coach activate-flip, gesundheitsakte de-hero 2026-06-16 21:50:27 +02:00
Marc-André Bombeck 9b191a208d feat(settings): round-2 information-architecture rework
Apply the v1.18.0 walkthrough notes across the settings surface:

- Drop the explanatory top-text block from every settings section so
  content starts at one consistent vertical height (the sr-only heading
  stays for the accessibility tree).
- Move "Restart onboarding tour" out of Account into Advanced.
- Module hub: drop the explanatory line above the first card.
- Split the Integrations page into three left-side entries — the
  Connections content is now the Integrationen page itself, with
  Channels and Sources (source weighting) as their own entries. Drop the
  /settings/sources redirect (it is a real route again); Channels is a
  new entry.
- Notifications: drop the top text, the channels / inbox cross-links,
  and the embedded Vorsorge editor (it has its own page). Keep the mood,
  low-stock, and proactive-coach reminder cards.
- Rename the Layout hub entry to "Layout".
- Flip the Coach toggle to "Activate Coach" (default on, opt-out) over
  the same single disableCoach column — one source of truth.
- Move the AI "About me" context into Account → Profil, before
  cycle tracking.
- Gesundheitsakte: replace the gradient hero with a normal card.

i18n across all six locales for the renamed / added labels; settings
tests updated for the new structure.
2026-06-16 21:47:00 +02:00
Marc-André Bombeck a7b2b61b99 merge: cycle log-day sheet redesign — phase header, field tooltips, goal-aware fertility 2026-06-16 21:42:29 +02:00
Marc-André Bombeck cdf7fa2d41 merge: admin consolidation — unified coach section, backups layout, danger-zone parity 2026-06-16 21:42:29 +02:00
Marc-André Bombeck 81e176bdb9 merge: standardize insight metric pages — canonical recovery page, per-signal charts + assessments 2026-06-16 21:42:23 +02:00
Marc-André Bombeck bc2ce702f1 fix(insights): rebuild the recovery page onto the canonical metric structure
The recovery page rendered a grid of compact device-score tiles plus a
redundant cross-link to the recovery-score anatomy at the top. It now
follows the same structure as every other metric page (blood pressure,
pulse, the HealthKit scaffold): each present device-native signal renders
a block carrying a short explainer, the max / median / mean stat strip,
the shared chart with the identical 7 / 30 / 90 / All toggle, and a
per-chart assessment. Multiple charts now carry multiple matching texts.

Register the seven device-native recovery / strain signals (ANS charge,
day / workout strain, cardio load, whole-day average / max heart rate,
energy expenditure) in the generic metric-status registry so each chart
gets a generated assessment, gate them on the recovery module, and give
each its own chart-overlay slot. Drop the duplicate top score-link block
(this surface is reached from the overview already).

Regenerate the OpenAPI contract for the widened metric-status enum.
2026-06-16 21:35:09 +02:00
Marc-André Bombeck f3388ceb70 feat(cycle): redesign the log-day sheet — phase header, field info, wider dialog
Anchor the capture surface to the ring and lower its cognitive load:

- A hue-tinted phase-context strip (day-of-cycle + phase) at the top of the
  sheet, on the wellness-tile surface, honesty-gated the same way the phase-
  education card is — no phase claim until prediction is on, not raw-chart,
  and at least three cycles observed; otherwise a neutral day header.
- Inline field explainers (FieldInfo) on the clinical fields: BBT, cervical
  mucus, ovulation test, spotting, pregnancy/progesterone test, intercourse,
  protection — factual, descriptive-only, reachable by keyboard and screen
  reader.
- BBT is now the shadcn Input rather than a bare element, with a morning-
  timing hint, so it matches every other control.
- The symptothermal section (Temperature & ovulation) auto-expands for
  fertility goals and for an edited day that already holds a fertility sign,
  so a pre-filled value is never hidden behind a collapsed section.
- Desktop dialog widened to lg so the form breathes.

The payload builders and the form-hydration reconciliation are unchanged, so
the wire contract and their unit tests are untouched.
2026-06-16 21:32:58 +02:00
Marc-André Bombeck eb40f9b46f feat(cycle): field-explainer tooltip + log-sheet i18n scaffold
Add a cycle-scoped FieldInfo affordance (a focusable, SR-reachable info
button with a 44 px touch target, mirroring the mood-explainer precedent)
and the i18n keys the redesigned log-day sheet needs: a cycle.fieldInfo.*
namespace of descriptive-only explainers (BBT, cervical mucus, ovulation
test, spotting, pregnancy/progesterone test, intercourse, protection) plus
cycle.sheet.phaseDayHeader and cycle.sheet.bbtHint, across all six locales.
2026-06-16 21:32:48 +02:00
Marc-André Bombeck 513287d732 feat(admin): hide the Replay-tour card on the admin About page
The onboarding tour is a user-facing affordance with no place in the
admin console. Add a hideTourReplay prop to AboutSection (default off, so
Settings -> About is untouched) and pass it from the admin renderer.
2026-06-16 21:32:23 +02:00
Marc-André Bombeck 804e8f713a fix(admin): align the Danger Zone with the shared card layout
The Danger Zone rendered a lone icon above a left-floating label with
the delete button dropped below, unlike every other admin section. Adopt
the shared SettingsCardHeader shape — icon, title, description, and the
destructive action in the status slot — so the structure matches the
rest of the console while the tint and warning icon keep it flagged as
destructive.
2026-06-16 21:32:18 +02:00
Marc-André Bombeck 1e7b701fa6 fix(admin): give the Backups header room to breathe
The snapshot-count badge now leads (numbers first) and the explainer
plus docs link move into the card header's description slot, which
carries proper spacing and left-aligns under the title. Previously the
description sat in a separate paragraph crammed directly beneath the
"Backup now" button.
2026-06-16 21:32:12 +02:00
Marc-André Bombeck 3a29387393 refactor(admin): consolidate assistant + feedback-quality into one Coach section
The ai-quality, assistant, and coach-feedback admin sections were three
separate navigation entries for one coherent coach-quality concern. Fold
them into a single "Coach" section that stacks the assistant surface
toggles, the server-key + module-availability operator config, and both
feedback-quality tables (Coach + Insights). Drop the three old slugs and
their section copy; add admin.section.coach.* across all six locales.
2026-06-16 21:32:06 +02:00
Marc-André Bombeck 559366e308 feat(settings): make medications a toggleable module
Medications graduated from the always-on core set (weight / blood
pressure / pulse stay core) to an opt-out module. The surface gates
off the resolved module map — the left-nav entry, the dashboard
medication widget, and the dedicated Medikamente settings entry hide
when the account turns the module off. The medication data routes stay
exempt from the gate (same data-layer reasoning as mood / labs): an
importer, a sync, and the user's ability to clean up entries keep
working while the surface is hidden, and re-enabling finds the
schedule and intake history intact.

Wire the module-route gate inventory, the dashboard widget/summary
maps, the PATCH allowlist, and the nav model; regenerate the OpenAPI
contract for the new module key.
2026-06-16 21:31:54 +02:00
Marc-André Bombeck 930c4a75dd chore(sw): sync service-worker cache version to 1.18.1 2026-06-16 21:20:09 +02:00
Marc-André Bombeck ba8804cce0 merge: coach-cadence suggestions — SUGGEST-REMINDER sentinel, suggestion SSE frame, one-tap reminder 2026-06-16 21:19:36 +02:00
Marc-André Bombeck 12f4bc24e8 feat(coach): evidence-based cadence suggestions with one-tap in-chat reminder
The Coach can append a ---SUGGEST-REMINDER--- sentinel to propose a
measurement cadence (weight daily, the ESH/AHA 7-2-2 home-BP protocol,
structured glucose). The server resolves the cadence from a closed catalog
and emits an additive 'suggestion' SSE frame the chat renders as a one-tap
action card; accepting it creates a MeasurementReminder with origin COACH
through the same engine the Vorsorge surface uses (server-authoritative
schedule, finite course window self-expires via endsOn).

Non-naggy by construction: a frequency-cap cooldown, dismissal memory, a
master opt-out under Coach settings, and an explicit 'I measure enough'
stop path, all in the coach-prefs blob. Triple dedup at suggest, render,
and create so a cadence is never double-suggested or double-created, and
suggestions for a disabled module never surface. RHR/HRV stay passive --
the Coach surfaces the baseline, it never nudges to 'go measure' them.

The reminder DTO now echoes origin and endsOn for the iOS client.
2026-06-16 21:17:51 +02:00
Marc-André Bombeck 6ee0060c8a merge: illness/condition journal P2 MVP — born-gated module, episode CRUD, symptom timeline 2026-06-16 21:17:09 +02:00
Marc-André Bombeck cc32e2adf7 merge: Labs overhaul — biomarker catalog, structured entry, dashboard-style chart, edit/undo, backfill
# Conflicts:
#	src/app/api/labs/route.ts
2026-06-16 21:17:09 +02:00
Marc-André Bombeck b079431a8d feat(illness): add the condition-journal MVP behind a born-gated module
Introduce a retrospective illness/condition journal: episodes with a
lifecycle (acute / chronic-ongoing / recurring / flare) and a daily
symptom timeline forked structurally from the cycle log-day surface. The
journal is a record, not a medical device — no prediction or diagnosis UI.

- Register `illness` as a born-gated (opt-in / default-off) module. The
  gate resolver flips the disabled-allowlist default to off for born-gated
  keys, so the surface stays dark until the account opts in from the
  Modules hub. `isIllnessEnabled` + `requireIllnessEnabled` delegate to the
  module foundation (one source of truth) and re-stamp an illness errorCode.
- Episode CRUD under /api/illness: list / create / get / edit / resolve /
  delete, plus a per-episode-day day-log upsert on (episodeId, date). Every
  route wraps apiHandler, Zod-validates with returnAllZodIssues, narrows
  userId from auth, builds data field-by-field, and gates the module.
  Free-text notes encrypt at rest through the shared Bytes codec.
- Log-day sheet (cycle fork): ~8 Jackson/WURSS symptom chips 0-3 + a
  functional-impact group + fever + an encrypted note, one row per day.
- Episode history page + a born-gated nav entry, neutral palette, status
  via a neutral Badge, retrospective-only copy. i18n across all six locales.
- Cover the new routes in the module-route-gate inventory; unit tests for
  the day-log upsert, the gate enforcement, and the born-gated resolver.

The /api/auth/me module map now projects the `illness` flag (openapi.yaml
regenerated).
2026-06-16 21:09:35 +02:00
Marc-André Bombeck 90e0f2191c feat(labs): OpenAPI contract, i18n, and module-gate inventory
Add the Biomarker + restore routes to the OpenAPI registry and carry
biomarkerId on the LabResult schema (additive; iOS resolves the unit +
range from the linked marker, never recomputes). Regenerate the YAML.
Add the labs.* / labs.catalog.* keys across en/de/es/fr/it/pl. List the
biomarker catalog tree + restore route on the module-gate inventory's
data-layer exemptions (the module gates the surfaces, not the row store).
2026-06-16 21:06:16 +02:00
Marc-André Bombeck 04c95e5ba1 feat(labs): structured entry, per-biomarker chart, and edit/delete UI
Rework the Labs surface to match the rest of the app:

- Add a biomarker manager on the Labs page (define/edit/delete markers,
  seed from a common panel) — the catalog lives with Labs, not Settings.
- Replace the free-text add form with a structured one: pick a biomarker,
  enter the value against its known unit/range; '+ define new' inline.
- New /labs/[biomarkerId] detail page with a proper dashboard-style chart
  (Recharts line + shaded reference band + dashed bounds, range tabs,
  tooltip) replacing the 72x20 sparkline, plus the reading history with
  full edit/amend/delete and a delete-Undo toast.
- List cards group by linked marker and deep-link to the detail chart.

The reference band uses a muted token fill, never an alarm colour; the
neutral ReferenceRangeBadge is reused for the in/out-of-range verdict.
2026-06-16 21:06:08 +02:00
Marc-André Bombeck e4722fb9a1 feat(labs): backfill job linking legacy readings to the catalog
Add a one-shot pg-boss job that heals the per-row free-text divergence:
for each user it groups un-linked live readings by lower(analyte),
mints (or reuses) one Biomarker per group from the most-recent report's
spelling/unit/range, and stamps biomarkerId on every row. Registered in
allQueues with a boot-time discovery enqueue; idempotent across reboots
(a completed pass drops the user from the discovery set).
2026-06-16 21:05:58 +02:00
Marc-André Bombeck e9037a0d92 feat(labs): biomarker catalog with server-resolved reference ranges
Add a user-scoped Biomarker catalog so a marker is defined once — name,
unit, lower/upper bound, optional encrypted context, panel — and every
later reading just links it. New /api/biomarkers CRUD plus a structured
/api/labs create path: a biomarkerId resolves the unit + reference range
server-side, so the list and detail DTOs carry the resolved values and
the client never recomputes the range. Lab reads include the linked
marker and fall back to the legacy free-text fields for unlinked rows.

Add a lab-result restore endpoint for the delete-Undo affordance, a
shared serialiser, a static common-panel seed list, and tests for
catalog validation, structured-entry resolution, and the context codec.
2026-06-16 21:05:51 +02:00
Marc-André Bombeck b6796eee96 merge: one activity-aware reminder engine (Vorsorge + eventful satisfaction + lab-resolve + type completeness) 2026-06-16 21:00:11 +02:00
Marc-André Bombeck a0d864db8c feat(vorsorge): widen the linkable measurement set to the body-comp family
The reminder type allow-list exposed 7 of the full MeasurementType enum.
Widen it to the ~15 active-measurement set: vitals (weight, BP, pulse,
glucose, SpO2, temperature) plus the body-composition family a scale
produces (fat mass, fat-free mass, muscle, lean body mass, bone mass,
body water, visceral fat, BMI). One step on the scale satisfies every
linked reminder at once. Passive scores, cumulative activity, and device
events stay excluded — nudging someone to 'go measure' a passive sample
makes no sense.

App-layer only: the column is the full enum, so no migration. Group the
dropdown (Vitals / Body composition) and add the labels across all six
locales.
2026-06-16 20:59:06 +02:00
Marc-André Bombeck 7b57c1d3d8 feat(vorsorge): resolve reminders on ingest, not only on the cron
Add a reminder-satisfy pg-boss queue (mirrors pr-detection) that every
measurement and lab ingest path enqueues — manual create, batch, and the
Withings / Polar / Oura / Nightscout / Fitbit / WHOOP syncs. A reminder
now auto-resolves the moment a matching reading lands instead of waiting
up to 15 minutes. The 15-minute cron stays as the idempotent safety net;
the forward-only guard lets the eventful hook and the cron converge
without double-stamping.

The worker honours the module toggle (a reminder for a disabled module
produces no engine activity) and runs the same satisfaction primitives
the cron uses.
2026-06-16 20:58:56 +02:00
Marc-André Bombeck f92247760c feat(vorsorge): one satisfaction primitive + lab auto-resolve
Extract a single satisfyReminder() that every path uses to mark a
reminder's cadence fulfilled: stamp lastSatisfiedAt (forward-only) and
recompute the server-authoritative nextDueAt. The cron auto-resolve and
the manual satisfy route now route through it instead of duplicating the
reschedule.

Add findSatisfyingEvent(): a typed reminder resolves from a matching
Measurement, a free-text reminder from any LabResult. This closes the
Lab<->Vorsorge loop — an annual-blood-panel reminder is now satisfied
when a lab result lands, not only on a manual check-off.
2026-06-16 20:58:46 +02:00
Marc-André Bombeck bee7295b19 merge: v1.18.1 data foundation (reminder engine, biomarker catalog, illness journal) 2026-06-16 20:36:22 +02:00
Marc-André Bombeck 016eb12b42 feat(schema): add the v1.18.1 data foundation
Three additive migrations the v1.18.1 feature surfaces build on:
reminder engine fields (endsOn + origin, so coach-cadence and preventive
care produce into one reminder table), a user-scoped biomarker catalog
(name, unit, bounds, optional encrypted context, panel) with a nullable
LabResult.biomarkerId link, and the condition-journal model (IllnessEpisode
plus a CycleDayLog-shaped symptom timeline). No code consumes these yet;
the surfaces ship gated behind the module system.
2026-06-16 20:36:13 +02:00
Marc-André Bombeck e1e3cb6364 chore(release): v1.18.1
Maintenance patch clearing two transitive security advisories. Bump the
package version, record the changelog entry, and regenerate the OpenAPI
version line. No functional changes.
2026-06-16 19:22:33 +02:00
Marc-André Bombeck 97ddf2da79 build(deps): override hono and picomatch to clear transitive advisories
Pin hono to >=4.12.25 (GHSA-88fw-hqm2-52qc, CORS middleware reflects
any Origin) and picomatch to >=4.0.4 (CVE-2026-33671, ReDoS) through
pnpm overrides scoped to the vulnerable ranges. Both are build- and
dev-tooling transitives — hono via Prisma's dev tooling and the shadcn
CLI, picomatch via the build chain — and neither is on a runtime path:
the app is Next.js and sets no CORS headers. The prod audit drops from
one high to zero.
2026-06-16 19:22:25 +02:00
Marc-André Bombeck 7cf3d54402 Merge pull request #335 from MBombeck/release/v1.18.0
v1.18.0 — module enable/disable and the settings IA redesign
v1.18.0
2026-06-16 17:07:28 +02:00
Marc-André Bombeck b700ae0079 test(e2e): opt the seed account into cycle tracking
v1.18.0 resolves the cycle module through isCycleEnabled(gender,
CycleProfile): a NULL cycleTrackingEnabled derives from gender, so the
MALE-seeded e2e account read as cycle-OFF and /cycle redirected home —
the cycle-wheel tile never mounted. Seed a CycleProfile with
cycle_tracking_enabled = true so the cycle vertical renders for the
mobile smoke spec.
2026-06-16 17:00:05 +02:00
Marc-André Bombeck 3bbc0016d9 fix(cycle): restore the wellness-tile entrance so the ring renders
The wheel tile, phase-education card, and dashboard cycle tile each set
both data-revealed and .wellness-tile-rise on the same node. The keyframe
selector is [data-revealed="true"] .wellness-tile-rise (descendant
combinator), so it never matched a same-node pair — the tiles stayed at
the rise rule's opacity: 0, invisible but still boxed. Lift data-revealed
onto a contents wrapper (the documented pattern from the cycle insight
summary card) so the entrance plays and the signature ring paints.
2026-06-16 16:52:03 +02:00
Marc-André Bombeck f0614a8080 fix(a11y): raise inactive tab-trigger contrast on dark theme
The inactive tab text used --muted-foreground (#9aa3b3) on the muted
(#44475a) tab strip, reading 3.6:1 — below the WCAG AA 4.5:1 floor that
axe flags on /settings/integrations. Add a --tab-inactive token (#b3bccd
dark, 4.79:1 on the strip; light mirrors the existing 4.58:1 muted tone)
and point the dark-mode inactive trigger color at it. The active trigger
keeps --foreground.
2026-06-16 16:51:56 +02:00