Commit Graph
3317 Commits
Author SHA1 Message Date
Marc-André Bombeck 44bc91c3ea feat(mental-health): drop the trend chart from the check-in page
A rising score curve on the check-in page itself reads as a verdict at
the moment someone is taking stock of how they feel. The page keeps the
calm dated list of past check-ins (severity badge, score, the discreet
crisis-resource re-surface); the score trend stays available through the
PHQ9_SCORE / GAD7_SCORE measurement rows on the Insights / Measurements
surfaces. The per-card trend sheet and its chart component are removed,
along with the now-unused chart i18n keys in all six locales.
2026-07-05 14:47:58 +02:00
Marc-André Bombeck 53f84abfbb Merge pull request #405 from MBombeck/release/v1.27.4
Release v1.27.4 — Google Health sync fixed against the live API
v1.27.4
2026-07-05 14:18:34 +02:00
Marc-André Bombeck 13ddd371a9 chore(release): v1.27.4 — google health sync fixed against the live api 2026-07-05 14:08:34 +02:00
Marc-André Bombeck ff27faf3ef docs(google-health): rewrite the mapping notes for the verified v4 contract
The previous notes carried two wrong premises: that every launch type
supports dataPoints.list, and that the response payload is snake_case.
Document the three per-type encodings (kebab path, snake_case filter,
camelCase payload union), the int64-as-string fields, the per-shape
filter grammar, the dailyRollUp read for the cumulative activity types,
the corrected daily-* data-type ids, the sleep end-time filter, the
exercise metricsSummary payload, the AIP-193 error surfacing and the
structure probe.
2026-07-05 13:59:31 +02:00
Marc-André Bombeck f92e9fd840 feat(google-health): surface the API error body and add a structure probe
- On a non-2xx data-point response, extract the AIP-193 error envelope
  into a redacted STATUS: message detail (bearer tokens stripped, URL
  query strings dropped, 200-char cap) and thread it into the thrown
  GoogleHealthApiError plus the external-call annotation, so a
  field-grammar 400 is diagnosable from operator logs. The OAuth token
  endpoint keeps its flat invalid_grant handling.
- POST /api/integrations/google-health/test with {"probe":"structure"}
  now fetches one recent page/window per data type and returns only the
  payload STRUCTURE (field names + typeof leaves, never values) plus the
  per-type error verdict — paste-safe diagnostics for payload-shape
  drift. Same auth and rate limit as the plain connection check.
2026-07-05 13:59:24 +02:00
Marc-André Bombeck 295a6a8050 fix(google-health): parse the camelCase v4 payloads and correct the read methods
The v4 client read none of its documented response shapes: the DataPoint
value is a camelCase union keyed by the camelCase type name with camelCase
nested times, proto3 int64 fields arrive as JSON strings, and four fetchers
targeted wrong data types or illegal filter fields. Every HTTP-200 type
parsed zero measurements and four fetchers returned HTTP 400.

- Read every value/time path in camelCase; keep snake_case only inside the
  filter parameter. Pin path/filter/payload-key triples per data type.
- Coerce int64 JSON strings in every numeric extractor.
- weight.weightGrams -> kg, height.heightMeters -> cm,
  distance millimetres -> m, bodyFat.percentage, heartRate.beatsPerMinute.
- Move SpO2, HRV, respiratory rate and VO2 max to their daily-* data types
  (the bare sample types reject a .date filter).
- Move steps, distance, active energy and floors to POST :dailyRollUp with
  windowSizeDays 1 (list returns minute-grain buckets, not daily totals;
  floors has no list method), chunked at 90 civil days, day-keyed on
  civilStartTime.date at the UTC-midday anchor.
- Filter sleep on interval.end_time (the only legal sleep time field) and
  anchor sessions on interval.endTime; filter exercise on
  interval.civil_start_time with an offset-less civil bound in the user's
  zone; read exercise metrics from metricsSummary and take the workout id
  from the DataPoint name.
