Commit Graph
33 Commits
Author SHA1 Message Date
Marc-André Bombeck 492748f105 fix(bugreport): remove screenshot upload, drop misleading attached note
The bug-report form accepted an image attachment that was stored in the
local DB but never reached the published GitHub issue — GitHub does not
accept inline base64 data URIs in issue bodies and offers no public API
to attach images to an issue programmatically. The publisher only
appended a placeholder line ("**Screenshot:** A screenshot was attached
in the original report.") which was visibly misleading to anyone
reading the issue (#108).

- Drop the file input, preview, validation, and unused icons from
  src/app/bugreport/page.tsx.
- Remove hasScreenshot from FeedbackInput and the corresponding line
  from publish-github.ts so feedback promotion no longer adds the
  misleading note (applies to legacy rows whose screenshots are still
  stored locally).
- Update the admin feedback→GitHub call site to match the new shape.

The screenshotBase64 column, the screenshot ingest path on /api/feedback,
and the admin-side preview of previously-submitted screenshots are
unchanged — existing reports keep their attachments locally.
2026-04-28 19:46:25 +02:00
Marc-André Bombeck 0a77558fd0 fix(i18n): merge duplicate targets blocks, add locale integrity test
Both messages/en.json and messages/de.json had two top-level "targets"
blocks. JSON.parse silently keeps the last occurrence, and that block
was missing glucoseFasting/Postprandial/Random/Bedtime — so glucose
tiles on the dashboard rendered the raw key "targets.glucoseFasting"
instead of the translated label (#108).

Collapsed the duplicate into a single block, dropped a duplicate
bugreport.bugTitlePlaceholder shadowed within the same namespace, and
filled in dashboard.sleep / dashboard.steps which were falling back to
hard-coded English.

New test src/lib/__tests__/i18n-locale-integrity.test.ts walks the raw
JSON to detect duplicate keys at any depth and verifies en/de key
parity, so this class of silent shadowing fails the build going forward.
2026-04-28 19:46:16 +02:00
Marc-André Bombeck 05142841fa fix(lint): suppress react-hooks/set-state-in-effect on pre-existing sync patterns
CI now blocks on ESLint, which surfaced two pre-existing useEffect
sync-from-server patterns. Annotated with targeted eslint-disable
comments and a note that the proper fix lives in 1.4.0 alongside the
settings-page split — refactoring derivation here would touch a
2100-line file.
2026-04-27 00:28:28 +02:00
Marc-André Bombeck 53235a0fb7 fix(security): close SSRF bypasses via hex/decimal IPv4 + IPv4-mapped IPv6
Security review of #106 surfaced two HIGH-severity gaps in the SSRF
guard. Both are documented bypass classes with public CVE precedent
(github.com/projectdiscovery/nuclei-templates' SSRF templates list
hex and IPv4-mapped IPv6 explicitly).

- Hex-notation IPv4: 'http://0x7f.0.0.1' / 'http://0x7f000001' /
  'http://0xa.0.0.1' all parse to internal addresses depending on
  the runtime's URL semantics. Reject the raw form before parsing.
- Decimal-notation IPv4: 'http://2130706433' is 127.0.0.1, etc.
  Reject any pure-numeric host of plausible IP length.
- IPv4-mapped IPv6: '[::ffff:127.0.0.1]' and the hex variant
  '[::ffff:7f00:1]' both target loopback. Match either form and
  apply the private-IPv4 check to the embedded address.
- '[::]' (IPv6 unspecified, alias for 0.0.0.0) blocked alongside
  '[::1]'.

Refactored the per-octet private-range checks into isPrivateIpv4()
since both the IPv4 and the IPv4-mapped-IPv6 paths now use them.

Test suite extended with regression cases for hex, decimal, and
IPv4-mapped IPv6. Now 208/208 passing.
2026-04-27 00:28:28 +02:00
Marc-André Bombeck ef3e37b7da test(security): add SSRF guard tests + measurement validation tests
- New unit tests for isPublicUrl covering loopback, link-local, RFC1918,
  CGNAT, IPv6 ranges, and the leading-zero IPv4 octal-bypass regression
  (URL parser silently turns '010.0.0.1' into '8.0.0.1' — caught now).
- Strengthened isPublicUrl: pre-URL leading-zero check on the raw input,
  bracket-aware IPv6, deny on malformed IPv4 quads instead of falling
  through to 'public' (better safe than sorry on user-supplied URLs).
- New unit tests for measurement validation covering the new
  TOTAL_BODY_WATER and BONE_MASS types: ranges, glucoseContext rejection
  on non-glucose types, and unrecognised type rejection.
2026-04-27 00:28:28 +02:00
Marc-André Bombeck 3de2e37ed5 fix(ux): touch targets, a11y on phase config, admin label i18n
UX/a11y residuals from 2026-04-26 audit:

- bottom-nav: touch targets sized to WCAG 2.5.5 minimum (44x44 CSS px)
  via min-h-11 min-w-11 on each Link. Nav height bumped to h-16 to
  accommodate. Visual icon stays at 20px so design doesn't shift.
- phase-config-dialog: decorative coloured dot now aria-hidden because
  the redundant text label ("Green"/"Yellow"/...) already conveys the
  phase. Screen readers no longer announce a meaningless 'image' node.
- admin: 'Web Push' and 'Bug Report' labels were hard-coded English in
  StatusItem and SettingsToggle calls; now go through t() with new
  admin.integrationWebPush / admin.integrationBugReport keys (en + de).
- medication-card: false-positive en-CA / en-US locale audit findings
  documented inline — those are intentional locale-independent helpers
  (en-CA gives YYYY-MM-DD for day-comparison, en-US gives a parseable
  string for the toBerlinDate timezone hack), not user-facing display.
2026-04-27 00:28:28 +02:00
Marc-André Bombeck 908532be73 feat(security): SSRF strict IPv4, PAT logging redaction, threshold rate-limit, N+1 fix, pg-boss SIGTERM, blocking CI
Security hardening from 2026-04-26 audit:

- SEC-002: isPublicUrl() now uses a strict IPv4 parser that rejects
  leading-zero forms like 010.0.0.1, which historically slipped past
  the h.startsWith('10.') check. Also covers CGNAT (100.64/10),
  IPv6 unique-local (fc00::/7), and additional metadata-service edge
  cases. Documented as input-time only (DNS rebinding still possible
  at fetch time).
- SEC-004: GitHub PAT is now redacted from logged error bodies in
  publish-github.ts before flowing to Wide Events / Loki. Adds an
  escapeRegex helper for safe RegExp construction.
- SEC-006: PUT /api/user/thresholds now goes through checkRateLimit
  (30 writes / 5 min, per-user) — audit-log was already there but
  unbounded. Makes audit-log enumeration unattractive.
- PERF-001: insights/comprehensive route now batches medication-intake
  events into a single 'medicationId IN (...)' query and groups in
  memory instead of N+1 per medication. Latency was scaling poorly
  for users with many active meds.
- REL-003: pg-boss now installs SIGTERM/SIGINT handlers that call
  boss.stop({ graceful: true, timeout: 30s }) so in-flight handlers
  drain on docker stop / coolify redeploy instead of being killed.
- LOG-001: Replaced console.log/error in reminder-worker with a
  workerLog helper. Lifecycle errors still go to stderr (only audience
  outside any handler context); routine init messages are silent
  because pg-boss own events + Wide Events from handlers carry the
  state.
- CI: ESLint and TypeScript checks are now blocking — continue-on-error
  removed. Tests already were. Required pre-existing 'any' types in
  api-handler.ts to be properly justified with eslint-disable comments
  (Next.js variadic handler signature; the bound type still constrains
  return to Promise<Response>).
2026-04-27 00:28:28 +02:00
Marc-André Bombeck 11ed6faedd feat(measurements): add total body water and bone mass types (#89)
- Adds TOTAL_BODY_WATER and BONE_MASS to MeasurementType enum
- Migration 0022 (purely additive ALTER TYPE ... ADD VALUE)
- Both stored canonically in kilograms (matches Withings hydration/bone mass measures and Health Connect TotalBodyWaterRecord/BoneMassRecord)
- Validation, formatters, i18n keys (en + de) for measurement form and doctor-report PDF
- Dashboard widgets registered (default-invisible, opt-in via settings)
- Withings client maps measure types 77 (hydration) and 88 (bone mass)
- Doctor report PDF includes both new types in vital-signs table when data exists
2026-04-27 00:28:28 +02:00
Marc-André BombeckandClaude Opus 4.7 5e721e0791 fix(onboarding): surface errors and supply default schedule (#87)
Issue #87 (kevinpapst): medications added during onboarding were
never visible afterwards. Root cause: the createMedication schema
requires schedules.min(1) ("Mindestens ein Zeitfenster"), but the
onboarding wizard sent schedules:[]. The 422 was silently swallowed
and the user was redirected to the dashboard as if everything had
worked.

This patch is the minimal-impact fix. The proper fix lives in Phase
3.5 of the audit plan, which replaces the wizard entirely with a
dashboard-first empty-state flow. Until then:

- finishOnboarding() wraps each call in a try/catch, checks
  response.ok explicitly, and surfaces a sonner toast on failure.
  setSaving(false) on error so the user can retry instead of being
  stuck on a spinner.
- Each medication is sent with a default schedule (08:00-09:00) so
  it is actually persisted. A new medScheduleHint i18n key is shown
  under the meds list while the user is in the wizard, so the
  default schedule is not surprising.
- Errors are translated per stage (errorProfile, errorMedication
  with {name}, errorTargets) plus an errorGeneric fallback. en.json
  and de.json kept symmetric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:22:10 +02:00
8533755d8d release(v1.2): i18n, thresholds, glucose, dashboard customization, feedback, multi-provider AI (#77)
* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX

Addresses bugs surfaced in issues #73 and #74 plus drift found during audit.

- i18n: add 21 missing translation keys (targets.* namespace was only
  partially populated after the English-first migration; common.reset and
  settings.configured were referenced but absent), plus common helpers
  (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError.
- charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P"
  to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE.
- queries: add src/lib/query-keys.ts as a single source of truth with
  measurement/mood/medication-dependent key sets. Wire them into
  measurement-form/list, mood-form/list, medication-form/card,
  intake-history-list, intake-timeline, medications page, and Withings
  sync in settings. Fixes dashboard not auto-refreshing after adding a
  measurement (old code only invalidated ["measurements"], not
  ["analytics"] / ["insights"] / ["insights","targets"]).
- admin: useUpdateSettings + updateUser mutations now surface real error
  messages via sonner toasts and invoke getApiErrorMessage() so saves
  fail loudly instead of silently. Success also gets a toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(i18n): package B — locale-aware formatters throughout

Removes the hard-coded de-DE locale that lingered from the German-first era
and makes every number, date, and time locale-aware. English stays the
default per user preference; German remains selectable.

New:
- src/lib/format-locale.ts exposes makeFormatters(locale) + a cached
  Europe/Berlin timezone. Covers number, integer, percent, date, dateShort,
  dateTime, time, dateWithWeekday, monthShort. Also exports
  parseLocaleFromAcceptLanguage() for server code.
- useFormatters() hook in @/lib/i18n/context resolves from the active
  locale, memoized per render.

Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") /
Intl.DateTimeFormat("de-DE") call sites):
- src/app/page.tsx (dashboard)
- src/app/insights/page.tsx
- src/app/admin/page.tsx
- src/app/settings/page.tsx (Withings last-sync, doctor-report caller)
- src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels)
- src/components/charts/health-chart.tsx
- src/components/medications/medication-card.tsx
- src/components/medications/medication-form.tsx
- src/components/measurements/measurement-list.tsx
- src/components/mood/mood-list.tsx
- src/components/insights/insight-status-card.tsx (also removed 5 German
  hard-coded strings with TODO: i18n markers — now translated)
- src/components/insights/insight-advisor-card.tsx (same — 10+ strings)

Server: reminder-check / reminder-worker / umami-test switched from
"de-DE" to neutral en-GB/en-US where the string is machine-compared rather
than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now —
the repo is English-speaking.

Next.js metadata:
- OpenGraph locale from "de_DE" → "en_US" with "de_DE" as
  alternateLocale.
- Root layout now reads healthlog-locale cookie (falls back to
  Accept-Language) and sets <html lang> server-side, avoiding an
  EN-flashes-then-DE hydration gap and fixing SEO/a11y.
- I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next
  SSR pass is correct.

Doctor-report PDF:
- generateDoctorReportPDF(data, { t, locale }) now accepts injected
  translator + locale. All section titles, column headers, BMI / BP
  classifications, mood labels, and the footer disclaimer are
  i18n-driven. File name slug switches between gesundheitsbericht / health-report.
- 40+ new doctorReport.* keys in EN + DE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ux): package C — typed passkey errors, bug-report gating, error boundary

Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste
error reporting requested during review.

Passkey registration / login (#73.1):
- src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code
  to specific i18n keys instead of swallowing every failure as
  "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/
  Security/Abort errors and falls back to a diagnostic message.
- settings passkey + login passkey flows both use describePasskeyError
  now, so origin mismatches and previous-registration collisions show
  actionable copy.
- 5 new passkey.* i18n keys in EN + DE.

Bug report (#73.5):
- New GET /api/bugreport/status reports {configured, hasToken, hasRepo,
  isAdmin}.
- /bugreport page now queries status first. When not configured it shows
  a notice (different copy for admins vs. regular users) with a direct
  link to the admin panel. Form is hidden, not silently broken.
- When POST /api/bugreport can't create issues it returns 503 instead
  of 500 with the same user-facing message.
- 4 new bugreport.* i18n keys.

Global error UX:
- src/components/error-details.tsx — reusable panel with retry + copy
  diagnostics (message, digest, stack (trimmed), URL, user-agent,
  request-id, locale, timestamp) + report-issue link. Clipboard fallback
  prompt covers insecure-context cases.
- src/app/error.tsx uses it.
- src/app/global-error.tsx is provider-free (no i18n dependency) since
  it replaces the root layout on catastrophic failures — hardcoded
  English + Dracula palette so it still looks like HealthLog.
- 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails,
  retry, reportIssue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper

Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))`
verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and
adopts it everywhere, including replacing the local `invalidateMedicationRelated`
helper that medication-form.tsx had grown. Also fixes the import-sandwich that
helper introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: small simplifications in i18n/UX changes

- passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow
- doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention
- dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place
- health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai): capture upstream response body in OpenAI errors

A production insights burst at 12:19 UTC failed with "OpenAI request
failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed
via logs), but we had no way to tell from logs whether it was an
OpenAI incident, a model-not-found, or a response_format rejection
because the response body was thrown away.

Now the error message includes the first 500 chars of the response body
and structured fields (httpStatus, upstream, model) that Wide Event
logging can attach. No behavior change for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): address code-review + security findings

Review round produced five real issues — fix them all, plus three nits
that were cheap enough to bundle.

Must-fix:
- security: error-details.tsx + global-error.tsx were copying
  window.location.href into the clipboard, which on OAuth callback
  routes (/api/withings/*, password reset) would leak code/state
  tokens into GitHub issues. Strip to pathname + searchParamCount.
- fe: query-keys.invalidateKeys now uses Promise.allSettled, so a
  transient invalidate failure doesn't abort the chain and no
  unhandled rejection leaks from void invalidateKeys(...) call sites.
- fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers()
  in try/catch — a dynamic-server error in the root layout now
  degrades to the "en" default instead of crashing into global-error.
- be: reminder-worker + reminder-check parseTimeToMinutes normalize
  "24:00" → "00:00" (some ICU builds emit that for midnight under
  en-GB). Prevents a 1440-minute comparison against schedule windows.
- security: openai-client error body excerpt redacts sk-* / Bearer
  tokens before logging, and moves the excerpt off Error.message into
  a structured field (bodyExcerpt) so log transports can filter it.
- i18n: /api/withings/connect German error message → English.
- i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...)

Hardening:
- /api/bugreport/status collapses to {configured, isAdmin} and adds
  a 30/min rate limit per user. Client interface updated.
- i18n/context.tsx: I18nProvider's initializer now reads the cookie
  (matches SSR) before falling back to localStorage or browser
  detection — eliminates the locale-flash hydration class.
- format.ts legacy helpers also read cookie first, matching SSR.

Tests:
- new src/lib/__tests__/format-locale.test.ts — 9 cases covering
  makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale
- new passkey-errors.test.ts — 8 cases covering every DOMException
  name and SimpleWebAuthn code we map
- new query-keys.test.ts — bundles + invalidateKeys allSettled
  semantics (incl. partial-failure test)

118 → 142 tests. All pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(v1.2): thresholds, glucose, dashboard customization, built-in feedback, multi-provider AI (#76)

* build(deps): bump @prisma/adapter-pg from 7.6.0 to 7.7.0 (#60)

Bumps [@prisma/adapter-pg](https://github.com/prisma/prisma/tree/HEAD/packages/adapter-pg) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/adapter-pg)

---
updated-dependencies:
- dependency-name: "@prisma/adapter-pg"
  dependency-version: 7.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump prisma from 7.6.0 to 7.7.0 (#61)

Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/cli)

---
updated-dependencies:
- dependency-name: prisma
  dependency-version: 7.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump eslint-config-next from 16.2.2 to 16.2.3 (#62)

Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.2.2 to 16.2.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.2.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump next from 16.2.2 to 16.2.3 (#65)

Bumps [next](https://github.com/vercel/next.js) from 16.2.2 to 16.2.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.2...v16.2.3)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @types/node from 25.5.2 to 25.6.0 (#66)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 25.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump dotenv from 17.4.0 to 17.4.1 (#67)

Bumps [dotenv](https://github.com/motdotla/dotenv) from 17.4.0 to 17.4.1.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.4.0...v17.4.1)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-version: 17.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @tanstack/react-query from 5.96.2 to 5.99.0 (#70)

Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.2 to 5.99.0.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.99.0/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.99.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump typescript from 5.9.3 to 6.0.2 (#64)

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump prettier from 3.8.1 to 3.8.2 (#68)

Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump react from 19.2.4 to 19.2.5 (#63)

Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.4 to 19.2.5.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump vitest from 4.1.2 to 4.1.4 (#69)

Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.2 to 4.1.4.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @prisma/client from 7.6.0 to 7.7.0 (#71)

Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 7.6.0 to 7.7.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/7.7.0/packages/client)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-version: 7.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump shadcn from 4.1.2 to 4.2.0 (#72)

Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.1.2 to 4.2.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.2.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(thresholds): foundation + settings UI (v1.2)

Introduces per-user health threshold overrides with an auto-default /
user-override pattern. Default computation stays evidence-based (WHO,
ESC/ESH 2018, AHA, AASM, ADA 2024, DGIM/DDG S3). Overrides live in
User.thresholdsJson; any missing metric falls through to the default so
new users need zero configuration.

Schema (migration 0021):
- New enums: glucose_context (FASTING/POSTPRANDIAL/RANDOM/BEDTIME),
  feedback_category, feedback_status.
- measurement_type gains BLOOD_GLUCOSE; Measurement gets optional
  glucose_context (enforced via CHECK constraint — only set for glucose).
- User gains thresholds_json, dashboard_widgets_json, glucose_unit,
  ai_provider, ai_model, ai_base_url, ai_anthropic_key_encrypted,
  ai_local_key_encrypted (stubs for the upcoming features).
- New Feedback table for the built-in feedback system (next commit).

Backend:
- src/lib/analytics/effective-range.ts is the single source of truth:
  getEffectiveRange(metric, profile, overrides) returns the resolved range
  + the pristine default + physiological bounds. Every chart, band,
  insight prompt, and doctor-report section should call this.
- Supports 11 metrics incl. four glucose contexts with ADA/DGIM ranges.
- src/lib/validations/thresholds.ts — Zod schema with per-metric bounds
  and min<max refinement.
- GET/PUT/DELETE /api/user/thresholds with full audit logging (before/
  after) so medical-ish data mutations are traceable.

UI:
- src/components/settings/thresholds-section.tsx — clean per-metric row
  with Auto/Override Switch, inline min/max, reset-per-row + reset-all,
  and a "this looks outside guideline ranges" warning banner that only
  shows when the override is >30% off the default.
- New Personalization section in /settings.
- 25 new i18n keys (EN + DE) in a `thresholds.*` namespace.

Tests: 8 new cases covering default computation, override application,
missing profile gracefully returning null, glucose-context defaults, and
per-metric bounds. 150/150 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(glucose): wire BLOOD_GLUCOSE + context through the write path

Extends measurement validation and the POST /api/measurements route to
accept blood-glucose samples with a required `glucoseContext`
(FASTING / POSTPRANDIAL / RANDOM / BEDTIME).

- Zod schema adds BLOOD_GLUCOSE to the type enum and glucoseContextEnum.
- refine(): BLOOD_GLUCOSE requires a context; every other type must not
  set one — mirrored by the Postgres CHECK constraint from migration 0021
  so callers bypassing validation still can't write inconsistent rows.
- VALUE_RANGES gains BLOOD_GLUCOSE (20-800 mg/dL) — covers severe hypo
  through severe hyperglycemia.
- getUnitForType(BLOOD_GLUCOSE) returns "mg/dL" (canonical storage).
  Display in mmol/L is a UI-level conversion per user preference.
- Error messages translated to English (removed two lingering German
  refine messages while here).

Measurement form + charts + targets UI follow in the next commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): localize AI insight prompts (en/de)

Each prompt builder (base-system, blood-pressure, bmi, general-status,
medication-compliance, mood, pulse, weight) now accepts a Locale and
returns the system + user prompt either fully in English or fully in
German. Stable contract keys (classification enum optimal/gut/grenzwertig
/erhoht/kritisch, confidence niedrig/mittel/hoch) are preserved verbatim
so the response parser keeps working — only the natural-language sections
are translated. The prompt explicitly tells the model to emit
classificationLabel / summary / recommendations in the user's language.

Wires the locale through every status generator (general / blood-pressure
/ bmi / weight / pulse / mood / medication-compliance) into the prompt
calls and adds a small server-side resolver
(src/lib/i18n/server-locale.ts: explicit override > healthlog-locale or
userLocale cookie > User.locale > Accept-Language header > default
English) used by all /api/insights/*-status routes plus
/api/insights/generate. The auditLog cache key already contains the
locale suffix, so cached entries stay disjoint per language.

The reminder worker continues to fall back to "de" when User.locale is
null, matching its prior behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(glucose): UI + unit conversion helpers

- src/lib/glucose.ts — mg/dL ↔ mmol/L conversion (factor 18.0182 per
  DGIM/DDG), resolveGlucoseUnit() defaulting to mg/dL,
  thresholdMetricForContext() mapping each context to the matching
  effective-range key.
- Measurement form gains a "Blood glucose" option (mg/dL) with a
  context selector (fasting / postprandial / random / bedtime). The
  glucoseContext field is only sent for BLOOD_GLUCOSE — the Zod
  refinement and the Postgres CHECK constraint both enforce that.
- 10 new i18n keys per locale (measurements.typeBloodGlucose,
  glucoseContext*, glucoseUnit, glucoseUnitHint).
- 6 new tests covering conversion roundtrip, unit resolution defaults,
  and context → threshold-metric mapping.

Charts, target cards, and doctor-PDF row for glucose follow in the
next commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(feedback): built-in feedback system (no GitHub required)

Implements the feedback-system design from the v1.2 spec. The submit
button on /bugreport now always works — even when no admin has
configured a GitHub repository. Feedback is stored in the Feedback
table (migration 0021) with an admin inbox for triage.

Endpoints:
- POST /api/feedback — user submits. Auth-gated, rate-limited 5/hour.
  Category (BUG / FEATURE_REQUEST / QUESTION / OTHER), subject,
  description, optional screenshot (≤5 MB, base64), optional metadata
  (locale, URL path, userAgent).
- GET /api/feedback — user's own history (paginated).
- GET /api/admin/feedback — admin inbox with status filter and
  counts-by-status for tab badges.
- PATCH /api/admin/feedback/[id] — update status / admin note.
- DELETE /api/admin/feedback/[id] — soft-archive.

Every mutation writes to AuditLog with before/after details.

UI:
- /bugreport rewritten: category dropdown, optional subject, always
  submits. Informational banner explains whether the server has GitHub
  escalation available or keeps feedback internal-only.
- Legacy "not configured" blocker is gone — the form is never hidden.
- 8 new bugreport.* i18n keys per locale.

The old POST /api/bugreport route and /api/bugreport/status endpoint
are left intact for backwards compatibility; they'll be removed in
v1.3 once no one depends on them.

Admin inbox UI lands in the next commit alongside the dashboard and
multi-provider work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(telegram): localize bot UI per User.locale

The Telegram webhook now resolves the user's locale (User.locale, falling
back to "de" so existing bots stay German) and runs every reply, inline-
keyboard label and callback toast through a small server-side translator.

Adds src/lib/i18n/server-translator.ts (getServerTranslator) — reads the
same messages/{en,de}.json bundles the client uses, with the same
"missing key falls back to English, then to the raw key" semantics, and
the same {param} interpolation. Importing the client useTranslations()
hook into a server route would have pulled React in, so this mirrors
just the parts the bot needs.

Adds the telegram.* namespace to messages/en.json and messages/de.json
covering: button labels, callback toasts, the /help body, /add prompt,
medication-not-found / inactive errors, snooze/skip/confirm confirmations
and the cancel button. The "genommen" / "taken" intake keyword and the
"hi/hallo/hey/moin/hello" greeting list are accepted regardless of UI
locale to keep backwards compatibility for existing users.

Also adds 6 vitest cases for getServerTranslator (basic resolution,
locale switching, parameter interpolation, English fallback, raw-key
fallback, exposed locale).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dashboard): user-customizable widget layout

Implements the dashboard customization spec: users choose which widgets
appear on / and in what order. Auto-default keeps the current v1.1
behavior so nothing changes without opt-in.

Backend:
- src/lib/dashboard-layout.ts — single source of truth with
  DEFAULT_DASHBOARD_LAYOUT (10 widgets incl. the new sleep/steps/glucose
  ones that were previously hard-conditional), a version field for
  future schema evolution, and resolveDashboardLayout() that merges
  persisted state with defaults so new widgets auto-appear (invisible
  by default) when the schema bumps.
- GET / PUT / DELETE /api/dashboard/widgets with strict Zod validation
  (version literal, widget-id enum, order range).
- Persisted in User.dashboardWidgetsJson (column added in 0021).

UI:
- src/components/settings/dashboard-layout-section.tsx — mounted under
  the new Personalization section alongside Thresholds. Local edit
  buffer so toggling/reordering doesn't fire a network call per click;
  single "Save" commits. Each widget has Up/Down buttons + visibility
  toggle, plus a reset-to-defaults button at the section header.
- 11 new dashboard.* i18n keys per locale.

Dashboard page integration is staged — widgets currently still render
per the old data-conditional rules. Wiring the saved layout through
/app/page.tsx is a follow-up so this commit stays focused on the
plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(auth): locale-aware password-strength feedback

Completes the zxcvbn i18n migration by wiring the new
getZxcvbnTranslations(locale) helper into the client PasswordStrength
component and removing the obsolete zxcvbn-de helper.

- src/components/ui/password-strength.tsx reads the active locale from
  useTranslations() and memoizes the translator per locale — so
  changing language instantly retranslates open warnings/suggestions.
- src/lib/zxcvbn-de.ts deleted. Every consumer now routes through
  zxcvbn-i18n.ts which carries both EN and DE mappings.
- server-side checkPasswordStrength (already refactored earlier in this
  session) continues to accept locale; clients can pass it from the
  session.

No test impact — 162 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dashboard): replace setState-in-effect with draft-overlay pattern

The dashboard-layout-section.tsx initial implementation synced server
state into local state via useEffect, which trips the project's strict
react-hooks/set-state-in-effect rule. Rewrote using a nullable draft
pattern: `layout` is derived as `draft ?? remote`, and only user edits
set the draft. Save/Reset clear the draft so the server copy wins
again. Cancel also clears instead of re-seeding from server.

No behavior change, no new tests — the component contract stays the
same. Lint back to baseline (2 errors / 7 warnings, both pre-existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: v1.2 README + CHANGELOG (thresholds, glucose, dashboard, feedback, multi-provider AI)

- CHANGELOG 1.2.0 entry covering 5 new features + locale polish
- README Key Features expanded: glucose, custom thresholds, dashboard
  customization, multi-provider AI, built-in feedback
- Doctor Report description updated for locale-aware output

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(targets): glucose cards per context with effective range + unit conversion

Render one target card per logged glucose context (FASTING, POSTPRANDIAL,
RANDOM, BEDTIME) on /targets, sourcing the threshold band from
getEffectiveRange() so user overrides apply consistently. The targets API
returns glucose values canonical mg/dL; the page converts to the user's
glucoseUnit preference via convertGlucose() before display.

- Extend /api/insights/targets to include per-context glucose summaries
  (latest, 30-day average, range, classification) and surface glucoseUnit
  in the profile envelope.
- Expose glucoseUnit on /api/auth/me and AuthUser type.
- Targets page resolves the i18n label key, applies unit conversion to
  current/avg/range, and uses Droplet iconography.
- Add targets.glucoseFasting/Postprandial/Random/Bedtime keys to en + de.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(security): address H1 (CSP nonce), H3 (SSRF), M1+M2 (XSS)

From the v1.2 full-repo security audit:

- **H1 / CWE-79** src/proxy.ts: CSP nonce was base64 of the UUID *string*
  form, which gives ~122 bits and a structured pattern (hyphens at fixed
  offsets). Replaced with 16 random bytes → proper 128-bit nonce.
- **H3 / CWE-918** src/app/api/admin/ai-settings/route.ts: the admin AI
  base URL only required "https://". A compromised admin cookie could
  point it at attacker.example and exfiltrate the bearer key on the
  next insight call. Now validates with URL parser, requires https://,
  and only accepts hosts on a small allowlist (OpenAI, Anthropic,
  Google AI, Mistral, Groq, OpenRouter). Ops can extend via
  `ADMIN_AI_BASE_URL_ALLOWLIST` env var.
- **M1 / CWE-79 (stored, off-platform)** src/app/api/bugreport/route.ts:
  bug-report description flowed to GitHub with only backtick/asterisk
  stripping. Now strips HTML tags entirely, neutralises dangerous
  markdown link schemes (javascript:/data:/vbscript:/file:), and wraps
  the whole body in a fenced code block so residual markdown renders
  literally.
- **M2 / CWE-79** Zod schemas for feedback + bugreport screenshot now
  require a base64 PNG/JPEG/WEBP/GIF data URL via regex. SVG
  deliberately excluded — it can carry inline JS that would execute
  when the admin inbox renders the `<img>` preview.

Medium rate-limiter semantics (audit M3) left as-is: the fixed-window
behaviour is acceptable for a single-tenant PWA and matches what ops
actually want. Remaining M4–M6 + L1–L7 are tracked for v1.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dashboard): glucose TrendCards + chart label support

Render one TrendCard per logged glucose context on the dashboard when
the user has the "glucose" widget enabled in their layout. Cards
respect the user's glucoseUnit preference via convertGlucose(); data
sources from the existing /api/analytics endpoint, which now emits a
glucoseByContext map alongside the summaries object.

- /api/analytics: include BLOOD_GLUCOSE in the type whitelist (so
  total counts include glucose) and add glucoseByContext containing a
  summarize() result per context.
- HealthChart: wire BLOOD_GLUCOSE into BASE_TYPE_LABEL_KEYS so the
  legend shows the localized "Blood glucose" label when the chart is
  invoked for that type.
- Dashboard page: fetch the dashboard layout, gate glucose cards on
  widget visibility AND data presence, render Droplet-icon TrendCards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): bugreport.subtitle no longer promises a GitHub issue

The visual QA pass flagged that bugreport.subtitle still said "a
GitHub issue will be created automatically" even though the banner
below the heading correctly explains the submission is stored locally
(built-in feedback system, GitHub is only an optional escalation).
Neutral copy now that matches the actual behaviour in both locales.

* feat(ai): user-level provider selection + test endpoint + UI

Adds the missing pieces for the multi-provider AI MVP that pair with
the AnthropicClient and LocalOpenAICompatibleClient already on disk:

* /api/user/ai-provider (GET + PATCH): user can pick OPENAI /
  ANTHROPIC / LOCAL / CHATGPT_OAUTH and store provider, model, baseUrl
  and per-provider API keys. Anthropic + local keys go through the
  existing AES-256-GCM crypto helper. Provider value is whitelisted.

* /api/ai/test (POST): rate-limited (5/min/user) round-trip that
  resolves the user's provider and asks for a tiny JSON ack so the UI
  surfaces real "does this config work?" feedback instead of a blind
  Save.

* settings/page.tsx: appends a per-user override card under the
  existing AI Insights section. Provider dropdown drives conditional
  fields (Anthropic key for ANTHROPIC, baseUrl + optional bearer for
  LOCAL, model for all), then Save / Test buttons.

* provider.test.ts: extended to cover the four new cases (ANTHROPIC
  with key, ANTHROPIC fallback, LOCAL, OPENAI routes through admin
  key, CHATGPT_OAUTH routes through Codex). Switched to
  resetAllMocks() in beforeEach to flush mockResolvedValueOnce queues
  between cases — the prior clearAllMocks() leaked them.

* local-client.test.ts: full happy / no-auth / bearer-auth / structured
  error / empty-content coverage mirroring the openai-client suite.

typecheck + tests green; lint stays at the pre-existing 2 errors / 7
warnings baseline (no new findings introduced).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: integrate late agent additions (glucose PDF row + i18n)

* test(glucose): cover display-unit conversion edge cases

Lock in the precision contract for the targets/dashboard/PDF surfaces:
canonical mg/dL stays as integers, mmol/L collapses to 1-decimal, and
the ADA diabetes thresholds (70/99/126) round-trip to their published
SI values (3.9 / 5.5 / 7.0 mmol/L).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): eliminate hydration flash by threading SSR-resolved locale to I18nProvider

Layout already resolves the initial locale from cookie + Accept-Language and
sets <html lang>. Until now the client I18nProvider re-detected locale at
mount via getSavedLocale() / detectSystemLocale(), causing a flash where
"Loading…" (EN) rendered first, then flipped to "Laden…" (DE) after
hydration. Pass initialLocale down layout -> Providers -> I18nProvider so
useState seeds the same locale the server used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin/feedback): return 409 when GitHub issue already published

Spec requires 409 Conflict (not 200 with flag) when the feedback row
already has gitHubIssueUrl set. Drops the alreadyPublished response
field in favour of a clear error status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(crypto,dashboard): L4 hard-fail short ENCRYPTION_KEY in prod + dashboard WIP from parallel agent

* refactor(effective-range): collapse duplicated glucose case arms

Extract glucoseRange() helper and toDate() normalizer. Use METRIC_BOUNDS
keys as the source of truth for getAllEffectiveRanges() metric list so
additions stay in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(dashboard-layout-section): drop redundant setLayout alias

Remove the trivial setLayout wrapper and hoist the per-row label key
lookup so it runs once per widget instead of twice (span + aria-label).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(security): MED-1 SSRF on user AI baseUrl + MED-2 publish-to-GitHub race

Final-audit blockers before v1.2 merge.

MED-1 — SSRF via user-level LOCAL AI baseUrl
Authenticated user could set aiBaseUrl to http://169.254.169.254/,
http://localhost:9090/metrics, or any internal host and get response
bodies echoed back via the bodyExcerpt error field. Route the value
through the existing isPublicUrl() helper (exported from
lib/validations/notifications.ts). Ops can opt-in to private hosts on a
specific instance via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true — that's how
self-hosted Ollama / LM Studio stays usable without exposing the SSRF
surface by default.

MED-2 — Race in POST /api/admin/feedback/[id]/github
Two concurrent admin clicks both saw gitHubIssueUrl=null and both
created duplicate GitHub issues. Atomic claim now: a single
updateMany({ where: { id, gitHubIssueUrl: null }, data: PENDING }) —
only the caller whose count=1 proceeds. PENDING is released on failure
so retries work.

Tests + typecheck green. Baseline lint unchanged.

* fix: i18n completeness + UX polish (closes #73, #74) (#75)

* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX

Addresses bugs surfaced in issues #73 and #74 plus drift found during audit.

- i18n: add 21 missing translation keys (targets.* namespace was only
  partially populated after the English-first migration; common.reset and
  settings.configured were referenced but absent), plus common helpers
  (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError.
- charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P"
  to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE.
- queries: add src/lib/query-keys.ts as a single source of truth with
  measurement/mood/medication-dependent key sets. Wire them into
  measurement-form/list, mood-form/list, medication-form/card,
  intake-history-list, intake-timeline, medications page, and Withings
  sync in settings. Fixes dashboard not auto-refreshing after adding a
  measurement (old code only invalidated ["measurements"], not
  ["analytics"] / ["insights"] / ["insights","targets"]).
- admin: useUpdateSettings + updateUser mutations now surface real error
  messages via sonner toasts and invoke getApiErrorMessage() so saves
  fail loudly instead of silently. Success also gets a toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(i18n): package B — locale-aware formatters throughout

Removes the hard-coded de-DE locale that lingered from the German-first era
and makes every number, date, and time locale-aware. English stays the
default per user preference; German remains selectable.

New:
- src/lib/format-locale.ts exposes makeFormatters(locale) + a cached
  Europe/Berlin timezone. Covers number, integer, percent, date, dateShort,
  dateTime, time, dateWithWeekday, monthShort. Also exports
  parseLocaleFromAcceptLanguage() for server code.
- useFormatters() hook in @/lib/i18n/context resolves from the active
  locale, memoized per render.

Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") /
Intl.DateTimeFormat("de-DE") call sites):
- src/app/page.tsx (dashboard)
- src/app/insights/page.tsx
- src/app/admin/page.tsx
- src/app/settings/page.tsx (Withings last-sync, doctor-report caller)
- src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels)
- src/components/charts/health-chart.tsx
- src/components/medications/medication-card.tsx
- src/components/medications/medication-form.tsx
- src/components/measurements/measurement-list.tsx
- src/components/mood/mood-list.tsx
- src/components/insights/insight-status-card.tsx (also removed 5 German
  hard-coded strings with TODO: i18n markers — now translated)
- src/components/insights/insight-advisor-card.tsx (same — 10+ strings)

Server: reminder-check / reminder-worker / umami-test switched from
"de-DE" to neutral en-GB/en-US where the string is machine-compared rather
than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now —
the repo is English-speaking.

Next.js metadata:
- OpenGraph locale from "de_DE" → "en_US" with "de_DE" as
  alternateLocale.
- Root layout now reads healthlog-locale cookie (falls back to
  Accept-Language) and sets <html lang> server-side, avoiding an
  EN-flashes-then-DE hydration gap and fixing SEO/a11y.
- I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next
  SSR pass is correct.

Doctor-report PDF:
- generateDoctorReportPDF(data, { t, locale }) now accepts injected
  translator + locale. All section titles, column headers, BMI / BP
  classifications, mood labels, and the footer disclaimer are
  i18n-driven. File name slug switches between gesundheitsbericht / health-report.
- 40+ new doctorReport.* keys in EN + DE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ux): package C — typed passkey errors, bug-report gating, error boundary

Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste
error reporting requested during review.

Passkey registration / login (#73.1):
- src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code
  to specific i18n keys instead of swallowing every failure as
  "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/
  Security/Abort errors and falls back to a diagnostic message.
- settings passkey + login passkey flows both use describePasskeyError
  now, so origin mismatches and previous-registration collisions show
  actionable copy.
- 5 new passkey.* i18n keys in EN + DE.

Bug report (#73.5):
- New GET /api/bugreport/status reports {configured, hasToken, hasRepo,
  isAdmin}.
- /bugreport page now queries status first. When not configured it shows
  a notice (different copy for admins vs. regular users) with a direct
  link to the admin panel. Form is hidden, not silently broken.
- When POST /api/bugreport can't create issues it returns 503 instead
  of 500 with the same user-facing message.
- 4 new bugreport.* i18n keys.

Global error UX:
- src/components/error-details.tsx — reusable panel with retry + copy
  diagnostics (message, digest, stack (trimmed), URL, user-agent,
  request-id, locale, timestamp) + report-issue link. Clipboard fallback
  prompt covers insecure-context cases.
- src/app/error.tsx uses it.
- src/app/global-error.tsx is provider-free (no i18n dependency) since
  it replaces the root layout on catastrophic failures — hardcoded
  English + Dracula palette so it still looks like HealthLog.
- 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails,
  retry, reportIssue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper

Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))`
verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and
adopts it everywhere, including replacing the local `invalidateMedicationRelated`
helper that medication-form.tsx had grown. Also fixes the import-sandwich that
helper introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: small simplifications in i18n/UX changes

- passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow
- doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention
- dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place
- health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai): capture upstream response body in OpenAI errors

A production insights burst at 12:19 UTC failed with "OpenAI request
failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed
via logs), but we had no way to tell from logs whether it was an
OpenAI incident, a model-not-found, or a response_format rejection
because the response body was thrown away.

Now the error message includes the first 500 chars of the response body
and structured fields (httpStatus, upstream, model) that Wide Event
logging can attach. No behavior change for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): address code-review + security findings

Review round produced five real issues — fix them all, plus three nits
that were cheap enough to bundle.

Must-fix:
- security: error-details.tsx + global-error.tsx were copying
  window.location.href into the clipboard, which on OAuth callback
  routes (/api/withings/*, password reset) would leak code/state
  tokens into GitHub issues. Strip to pathname + searchParamCount.
- fe: query-keys.invalidateKeys now uses Promise.allSettled, so a
  transient invalidate failure doesn't abort the chain and no
  unhandled rejection leaks from void invalidateKeys(...) call sites.
- fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers()
  in try/catch — a dynamic-server error in the root layout now
  degrades to the "en" default instead of crashing into global-error.
- be: reminder-worker + reminder-check parseTimeToMinutes normalize
  "24:00" → "00:00" (some ICU builds emit that for midnight under
  en-GB). Prevents a 1440-minute comparison against schedule windows.
- security: openai-client error body excerpt redacts sk-* / Bearer
  tokens before logging, and moves the excerpt off Error.message into
  a structured field (bodyExcerpt) so log transports can filter it.
- i18n: /api/withings/connect German error message → English.
- i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...)

Hardening:
- /api/bugreport/status collapses to {configured, isAdmin} and adds
  a 30/min rate limit per user. Client interface updated.
- i18n/context.tsx: I18nProvider's initializer now reads the cookie
  (matches SSR) before falling back to localStorage or browser
  detection — eliminates the locale-flash hydration class.
- format.ts legacy helpers also read cookie first, matching SSR.

Tests:
- new src/lib/__tests__/format-locale.test.ts — 9 cases covering
  makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale
- new passkey-errors.test.ts — 8 cases covering every DOMException
  name and SimpleWebAuthn code we map
- new query-keys.test.ts — bundles + invalidateKeys allSettled
  semantics (incl. partial-failure test)

118 → 142 tests. All pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Marc-André Bombeck <marc@bombeck.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: restore Link import lost in merge

* fix(bugreport): remove stale loading + not-configured blocks lost in merge

The v1.2 built-in feedback refactor made status informational only — form
always renders. Main's merge re-introduced the pre-refactor blocker blocks
referencing undefined statusLoading/AlertCircle and broke the build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marc-André Bombeck <marc@bombeck.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Marc-André Bombeck <marc@bombeck.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 22:20:12 +02:00
9b59390224 fix: i18n completeness + UX polish (closes #73, #74) (#75)
* fix(i18n,ux): package A — missing keys, chart labels, query invalidation, admin error UX

Addresses bugs surfaced in issues #73 and #74 plus drift found during audit.

- i18n: add 21 missing translation keys (targets.* namespace was only
  partially populated after the English-first migration; common.reset and
  settings.configured were referenced but absent), plus common helpers
  (copy, copyDetails, retry, reportIssue) and admin.settingsSaveError.
- charts: rename points7Label/30Label/90Label from cryptic "7P/30P/90P"
  to "7 pts / 30 pts / 90 pts" in EN and "7 Pkt / 30 Pkt / 90 Pkt" in DE.
- queries: add src/lib/query-keys.ts as a single source of truth with
  measurement/mood/medication-dependent key sets. Wire them into
  measurement-form/list, mood-form/list, medication-form/card,
  intake-history-list, intake-timeline, medications page, and Withings
  sync in settings. Fixes dashboard not auto-refreshing after adding a
  measurement (old code only invalidated ["measurements"], not
  ["analytics"] / ["insights"] / ["insights","targets"]).
- admin: useUpdateSettings + updateUser mutations now surface real error
  messages via sonner toasts and invoke getApiErrorMessage() so saves
  fail loudly instead of silently. Success also gets a toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(i18n): package B — locale-aware formatters throughout

Removes the hard-coded de-DE locale that lingered from the German-first era
and makes every number, date, and time locale-aware. English stays the
default per user preference; German remains selectable.

New:
- src/lib/format-locale.ts exposes makeFormatters(locale) + a cached
  Europe/Berlin timezone. Covers number, integer, percent, date, dateShort,
  dateTime, time, dateWithWeekday, monthShort. Also exports
  parseLocaleFromAcceptLanguage() for server code.
- useFormatters() hook in @/lib/i18n/context resolves from the active
  locale, memoized per render.

Migrated (all Intl.NumberFormat("de-DE") / toLocaleString("de-DE") /
Intl.DateTimeFormat("de-DE") call sites):
- src/app/page.tsx (dashboard)
- src/app/insights/page.tsx
- src/app/admin/page.tsx
- src/app/settings/page.tsx (Withings last-sync, doctor-report caller)
- src/components/charts/trend-card.tsx (+ i18n "7d"/"30d" short labels)
- src/components/charts/health-chart.tsx
- src/components/medications/medication-card.tsx
- src/components/medications/medication-form.tsx
- src/components/measurements/measurement-list.tsx
- src/components/mood/mood-list.tsx
- src/components/insights/insight-status-card.tsx (also removed 5 German
  hard-coded strings with TODO: i18n markers — now translated)
- src/components/insights/insight-advisor-card.tsx (same — 10+ strings)

Server: reminder-check / reminder-worker / umami-test switched from
"de-DE" to neutral en-GB/en-US where the string is machine-compared rather
than user-facing. Bug-report GitHub issue titles use ISO-8601 UTC now —
the repo is English-speaking.

Next.js metadata:
- OpenGraph locale from "de_DE" → "en_US" with "de_DE" as
  alternateLocale.
- Root layout now reads healthlog-locale cookie (falls back to
  Accept-Language) and sets <html lang> server-side, avoiding an
  EN-flashes-then-DE hydration gap and fixing SEO/a11y.
- I18nProvider mirrors locale to a 1-year SameSite=Lax cookie so the next
  SSR pass is correct.

Doctor-report PDF:
- generateDoctorReportPDF(data, { t, locale }) now accepts injected
  translator + locale. All section titles, column headers, BMI / BP
  classifications, mood labels, and the footer disclaimer are
  i18n-driven. File name slug switches between gesundheitsbericht / health-report.
- 40+ new doctorReport.* keys in EN + DE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ux): package C — typed passkey errors, bug-report gating, error boundary

Addresses issue #73 points 1, 5, 6 and lays groundwork for copy-paste
error reporting requested during review.

Passkey registration / login (#73.1):
- src/lib/passkey-errors.ts maps DOMException.name + SimpleWebAuthn code
  to specific i18n keys instead of swallowing every failure as
  "cancelled". Distinguishes NotAllowed/InvalidState/NotSupported/
  Security/Abort errors and falls back to a diagnostic message.
- settings passkey + login passkey flows both use describePasskeyError
  now, so origin mismatches and previous-registration collisions show
  actionable copy.
- 5 new passkey.* i18n keys in EN + DE.

Bug report (#73.5):
- New GET /api/bugreport/status reports {configured, hasToken, hasRepo,
  isAdmin}.
- /bugreport page now queries status first. When not configured it shows
  a notice (different copy for admins vs. regular users) with a direct
  link to the admin panel. Form is hidden, not silently broken.
- When POST /api/bugreport can't create issues it returns 503 instead
  of 500 with the same user-facing message.
- 4 new bugreport.* i18n keys.

Global error UX:
- src/components/error-details.tsx — reusable panel with retry + copy
  diagnostics (message, digest, stack (trimmed), URL, user-agent,
  request-id, locale, timestamp) + report-issue link. Clipboard fallback
  prompt covers insecure-context cases.
- src/app/error.tsx uses it.
- src/app/global-error.tsx is provider-free (no i18n dependency) since
  it replaces the root layout on catastrophic failures — hardcoded
  English + Dracula palette so it still looks like HealthLog.
- 2 new errorBoundary.* keys and 4 common.* keys (copy, copyDetails,
  retry, reportIssue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(query-keys): collapse repeated invalidation blocks into invalidateKeys helper

Eleven mutation handlers repeated `Promise.all(keys.map(k => invalidate({queryKey: k})))`
verbatim. Adds `invalidateKeys(qc, keys)` to the existing query-keys factory and
adopts it everywhere, including replacing the local `invalidateMedicationRelated`
helper that medication-form.tsx had grown. Also fixes the import-sandwich that
helper introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: small simplifications in i18n/UX changes

- passkey-errors: drop the typeof-name dance now that error.name is already string after the instanceof Error narrow
- doctor-report-pdf: replace nested gender ternary with a small lookup map; clarify unitFor's null-as-translation-marker convention
- dashboard page: route the analytics queries through the new queryKeys factory so future invalidation stays in one place
- health-chart: add missing fmt dependency to trendInfo useMemo (introduced when formatSigned was switched to the locale-aware formatter)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ai): capture upstream response body in OpenAI errors

A production insights burst at 12:19 UTC failed with "OpenAI request
failed (500)". The 500 was the upstream OpenAI HTTP status (confirmed
via logs), but we had no way to tell from logs whether it was an
OpenAI incident, a model-not-found, or a response_format rejection
because the response body was thrown away.

Now the error message includes the first 500 chars of the response body
and structured fields (httpStatus, upstream, model) that Wide Event
logging can attach. No behavior change for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): address code-review + security findings

Review round produced five real issues — fix them all, plus three nits
that were cheap enough to bundle.

Must-fix:
- security: error-details.tsx + global-error.tsx were copying
  window.location.href into the clipboard, which on OAuth callback
  routes (/api/withings/*, password reset) would leak code/state
  tokens into GitHub issues. Strip to pathname + searchParamCount.
- fe: query-keys.invalidateKeys now uses Promise.allSettled, so a
  transient invalidate failure doesn't abort the chain and no
  unhandled rejection leaks from void invalidateKeys(...) call sites.
- fe: resolveInitialLocale in app/layout.tsx wraps cookies()/headers()
  in try/catch — a dynamic-server error in the root layout now
  degrades to the "en" default instead of crashing into global-error.
- be: reminder-worker + reminder-check parseTimeToMinutes normalize
  "24:00" → "00:00" (some ICU builds emit that for midnight under
  en-GB). Prevents a 1440-minute comparison against schedule windows.
- security: openai-client error body excerpt redacts sk-* / Bearer
  tokens before logging, and moves the excerpt off Error.message into
  a structured field (bodyExcerpt) so log transports can filter it.
- i18n: /api/withings/connect German error message → English.
- i18n: /insights/page.tsx hardcoded "Fehler beim Laden" → t(...)

Hardening:
- /api/bugreport/status collapses to {configured, isAdmin} and adds
  a 30/min rate limit per user. Client interface updated.
- i18n/context.tsx: I18nProvider's initializer now reads the cookie
  (matches SSR) before falling back to localStorage or browser
  detection — eliminates the locale-flash hydration class.
- format.ts legacy helpers also read cookie first, matching SSR.

Tests:
- new src/lib/__tests__/format-locale.test.ts — 9 cases covering
  makeFormatters, parseLocaleFromAcceptLanguage, resolveIntlLocale
- new passkey-errors.test.ts — 8 cases covering every DOMException
  name and SimpleWebAuthn code we map
- new query-keys.test.ts — bundles + invalidateKeys allSettled
  semantics (incl. partial-failure test)

118 → 142 tests. All pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Marc-André Bombeck <marc@bombeck.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:15:43 +02:00
Marc-André BombeckandClaude Opus 4.6 dc8942a220 release: v1.1.0 — AI Insights Overhaul
- ChatGPT proxy integration (no API billing)
- 7 medical insight prompts (ESC/ESH, WHO, DGE, DEGAM)
- 10+ new computed health metrics (RPP, seasonal BP, body composition, sleep/activity)
- 5 cross-metric correlations
- New InsightStatusCard + InsightAdvisorCard UI components
- Provider abstraction with automatic failover
- 3 security audits applied
- Rate limit restored to 2/hour

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:55:03 +02:00
Marc-André BombeckandClaude Opus 4.6 9932f88666 refactor(ui): simplify and polish insight card components
Replace hardcoded hex color values with Dracula CSS var tokens
(text-dracula-purple, text-dracula-green, etc.) for theme consistency.
Add prefers-reduced-motion support for insight animations, aria-expanded
on the data quality toggle, and extract nested ternary into lookup map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:37:47 +02:00
Marc-André BombeckandClaude Opus 4.6 a4c8f28ecb feat(ui): integrate InsightStatusCard into insights page
Replace 7 inline status text sections (general, blood pressure, weight,
pulse, mood, medication compliance, BMI) with the new InsightStatusCard
component. This adds consistent Card-based rendering with loading states,
provider detection, cache indicators, and timestamps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:34:27 +02:00
Marc-André BombeckandClaude Opus 4.6 eaca8674f5 feat(ui): add premium InsightAdvisorCard and InsightStatusCard components
New insight card components for the health advisor feature:
- InsightAdvisorCard: full structured display with primary recommendation,
  findings, correlations, recommendations, and collapsible data quality
- InsightStatusCard: compact per-metric status card with classification indicator
- Subtle fade-in animation for insight cards in globals.css

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:30:53 +02:00
Marc-André Bombeck c730845775 chore: temporarily increase insight rate limit to 10/h for testing 2026-04-06 13:23:52 +02:00
Marc-André BombeckandClaude Opus 4.6 3bb3837938 feat(insights): comprehensive upgrade — RPP, seasonal BP, body composition, sleep/activity, fitness ladder, clinical milestones
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:56:12 +02:00
Marc-André BombeckandClaude Opus 4.6 7b5fabe46c fix(insights): QA fixes — paired BP targets, stdDev, pulse pressure, personal tone, schema enrichment
- Fix pctInTarget to use paired-reading logic (both sys AND dia must be in range simultaneously)
- Add sdSys30/sdDia30 (BP standard deviation) and pulsePressure30 (arterial stiffness marker)
- Add mood scale descriptor for AI context
- Extend medication data with compliance90, 30-day streak window, and category field
- Rewrite base system prompt: personal advisor tone, second person, positive-first, "One Thing" pattern
- Add conditional correlation instructions to all domain prompts (only mention when |r| > 0.4)
- Enrich InsightResult schema with insightType, classificationLabel, primaryRecommendation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:48:28 +02:00
Marc-André BombeckandClaude Opus 4.6 64c72d241c feat(insights): enrich features with correlations, mood, historical comparisons and improve prompts
Add 90-day averages, all-time stats, mood data, cross-metric Pearson correlations,
historical comparisons (7d vs previous 30d baseline), and age/gender context to the
AI feature extraction. Update all prompts with correlation analysis instructions,
clinical significance thresholds, and temporal layer comparisons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:37:39 +02:00
Marc-André BombeckandClaude Opus 4.6 360a11523e feat(ui): replace API key input with ChatGPT OAuth connect button
Settings now shows "Mit ChatGPT verbinden" button instead of API key
input field. Shows connection status, connected-since date, and
disconnect option. Handles OAuth callback query params with toast.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:56:35 +02:00
Marc-André BombeckandClaude Opus 4.6 d3149170cd security: fix rate limiting, error message leaks, decrypt crash, key preview
- H1: Add checkRateLimit to OAuth authorize/callback/disconnect + admin AI settings
- H2: Fix decrypt("") crash in provider.ts onTokenRefresh by using decrypt() directly
- H3: Truncate error messages in codex-oauth, codex-client, openai-client to prevent leaking upstream response bodies
- M5: Fix keyPreview to show last 4 chars of decrypted key instead of ciphertext prefix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:47:05 +02:00
Marc-André BombeckandClaude Opus 4.6 50911ad801 chore: cleanup legacy OpenAI references, update CSP, verify build
- Add https://chatgpt.com to connect-src CSP directive for Codex OAuth token exchange
- Fix eslint no-explicit-any errors in codex-client.ts and provider.test.ts
- Fix unused variable warnings in authorize route
- All 118 tests pass, build succeeds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 5a71f9b36a refactor(insights): use provider abstraction in all 7 insight generators
Replace direct OpenAI API calls with resolveProvider() in all insight
generators (general, blood-pressure, weight, pulse, bmi, medication-
compliance, mood). Each generator now uses domain-specific prompts from
@/lib/ai/prompts/ and the AIProvider.generateCompletion() interface.
hasKey -> hasProvider in return signatures and all UI consumers.
Remove openaiKeyEncrypted references from data reset routes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 cd8508b6ad feat(prompts): add medical insight prompts with ESC/ESH/WHO/DGE guidelines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 249a2c74ba refactor(insights): use provider abstraction in settings and generate routes
Replace direct OpenAI API calls with resolveProvider() abstraction.
Settings route now returns codexStatus/codexConnectedAt/hasAdminKey
instead of hasKey. Generate route validates AI response with
insightResultSchema and logs providerType/model. Also fixes
AppSettings lookup id from 1 to "singleton" in provider.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 fde3a0941a feat(oauth): add Codex OAuth authorize/callback/disconnect routes and admin AI settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 401e78e01c feat(ai): add provider resolution with codex → admin-key → none hierarchy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 0f2d258f26 feat(ai): add Codex client with auto-refresh on 401
TDD implementation of CodexClient that calls the Codex responses
endpoint with OAuth Bearer token and automatically retries once
after refreshing the token on 401 responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 80ad0b8851 feat(ai): add Codex OAuth PKCE utilities and token management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 88db398aca feat(ai): add OpenAI client with custom base URL support
TDD implementation of OpenAIClient that wraps the OpenAI chat completions
API with configurable base URL (supports OpenRouter and other compatible APIs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André BombeckandClaude Opus 4.6 f18ded4860 feat(ai): add AI provider types and InsightResult Zod schema
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:42:46 +02:00
Marc-André Bombeck 0c0c345e82 i18n: migrate from German-first to English-first default locale
Switch defaultLocale from "de" to "en" and make English the code-level
language throughout. All ~145 hardcoded German strings translated:

- API error messages (52 routes) now return English
- Zod validation messages in English
- Classification/analytics functions return English categories
- Notification type labels in English
- Component strings use i18n t() calls (heatmap, mood chart,
  medication form, intake history, password strength, insights card)
- ~50 new i18n keys in both de.json and en.json

Route migration:
- /stimmung → /mood, /zielwerte → /targets (new pages)
- 301 redirects in proxy.ts for legacy URLs
- Sidebar + bottom nav updated

Bump version to 1.0.1.
2026-03-12 21:31:48 +01:00
Marc-André Bombeck 38309932f3 Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.
2026-03-10 20:14:33 +01:00