mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fd1464867d039b528db3fb00a85322f2b56dec5c
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
85a343a68c |
feat(documents): assist, summary, content-index endpoints + search union
P2-1-T2/T3/T4/T5/T6.
- assist.ts + [id]/suggest: one provider call proposes a { title, kind,
documentDate } filing draft over the stored original or browser-OCR text.
Runs the full extract gauntlet (module → provider → consent → rate-limit →
budget) but writes nothing — never touches the fact rail or status.
- describe.ts + [id]/summary?mode=summary|text: session-only summary or raw
transcribed text. Descriptive only, no diagnosis; persists only the budget
ledger + audit.
- content-index.ts + [id]/index: normalise → tokenise (de-accent, stopword
drop, whole-word) → HMAC-SHA256 under an HKDF-derived subkey → store the
token array plus AES-256-GCM ciphertext of the text. Vision path transcribes
via the provider (consent + budget); text path indexes posted local-OCR text
(no egress). Gated on the existing AI consent — no new per-user toggle.
- crypto.deriveSubkey: HKDF-SHA256 purpose-bound subkey off the active key.
- list search now unions title/filename ILIKE with a GIN-accelerated
whole-word content-token match; DTO gains hasContentIndex.
- usage gains assistAvailable + contentIndex { enabled, indexedCount,
totalCount }; detail gains hasContentIndex.
|
||
|
|
bd64b78e5d |
feat(documents): upload policy layer and binary content codec
The policy module (src/lib/documents/upload-policy.ts) is the one source of truth for what the vault accepts and how it serves: Class A (PDF/JPEG/PNG/ WebP/GIF) renders inline; Class B (Office OOXML+CFB, text/CSV/Markdown/RTF, TIFF, HEIC/HEIF, XML/JSON) is stored verbatim and leaves the origin only as an opaque attachment download; executables, HTML/XHTML/SVG, and generic archives are denied. Detection is magic-bytes first — the wire Content-Type is never trusted; containers (ZIP → OOXML, CFB → legacy Office) and extension-only text formats gate on the filename plus a no-NUL heuristic. resolveDocumentLimits() reads the admin-tunable per-file cap (clamped to the hard 100 MiB ceiling — single-shot GCM and bounded in-memory reads are load-bearing past that) and the quota (user override ?? instance default). crypto.ts gains the binary2 codec: encryptBytes/decryptBytes do AES-256-GCM over raw bytes with a versioned key-id header, skipping the base64 detour (−33 % at rest, one fewer copy). Same fail-closed discipline as the string codec. The store dispatches per row on the explicit contentCodec column: new uploads write binary2, pre-existing base64v1 rows keep decoding byte-identically, and an unknown codec throws instead of guessing. Exhaustive unit coverage: every accept class from real magic-byte fixtures, every deny case (incl. the PDF-with-HTML-head, SVG-as-image, and ZIP-renamed-docx polyglots), header-matrix source of truth, limit clamping, codec round-trips both ways, crossed-codec and unknown-codec fail-closed. Tasks: W1-T3, W1-T4. |
||
|
|
e5f9fb7c4a |
feat(admin): show encryption coverage and rotation progress
Add a read-only admin Encryption section that surfaces what the rotation tooling computes: per encrypted-column rows-per-key-id, legacy-key row counts, and overall rotation progress, plus a 'safe to drop the legacy key' state once every column sits on the active key. GET /api/admin/encryption/status drives it and returns only key ids and counts — never key material. A shared scan over the canonical encrypted- column registry backs both this view and the rotation run. Cookie-only admin. |
||
|
|
288569297f |
fix(crypto): fail closed on weak dev key outside development/test
The dev-only SHA-256 key padding was gated on the absence of a production marker, so a staging or preview container with NODE_ENV unset silently derived a weak key the operator never recorded — rotating to a real key later orphaned those rows (decrypt is fail-closed). Gate the padding on an explicit NODE_ENV of development or test; every other case, including unset NODE_ENV, throws the same hard error as in production. |
||
|
|
d7778c1085 |
feat(v1.4): multi-tenant prep — off-host backup, encryption-key versioning, worker/web split, native-Bearer UA gating (#135)
* feat(crypto): G2 — encryption-key versioning + rotation CLI
Adds backwards-compatible support for ENCRYPTION_KEYS (JSON map of
{ "id": "<key>" }) plus ENCRYPTION_ACTIVE_KEY_ID. Ciphertext format is now
"<keyId>.<base64payload>"; legacy bare-base64 ciphertext still decrypts
under the synthetic "v1" id derived from the existing ENCRYPTION_KEY.
scripts/rotate-encryption-key.ts re-encrypts every encrypted column on
User / WithingsConnection / AppSettings with the active key. Idempotent —
rows already on the active id are skipped.
Why: enables zero-downtime rotation when a key is suspected leaked,
without forcing every encrypted row through a single big-bang rewrite.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(auth): G4 — native-Bearer UA gating + rotating refresh tokens
Web callers (UA contains 'Mozilla/') keep the legacy 90d Bearer when they
explicitly opt in via X-Client-Type: native. Native callers (HealthLog-iOS,
HealthLog-iPad, n8n, Health-Connect, unrecognised UAs) now receive a 24h
access token plus a 60d rotating refresh token in the login response.
New /api/auth/refresh endpoint atomically validates + rotates the refresh
token (one-time-use), revokes the previously paired access token, and
audit-logs every refresh + revoke. Reuse of an already-consumed refresh
token revokes the entire family for the user/device — defence against a
stolen refresh token.
Migration 0025_refresh_tokens adds the table; the only DDL is CREATE
TABLE so it is forward-compatible. Down migration documented at
docs/ops/migrations/0025-down.sql.
Why: the V3 audit (still-V2-A) flagged that 90d tokens are too long for
native callers when refresh-token rotation is now table-stakes for mobile
apps. Web flows are unchanged so existing browser sessions keep working.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(infra): G3 — web/worker container split via HEALTHLOG_PROCESS_TYPE
Adds a tiny startup gate (src/lib/process-type.ts) so the same Docker image
can run as web-only, worker-only, or 'all' (default — single-container,
backwards compatible). assertSubsystemEnabled() refuses to boot the
reminder worker when HEALTHLOG_PROCESS_TYPE=web, preventing accidental
double-cron when an operator splits the deployment.
docker-compose.yml ships the optional app-worker service block commented
out so 'docker compose up' keeps working out of the box; uncommenting it
+ flipping app to HEALTHLOG_PROCESS_TYPE=web is the only change to scale
HTTP horizontally. Healthchecks are isolated per service so neither
startup deadlocks the other; both wait on db: service_healthy.
docs/self-hosting/scaling.md walks through when + how to split.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* feat(jobs): G1 — daily off-host encrypted backup to S3-compatible storage
New pg-boss queue 'data-backup-offhost' runs at 02:30 Europe/Berlin, dumps
each user's measurements + medications + intake events + mood entries,
encrypts the JSON under a SEPARATE key (BACKUP_ENCRYPTION_KEY, isolated
from the application ENCRYPTION_KEY), and uploads to any S3-compatible
target via @aws-sdk/client-s3.
Wire format documented in docs/ops/backup-restore.md:
magic "HLBK" || version 0x01 || iv(12) || authTag(16) || ciphertext
The worker prunes objects older than BACKUP_RETENTION_DAYS (default 30);
operators are encouraged to also configure a bucket-level lifecycle rule.
Admin endpoint POST /api/admin/backup/test issues a 1-byte PUT+GET
roundtrip and returns endpoint + bucket + per-call latency. Credentials
are never returned.
scripts/restore-backup.ts downloads + decrypts a single object so
operators can verify the chain end-to-end without standing up a separate
restore harness.
Why: GDPR Art. 32 + V3 audit "data durability" — without an off-host copy
the only line of defence against a Postgres volume loss is the in-DB
DataBackup model on the same host. Separate-key isolation means a leak of
the application encryption key does not also expose every user's archive.
Co-Authored-By: Marc-André Bombeck <noreply@anthropic.com>
* fix(multi-tenant): address review findings — rotation coverage, hard decrypt errors, web/worker gates
Multi-agent review on PR #135 caught several CRITICAL + HIGH issues that
would have shipped silent data corruption or weakened the threat model:
Encryption + rotation
- scripts/rotate-encryption-key.ts: stop selecting `bugReportTokenEncrypted`
(column doesn't exist in the schema; the script crashed on the AppSettings
step). Add the three encrypted columns it was missing —
NotificationChannel.config, PushSubscription.p256dh, PushSubscription.auth
— so rotating to a new key actually re-encrypts every encrypted row.
Without this, dropping the old key id from ENCRYPTION_KEYS would leave
notification channels and web-push subscriptions permanently
undecryptable.
- src/lib/crypto.ts: refuse to silently fall back to the active key when a
legacy bare-base64 row is decrypted but no v1 entry is configured. Throws
a clear "Restore ENCRYPTION_KEY (or add a v1 entry to ENCRYPTION_KEYS)
and run scripts/rotate-encryption-key.ts before removing it" so an
operator who pulled the v1 too early notices immediately instead of
hitting opaque GCM tag errors.
- docs/ops/encryption-key-rotation.md: full step-by-step + rollback note +
troubleshooting section. Spells out that rotation is a forward-only
operation and what to do if the rollback path is needed.
Web / worker split (G3)
- src/proxy.ts: refuse HTTP traffic with a 503 +
X-HealthLog-Process-Type: worker header when HEALTHLOG_PROCESS_TYPE=worker.
The worker container (which still runs `node server.js` for now) no
longer accepts requests, so a misrouted client gets a clear hint instead
of a partially-served response.
- src/instrumentation.ts: short-circuit before importing the reminder
worker when HEALTHLOG_PROCESS_TYPE=web — avoids the noisy
"[instrumentation] CRITICAL" stderr line we emitted on every web boot.
- /api/version added to PUBLIC_PATHS so the compose healthcheck doesn't
silently 302-redirect through the auth gate (the prior wget treated the
login page as a 200 success and would never notice an unhealthy backend).
Refresh-token rotation (G4)
- src/lib/auth/refresh-token.ts: reuse-detection now revokes ALL active
refresh tokens for the user, not only those scoped to the same deviceId.
An attacker who rotated without an X-Device-Id header would otherwise
keep their family alive on the no-device branch. Defence-in-depth.
- revokeRefreshToken() also revokes the paired access token so a leaked
access token cannot outlive the refresh-token sibling that the caller
just killed (logout-on-device).
Off-host backup (G1)
- src/lib/jobs/offhost-backup.ts: drop the worker-side DeleteObject path.
Retention is the bucket's lifecycle-policy job; the worker keeps a
PutObject + GetObject grant only. A compromised worker can no longer
wipe the backup history. Per-user errors are now surfaced as Wide
Event warnings + a structured failures[] array on the report so an
operator can tell which user failed and why.
- scripts/restore-backup.ts: schema-validate the decrypted payload before
writing to disk, plus an optional --user-id=<id> flag that refuses to
proceed unless the dump's userId matches. Prevents silent
cross-tenant restores when an operator points the script at the wrong
S3 key.
Idempotency
- src/lib/idempotency.ts: the leak-guard already blocked `hlk_`
(access-token prefix); also block `hlr_` so a future caller wrapping
the new /api/auth/refresh endpoint with withIdempotency() can't
accidentally cache a freshly-issued refresh token.
Migration / schema
- prisma/migrations/0025_refresh_tokens/migration.sql: wrap in a single
transaction; switch to CREATE { TABLE | INDEX } IF NOT EXISTS so a
partial-deploy retry is a no-op instead of "relation already exists";
guard the FK ADD with information_schema lookup for the same reason.
- Schema replaces the single-column `@@index([deviceId])` with a
composite `@@index([userId, deviceId])` (every lookup also filters on
userId; the old index would never have been used). Comment on
accessTokenHash corrected to HMAC-SHA-256.
Container / ops
- Dockerfile: install tzdata + ENV TZ=Europe/Berlin so the daily
pg-boss schedule (`30 2 * * *` Europe/Berlin) actually fires at 02:30
Berlin instead of silently falling back to UTC on the alpine base.
- docker-compose.yml: tighten the healthcheck to `wget --spider` (matches
the Dockerfile HEALTHCHECK style, doesn't pipe the body), pass through
BACKUP_RETENTION_DAYS so a custom retention env reaches the container.
- .env.example: document every new env var added in this PR
(HEALTHLOG_PROCESS_TYPE, ENCRYPTION_KEYS, ENCRYPTION_ACTIVE_KEY_ID,
BACKUP_S3_*, BACKUP_ENCRYPTION_KEY, BACKUP_RETENTION_DAYS) with
comments and the all-or-none semantics for the backup block.
491 tests still green, typecheck clean.
* chore: retrigger CI
---------
Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
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. |