- Seed height from max(sampleTime) explicitly - list order is descending,
  so last-row-wins picked the oldest sample.
- Rewrite the mapper fixtures to the documented shapes.
2026-07-05 13:58:50 +02:00
Marc-André Bombeck 37449c74b0 Merge pull request #404 from MBombeck/release/v1.27.3
Release v1.27.3 — briefing resilience and insights readability
v1.27.3
2026-07-05 13:36:52 +02:00
Marc-André Bombeck f23486cd07 chore(release): v1.27.3 — briefing resilience and insights readability 2026-07-05 13:27:30 +02:00
Marc-André Bombeck d83220a4e0 feat(insights): assessment and prose readability pass
- assessments render in full; the three-line clamp and its show-more
  toggle are gone
- AI prose (assessment, briefing, explainers, usual-range note) reads
  in the regular foreground; muted stays reserved for meta lines
- the usual-range note becomes a proper card with the shared TileHeader
  (icon + title, labelled in all six locales)
- the metric-page heading + explainer indent to the cards' text edge
- the health-score card drops its Ask-the-Coach corner button
2026-07-05 13:27:30 +02:00
Marc-André Bombeck 8579705fbf fix(insights): keep the briefing on a transport-failed grounding retry
When the corrective grounding retry dies on transport (provider outage,
timeout) the model never got its chance to self-correct — stripping the
briefing to null left the morning surface empty. The strip now falls
back to the previous payload's briefing, which passed this same gate
when it was written; a content-failed retry still strips hard so a
fabricated number never persists.
2026-07-05 13:27:30 +02:00
Marc-André Bombeck 1623a20af5 Merge pull request #403 from MBombeck/release/v1.27.2
Release v1.27.2 — readable AI texts
v1.27.2
2026-07-04 23:42:35 +02:00
Marc-André Bombeck 0e67168779 chore(release): v1.27.2 — readable AI texts 2026-07-04 23:33:17 +02:00
Marc-André Bombeck e754979abf feat(ai): allow lists and one bold takeaway in the formatting contract
The shared contract banned every structure beyond 1-3 paragraphs, so
longer replies read as unformatted blocks. It now asks for short
blank-line paragraphs (2-4 on longer replies), permits a plain "- "
list when a reply genuinely enumerates three or more parallel items,
and one bold key takeaway per reply. The Coach ground rule keeps
prose-first; a list is the exception, never the default shape.
2026-07-04 23:33:17 +02:00
Marc-André Bombeck a778a0b2f7 feat(insights): render plain lists and bold in the AI prose surfaces
The prose renderer knew only blank-line paragraphs, so a reply that
enumerated options read as dash-prefixed text lines and a bold marker
surfaced as literal asterisks. ProseBlocks now groups consecutive
"- " lines into a real list and renders **bold** spans as strong
text — pure string splitting over the trusted string, fail-closed on
anything unclosed or unknown, still no markdown library. The Coach
streaming renderer settles completed paragraphs through the same
block renderer.
2026-07-04 23:33:16 +02:00
Marc-André Bombeck 6360ba19ee Merge pull request #402 from MBombeck/release/v1.27.1
Release v1.27.1 — iPad layout fixes
v1.27.1
2026-07-04 16:21:54 +02:00
Marc-André Bombeck 109866b5d0 fix(layout): apply the sidebar viewport default after hydration
Reading matchMedia (or localStorage) inside the initial render branches
the hydration render away from the SSR markup — the sidebar is only
CSS-hidden below md and still hydrates its DOM there, so the phone
viewport tripped React #418 in the dashboard console guard. The
collapsed state now gates on the mounted probe like the module filter
above it: SSR and hydration paint the expanded shell, the stored pref
or tablet default applies right after. useIsMobile learns the lg cut
for the tablet probe.
2026-07-04 16:13:15 +02:00
Marc-André Bombeck fe61f34fca chore(release): v1.27.1 — iPad layout fixes 2026-07-04 15:51:47 +02:00
Marc-André Bombeck 2a5cfefc3a test(e2e): guard iPad-portrait widths against overflow and a squeezed column 2026-07-04 15:51:47 +02:00
Marc-André Bombeck c10288d61b fix(layout): let the content column shrink below its children's min width
Without min-w-0 the shell's flex column adopted the measurements
table's intrinsic min width, widening main past the viewport and
putting the whole page on a horizontal scrollbar at tablet widths.
Wide children scroll inside their own overflow-x-auto containers.
2026-07-04 15:51:46 +02:00
Marc-André Bombeck ed5cadf25d fix(layout): start the sidebar on the icon rail at tablet widths
A 256 px expanded sidebar next to a 768 px viewport leaves the content
a 512 px column — an iPad held upright rendered every page squeezed.
With no stored preference, md–lg widths now start collapsed to the
icon rail; a manual toggle persists and wins on every later visit.
2026-07-04 15:51:46 +02:00
Marc-André Bombeck a9e554a083 Merge pull request #400 from MBombeck/release/v1.27.0
Release v1.27.0 — Fitbit and Pixel Watch through Google Health
v1.27.0
2026-07-04 15:10:14 +02:00
Marc-André Bombeck 5b63cb9ac9 Merge main into release/v1.27.0
Reconciles the v1.26.1 Fitbit setup-guide fix with the Google Health
release: google-health.md stays the full runbook (with a pointer to the
classic fitbit.md guide), the doc-parity guard now asserts the Google
Health page against the google-health client's endpoints and scopes,
and the changelog carries both sections.
2026-07-04 14:57:39 +02:00
Marc-André Bombeck 73820016c1 Merge pull request #399 from MBombeck/fix/fitbit-setup-docs
fix(fitbit): correct the setup guide to the classic Fitbit Web API (#396)
2026-07-04 14:55:30 +02:00
Marc-André Bombeck b8794dd4c7 test(integration): anchor the reconciled sleep night inside the 30-day window
The midnight-spanning dual-source case seeded fixed calendar dates; once
they aged past the route's trailing 30-day window the assertion went red
on every branch. Anchor the night relative to now, like the first case
in the file already does.
2026-07-04 14:46:19 +02:00
Marc-André Bombeck f95c0230a3 docs(changelog): restore the 1.26.0 heading above its entries 2026-07-04 14:28:34 +02:00
Marc-André Bombeck 951a8cf9ea test(e2e): count the Google Health status pill on the integrations page 2026-07-04 14:26:36 +02:00
Marc-André Bombeck 160ab6c6b5 style(changelog): remove stray blank lines 2026-07-04 14:25:34 +02:00
Marc-André Bombeck c2bffe9c60 chore(release): v1.27.0 — Fitbit and Pixel Watch through Google Health
Add a Google Health connection that reads Fitbit, Pixel Watch, and Fitbit
Air data through the user's Google account, alongside the existing Fitbit
connection, as the path forward before the classic Fitbit Web API sunsets
in September 2026.
2026-07-03 14:43:07 +02:00
Marc-André Bombeck 0d3067837e fix(google-health): surface the connection status and add the resume and test routes
Wire the settings card to the live connection so it reflects reality
after a connect.

- Emit a google-health entry on the consolidated integration-status
  envelope (configured, connected, last sync, backfill, and the
  needs-reconnect flag). Without it the card read an undefined status and
  showed a permanently disconnected connection even after a successful
  OAuth grant, hiding the sync, disconnect, and reconnect controls.
- Add the resume and test-connection routes the card calls, mirroring the
  classic Fitbit ones.
- Use the shared refresh icon on the reconnect action.
- Drop the experimental-scope opt-in from the runbook and describe ECG
  and irregular-rhythm as a planned addition rather than a current toggle.
2026-07-03 14:41:53 +02:00
Marc-André Bombeck 422a6a7df0 fix(google-health): correct day-keying, timezone, and skin-temperature handling
Tighten the transport so device data lands on the right day and in the
right slot.

- Cumulative daily totals (steps, distance, energy, floors) now key their
  measuredAt and stats:<tag>:<day> external id off the interval's civil
  start date anchored at UTC-midday, not the physical instant. Keying off
  the physical instant put a positive-UTC-offset user's day one calendar
  day early and split the same local day across the Google, Apple, and
  Fitbit sources so neither won the per-day source pick.
- Thread the user's timezone through the sleep, workout, and interval
  parses so an offset-less wall-clock timestamp resolves in the user's
  zone rather than the process zone — the same fix the classic Fitbit
  sleep path already carries.
- Drop skin temperature from the launch set. Google reports a signed
  nightly deviation from baseline, not an absolute reading, so mapping it
  into the absolute wrist-temperature slot stored a delta as a
  temperature and dropped every cold-night negative. It returns once a
  signed-delta model exists.
- Remove the unused daily-rollup read path and field-map, and the
  experimental ECG and irregular-rhythm scopes that had no reader — the
  connect flow now requests exactly the four core scopes it uses.
2026-07-03 14:41:44 +02:00
Marc-André Bombeck 9f7947a729 feat(google-health): add the settings card, runbook, and translations
Surface the Google Health integration to the operator and document the
setup honestly.

- A settings card matching the other integration cards: the BYO Google
  client id and secret form, connect and disconnect, a manual sync, and
  a distinct reconnect prompt when the connection needs fresh consent.
- Register the card in the connections panel and extend the integration
  key, the docs-link provider, and the query-key factory.
- Translations for the card copy across all six locales. The copy states
  plainly that this reads Fitbit, Pixel Watch, and Fitbit Air through the
  user's Google account, that it succeeds the Fitbit connection Google
  retires in September 2026, that the operator registers their own Google
  client, that stress and readiness are not available, and that a
  periodic reconnect may be needed.
- Rewrite the Google Health runbook into the real setup: a Google Cloud
  project, the Health API, an OAuth consent screen kept in Testing, a
  Web Server client, the callback redirect, the four Restricted scopes,
  the hundred-test-user ceiling, and the reconnect caveat.
- Regenerate the OpenAPI contract for the new measurement source.
2026-07-03 14:13:39 +02:00
Marc-André Bombeck b26a18d35e feat(google-health): wire the OAuth routes, sync jobs, and source priority
Add the request surface and the background sync for the Google Health
provider, all built on the shared integration plumbing.

- Routes under /api/google-health: connect (mint the PKCE pair and state
  nonce, redirect to Google), callback (timing-safe CSRF, atomic state
  consume, token exchange, connection upsert, clear the reauth flag,
  enqueue backfill), credentials (encrypt the operator's BYO client id
  and secret field-by-field), disconnect, status, and a rate-limited
  manual sync.
- Jobs: a boot-time backfill, an hourly poll cohort, and a daily
  oauth-state cleanup, registered under the existing dead-queue contract.
- Rank GOOGLE_HEALTH just after Fitbit in the per-metric source ladders;
  it is the successor API for the same devices.
- Add GOOGLE_HEALTH to the readable measurement-source enum but leave it
  out of the writable set — the provider is server-owned, so a client
  cannot attribute a write to it.
- Register the two new encrypted token columns and the two BYO-client
  columns in the encryption-rotation registry and the rotation script.
2026-07-03 14:13:28 +02:00
Marc-André Bombeck fb17a4e9c6 feat(google-health): add the Google Health API transport
Add the transport that reads a user's device data from the Google Health
API (health.googleapis.com/v4): Google OAuth with PKCE and offline
access, the paginated dataPoints reader with a daily-rollup fallback for
the types that lack a list method, the metric/activity/sleep/workout
mappers onto the shared measurement shape, and a response classifier
that separates retryable errors, hard rejects, and the re-consent case.

Access tokens live an hour and refresh tokens are reused, not rotated. A
refresh that comes back invalid_grant raises a distinct reauth-required
signal so the app can prompt a fresh consent rather than fail silently —
the periodic re-consent a Testing-mode Google client needs. The four
core readonly scopes ship by default; the Pixel Watch ECG and
irregular-rhythm scopes stay behind an opt-in flag.

Only the transport is new: the mapped-measurement output, the
upsert/dedup/rollup tail, and the source ladders are the shared ones.
2026-07-03 14:13:16 +02:00
Marc-André Bombeck f56268adf6 feat(google-health): add the connection data model and migration
Introduce the data model behind a Google Health provider that reads
Fitbit, Pixel Watch, and Fitbit Air data through a user's Google account.

- GoogleHealthConnection: 1:1 with the user, encrypted access/refresh
  tokens, token expiry, external Google user id, sync high-water-mark,
  and a needsReauth flag for the periodic re-consent that Google's
  Testing-mode clients require.
- GoogleHealthOAuthState: the connect-flow nonce ledger with the PKCE
  verifier.
- Two encrypted BYO-client columns on User for the operator's own Google
  OAuth client id and secret.
- GOOGLE_HEALTH added to the measurement_source enum.

Migration 0224 mirrors the existing integration shape: a standalone
enum-add, guarded CREATE TABLE / ADD COLUMN, forward-only and idempotent.
2026-07-03 14:13:06 +02:00
Marc-André Bombeck 3fbd7428e5 chore(release): v1.26.1 — Fitbit setup guide fix 2026-07-02 09:10:38 +02:00
Marc-André Bombeck 46db21dbeb fix(fitbit): correct the setup guide to the classic Fitbit Web API
The Fitbit integration connects through the classic Fitbit Web API, so a
self-hoster registers a Fitbit developer app at dev.fitbit.com — but the
setup guide walked users through creating a Google Cloud OAuth client,
whose client id the Fitbit sign-in rejects (unauthorized_client). Rewrite
the guide for the dev.fitbit.com flow (app type, HTTPS callback, scopes,
PKCE), leave the old page as a pointer, and add a test that keeps the
documented scopes and endpoints in sync with the client so the guide
cannot drift from the code again. Flags the September 2026 sunset of the
classic Fitbit Web API.
2026-07-02 09:10:38 +02:00
Marc-André Bombeck 3bceb61aed Merge pull request #397 from MBombeck/release/v1.26.0
Release v1.26.0 — light theme and a consistent interface
v1.26.0
2026-07-02 00:43:22 +02:00
Marc-André Bombeck 2f61ad7a29 chore(release): v1.26.0 — light theme and a consistent interface 2026-07-02 00:32:00 +02:00
Marc-André Bombeck 88474adcd0 refactor(ui): finish the page-header adoption and align the demote label
Route the notifications main view and the documents header through the
canonical page-header vocabulary the rest of the release adopted, and
align the admin demote-role label to the standard Nutzer term.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck f118b6f8c7 refactor(insights): route the dashboard chart colors through theme tokens
The authenticated dashboard's chart lines, reference bands, and blood-
pressure target zones still carried raw Dracula hex, so they rendered as
low-contrast neon on the light theme. Route them through the same tokens
the insights pages use; dark rendering is unchanged.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck 7ca2657454 fix(api): keep logout resilient and drop the over-strict batch time bound
A transient database fault during logout must still clear the session
cookie; record the delete failure on the wide event instead of throwing
before the cookie is cleared, so a user can never stay authenticated
after logging out. Remove the batch measurement future/past instant
refine: cumulative HealthKit rows carry an end-of-day endDate that is
legitimately hours ahead for the current day, and a whole-array parse
would 422 the entire sync. Note the workout batch's deferred Withings
path must write its source directly.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck ba8118546d build(lint): ban raw palette colors and guard the design contract
Add the healthlog/no-raw-palette-color rule so UI code routes color
through the semantic tokens instead of Tailwind palette shades, and
sweep the remaining offenders (password-strength scale, injection-site
accents, redundant dark overrides). Add guard tests for width stability,
the single page-header vocabulary, and motion-reduce coverage on
indefinite animations.
2026-07-02 00:16:03 +02:00
Marc-André Bombeck fda0e504f0 fix(insights): compute the adherence storyline from the schedule-anchored engine
The Coach adherence storyline derived its rate from the raw coverage
rollup, whose denominator counts logged slots rather than scheduled
doses, so a user who logged only taken doses read ~100% and the dip gate
suppressed a real decline. Extract the dashboard tile's schedule-anchored,
cadence-aware compliance into one shared engine and route both surfaces
through it, so Coach and the dashboard report the same number.
2026-07-02 00:16:03 +02:00
Marc-André Bombeck 40859613aa fix(insights): bound the tiered rollup series to the requested window
The week/month/year rollup read anchored on now and always returned the
trailing window, so a historic from/to range (API and iOS callers) came
back with buckets outside the requested span. Thread the resolved from/to
bounds through so the read is bounded on both ends, matching the day-tier
and live-SQL fallback contract; tier selection is unchanged.
2026-07-02 00:16:02 +02:00
Marc-André Bombeck c9f8eb9423 fix(ui): replace native date pickers and tokenize caution colors
Swap the remaining native date inputs (allergy onset, environment
travel/backfill ranges, inbound-document dates) for the app DateField
so they no longer render in the browser's locale order. Route the
medication caution amber and the document info banner through the
warning/info tokens so they adapt to the theme.
2026-07-02 00:00:38 +02:00
Marc-André Bombeck 2afe32492b refactor(insights): route metric-page and chart colors through theme tokens
The per-metric reference bands, chart palettes, and status tints across
the insights pages and shared charts carried hardcoded hex that glared
on the light theme. Route them through the semantic and chart CSS
variables; dark rendering stays byte-identical, light gets the muted set.
2026-07-02 00:00:37 +02:00
Marc-André Bombeck 887c79ec86 refactor(ui): unify page headers and surface read failures
Add a PageHeader primitive (one H1 vocabulary, optional description,
back-link, and action slots) and route the hand-rolled headers through
it, so every module reads with the same title/subtitle rhythm and no
icon beside the page title. Mental Wellbeing gains a visible header.
Add a QueryErrorCard and wire the list surfaces so a failed read shows
a retry card instead of falling through to the empty state, and swap
their full-page spinners for skeletons that hold the layout.
2026-07-02 00:00:37 +02:00
Marc-André Bombeck 336ce5aef3 fix(api): narrow swallowed database errors and redact the incident URL
The incident forwarder stripped the query string but not path-segment
secrets from the reported URL; route it through the same redaction the
message and stack already use. Several delete-and-swallow paths caught
every error as the expected unique/absent-row case, masking real
failures on security-notification, logout, and integration-disconnect
paths and the Telegram reply-consume guard; narrow each to the specific
Prisma code and let real failures surface.
2026-07-01 23:45:08 +02:00
Marc-André Bombeck 9aa0a3597a fix(api): constrain client-writable sources and batch timestamps
Narrow the workout create schema's source to the client-writable set
(MANUAL, APPLE_HEALTH), matching the measurement write surfaces, so a
client cannot forge a server-owned source into the cross-source picker.
Apply the single-entry plausibility bound to the measurement batch
timestamps so a backdated or future-dated row can no longer slip past
the batch path. Regenerate the OpenAPI contract.
2026-07-01 23:45:08 +02:00
Marc-André Bombeck 86d80d2c04 refactor(admin): migrate section cards onto the shared card primitives
The admin tree hand-rolled its card surfaces and stepped padding at sm:
instead of md:, so it read denser than every other route on a tablet.
Move the sections onto SettingsCard/SettingsCardHeader and the mini-rows
onto ListRow; no behaviour, data, or copy changes.
2026-07-01 23:44:55 +02:00