mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
e6c709876dfd12356855c551ffefff3fca11621e
46
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7dab9cc5e9 |
feat(admin): judge every backup signal and push changes to admins
Every backup failure was a pull, on a page nobody opens until the day it matters: the weekly copy stopped for 46 days and showed an ordinary row, a failed monthly drill was on the failing-jobs card three days in thirty (job-failures.ts reads 72 hours), and an off-host bucket that was never set up showed only on the backups page. One readiness model (src/lib/recovery/readiness-model.ts) judges six signals as ok, attention or failing, with a reason code, the time it is about and where to fix it: - database dump: failing once the newest good dump is older than 36 h while dumps are on, or when it can be kept nowhere; read through a provider that reports "not available" until the dump lands; - weekly account copies: worst account decides, attention past 9 days, failing past 14; an account without a copy ages from its creation; - off-host copies: attention when any account is two nights behind; - restore drill: failing until a later drill passes, with no time window; the drill now keeps its verdict in restore_drill_outcomes; - off-host setup: attention 7 days after the first admin exists; - recovery kit: attention when a needed key is not confirmed, by id, 7 days after setup or as soon as a confirmed kit is out of date. A "Recovery readiness" card shows them on the admin overview and the backups page. An hourly job (recovery-readiness, :33) sends each change to every admin through the localised admin path, under a new admin-only BACKUP_HEALTH event that the channel preferences route: once when a signal turns to attention or failing, once a day while it stays failing, once when it recovers. Each notice is claimed with a compare-and-set before it is sent, so two workers never send the same one, and nothing is claimed while no admin exists. The backups page calls the weekly copy stale at the same instant as the model (past nine days rather than past ten). |
||
|
|
fea1d1e0b5 |
chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment lines in 1,308 files lose the work-item identifier at their head. Release versions and issue references stay; nothing outside comment text changes, which the script checks per file against a comment-free printout. healthlog/no-planning-tags is enabled at error level with this commit, so the tree it lands on is clean. |
||
|
|
3cf67d05f9 |
fix(ui): let the read-failure primitives decide by the error
About forty compact rows and seventy cards still offered a retry on a spent read budget and drew an error card on a refusal, while a third primitive (ReadFailureRow) did the right thing at eight places. QueryErrorRow and QueryErrorCard now take the query's `error` and read it through readFailureKind, which moves to src/lib/api next to ApiError: a refusal (403) draws the surface's refused state or nothing, a spent budget (429) says so without a retry, an ended session (401) says so and offers sign-in instead of going blank, and only a real failure offers the retry. ReadFailureRow is gone and every site passes its error; a guard test fails on a site that does not. The coach read strip keeps its card in the error state and holds one body height across loading, failure and a one-line read. |
||
|
|
9f17cef8e4 | feat(admin): pulse day-mean dry run, apply step and admin card | ||
|
|
fd44a8381b |
Merge branch 'v143/ai' into release/v1.43.0
# Conflicts: # docs/api/openapi.yaml # messages/de.json # messages/en.json # messages/es.json # messages/fr.json # messages/it.json # messages/ko.json # messages/pl.json # src/__tests__/contract-stub-inventory.test.ts |
||
|
|
ec67663def |
feat(settings): choose the AI usage level, and set its bounds as an operator
Settings, AI gains one card with the three levels, each with what it does and the server's estimate of tokens per day, labelled as one. Levels above the operator's ceiling are shown disabled with the reason; a capped choice is named and kept; a stored choice can return to the server default. The automatic document read says when the frugal level rests it. Admin, Coach gains the ceiling, the default and the sync gap, and the provider readout the accounts per level. Strings in all seven languages. The admin card's save confirms with a success toast like its neighbour assistant-section; it is a settings write without counts, so it is pinned in the success-affordance guard at one. Refs #1217 |
||
|
|
f7386742b4 | feat(web): show the error id on server failures and add the support info and report surfaces | ||
|
|
a0c5ab6591 |
Hold data-shaped page bodies until their reads settle
Several pages stacked cards whose final height is the data's: the illness day timeline, the measurement list, the insights arrange list, the system status cards, the score notices that land with the slower report. Each resolving on its own pushed the rest down the page, for a CLS of 0.11 to 0.18 at desktop width. RevealWhenSettled mounts the body at once but keeps it out of layout until no query is in flight (two frames running, two seconds at most), with a placeholder in its place. The chart loading shells were also ~100 px shorter than the card they became: they now reserve the day-dot row, its caption and the collapsed data-table disclosure, and the range-tab row at its 44 px height. |
||
|
|
12e422c25e |
Lighten the client shell and finish four client surfaces
- Load each settings section on its own: the section route imported all twenty-one statically. A preload starts the chunk beside the record gate's read, and the loading state paints the settings card shell. /settings/[section] goes from 611 to 403 KB gz. - Take Zod out of the shell every route shares: the account-access parse run on each boot is plain TypeScript now, held to the published schema by an equivalence test over good and broken blocks. The canonical unit table moves to its own module, so the display transform no longer pulls Zod in either. Layout entry 199 to 136 KB gz; /insights 369 to 304, /medications 428 to 365, /labs 400 to 337. - Open a saved Coach conversation at its last answer: an instant jump instead of the smooth scroll that lost the end, then follow late layout (charts, tables) for a few seconds until the reader scrolls. - Show the document archive as a month grid of preview tiles, two across on a phone and three or four on a desktop, with the month headings kept. - Add the admin card for the measurement table maintenance: a status read on the maintenance route, one calm confirmation, and the run's state while it is queued, running, finished or stopped. - Re-base the bundle budget: /insights at 304, /settings/[section] watched at 403, aggregate cap 4335 with the reason written down. |
||
|
|
f57b6bf979 |
fix(admin): one card that fails to render no longer takes the page down
An admin page stacks independent cards, and a card that threw while rendering replaced the whole page, so a field missing from the off-host card's payload hid the snapshot table beside it. Every card in an admin section now sits in its own boundary and fails as an error card with a retry that remounts it; the rest of the page keeps working. |
||
|
|
1c10bbbde8 |
feat(admin): say per account how fresh the off-host copy is
The nightly off-host job reports counts — uploaded, failed, total — and a count cannot name the account it is failing for. A cohort where one object has been refused every night for a month reads as "99 uploaded, 1 failed" each time, which looks like weather rather than like an account with no disaster-recovery copy at all. The worker now writes a ledger row when an object lands (account, instant, size), and Admin → Backups gains a second card that rates every account against the nightly schedule: fresh inside one run, due when one run produced nothing, stale when two did not, never when no object has ever landed. On a host without BACKUP_S3_* the card says off-host backup is not configured and stops there. The verdicts are computed from the ledger, never from a bucket listing: the worker's grant is deliberately PutObject + GetObject, and an admin page must not put the operator's credentials on its render path to state a number the database already holds. |
||
|
|
6c4682acfe |
Give the operator a provider-health readout
The per-user retry ledger has recorded every AI delivery outcome for a long time, but no surface ever read it for the operator: a central provider could fail on every call for weeks and the admin console said nothing. The AI settings section now carries a health card, backed by a new admin-only endpoint that folds the ledger into one row per provider type: users tracked, users currently failing, the worst uninterrupted failure run, and the last success and failure instants. Operator-managed types sort first because their failure takes the shared fallback down for everyone whose chain relies on it. Counts and timestamps only; the response never names a user. |
||
|
|
ba42354733 |
fix(admin): replace hand-rolled read-error boxes with the shared primitive
Eight admin surfaces rendered a failed read as their own destructive- tinted box, and most offered no way to recover. Sweep them onto the shared QueryErrorRow with retry so the console uses one error form (§13). The statically renderable sections were watched fail first against the hand-rolled boxes. |
||
|
|
330f60115a |
refactor(admin): the console speaks the Settings vocabulary without an accent
Twenty-six of twenty-eight admin files already used the Settings card and header. What made the console read as its own system was everything on top of that: three hand-rolled cards, a header slot that meant five different things, and a heading the frame took but never painted. - `SectionFrame` drops `title` and `subtitle`. Seventeen call sites passed both into a component that has rendered neither since v1.18.6.1 — the shell reads the same keys itself. A prop contract nothing honours is a promise the next reader believes. - The invite card, the four host-metric headers, and the `/admin` welcome card stop hand-rolling `bg-card border rounded-xl` shells with their own padding steps and their own heading weights. `text-sm` as a card title on the first card of a page, directly above a correct one, was the first thing the eye landed on. - The header status slot carries status. The backups run, the invite create, the danger-zone wipe, and the user filters come out of it; the app-log refresh stays but reads as the secondary it is. - Loading states paint their card and header. Encryption returned a bare spinner row and then a card, so the section jumped when the status landed — the fix `coach-feedback-section.tsx` already carried, applied where it was missing. Same for the notification-status card. - One label/value tile. `StatusItem`, the encryption section's private `Stat`, the user-management inline panels and the backups upload box were four fills and three paddings for one role. - The overview stopped printing the running version, build SHA and build time twice, thirty pixels apart. The version tile owns build identity — it is the one that checks for a newer release; the snapshot owns what is running. - `ai-server-key` had three paragraphs stacked in a muted `text-xs` slot; one sentence stays there and the two that carry information move into the body as foreground. The central-codex consent copy moves the same way, and its disconnect is `variant="destructive"` — which `ConfirmButton` refused to accept, so every destructive confirm in the tree had been hand-tinting an outline instead. - Leftovers: the `-mt-2` correcting the card's own rhythm, two alpha hovers on text, `text-[10px]` badges, the redundant `space-y-6` wrapper, the empty state repeating the header's CTA, and the `mt-6` a card added inside a frame that already spaces its children. |
||
|
|
d7e616eaa8 |
feat(ai): add operator-shared central Codex connection with per-user opt-in
Introduce an operator-level ChatGPT-subscription (Codex) credential that the operator connects once and shares server-wide, plus a per-user switch to route AI egress through it. - Schema (migration 0235): three encrypted columns on AppSettings for the operator's central Codex access/refresh tokens and the ChatGPT-Account-ID claim, plus expiry/connected-at/status; a per-user useCentralCodex flag (default false). Register the new encrypted columns in the rotation registry and script. - Resolution: add the admin-codex chain type; append it to a user's resolved chain only when they opted in AND the operator connected it. Tokens refresh server-side against the singleton row (CAS-style), same primitives as the per-user Codex path. - Budget: bill an admin-codex primary against the operator cost cap, not the user plan — it drains the operator's account. - Consent: admin-codex is server-managed external egress, gated by the existing consent machinery (self-snapshot and document surfaces alike). No existing consent path is weakened. - Document order: rank admin-codex with the subscription tier (last), so the document class stays local-first. - UI: admin device-code connect/disconnect card (cookie-only admin routes) with honest copy on account pooling, shared rate limits, and training-by-default; a per-user opt-in switch shown only once the operator has connected it, off by default, behind a one-time honesty confirm. - i18n across all six locales; unit tests for the chain type, budget cap, consent gate, vision capability, and document order; a mocked e2e for both the admin connect surface and the per-user switch. The central credential's tokens are encrypted at rest and never returned to a non-admin API; the opt-in only takes effect once the operator has connected the shared account. |
||
|
|
b271a39fe4 |
merge: remove the bug-report integration for v1.23
# Conflicts: # messages/de.json # messages/en.json # messages/es.json # messages/fr.json # messages/it.json # messages/pl.json # src/app/admin/[section]/renderer.tsx |
||
|
|
de33f1ad4c |
chore: remove the bug-report integration
Drop the in-app bug-report surface end to end: the user submission page, the feedback intake and admin inbox, the optional GitHub-issue escalation, the operator config, and the dedicated achievement badges. - delete the /bugreport page and the bugreport / feedback / admin-feedback API routes - drop the Feedback model, its enums, and the AppSettings GitHub-issue and enable columns (migration 0205) - remove the admin Feedback section, the Bug Report integration card, and the system-status row - drop the bug-report nav entry and the app-settings provider that only carried its flag - remove the bug-hunter achievement badges and their metric - prune the related i18n keys across every locale and the encrypted-column and key-rotation registries |
||
|
|
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. |
||
|
|
f67542d29e |
fix(settings): align section nav with the first card via the grid, not a spacer
The settings and admin shells reserved the page-heading height in the left nav with a hidden replica block. The replica wrapped differently in the 220px nav column than the real heading in the wide content column, so the first nav item sat too low and drifted across locales. Hoist the heading into the shell and place it in its own grid row spanning only the content column: row 1 holds the heading, row 2 holds the nav and the cards. The nav's first item and the first card now start the same grid row, so they align by construction regardless of how the title or subtitle wraps. Drop the spacer component. |
||
|
|
33d7d103da |
feat(settings): standardize settings and admin section framing
Introduce a shared SettingsSectionFrame (the settings twin of the admin renderer's SectionFrame) that emits the canonical visible heading + subheading, and route all 19 settings sections through it. Every settings page now shows a real h1 + subtitle in the reference Übersicht style instead of an invisible sr-only title; the section bodies drop their own headings and off-style intro paragraphs (the explanatory copy folds into the frame subtitle). - Route the admin About page through SectionFrame so it carries the same visible heading as every other admin section. - Rename the AI settings section from KI-Auswertung to KI-Anbieter across six locales: the page configures the provider and key, not an analysis. - Add a heading-block spacer to the settings and admin sidebars so the first menu item lines up with the top of the first card. - Remove the always-on core-domains card from Module: a tracking area that can't be turned off doesn't need listing. - Move the proactive-coach control out of Benachrichtigungen into Coach. - Move admin module availability out of the Coach section into its own page: it gates every module, not just the coach. - Give the Layout Insights editor a proper Insights heading. Adds settings.sections.<slug>.subtitle and the new admin keys across all six locales; removes the now-unused module core-domain strings. |
||
|
|
33fec28b29 |
feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the tour narrowed to that module's card in place, plus additive data-tour anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/ insights/coach/integrations/export/achievements) so the cross-page walk has a real target on each page. Consolidate the full-tour replay onto a single Settings -> Advanced card, relabel it, and drop the duplicate from About. The replay reset clears the stored progress so it restarts cleanly. |
||
|
|
3a29387393 |
refactor(admin): consolidate assistant + feedback-quality into one Coach section
The ai-quality, assistant, and coach-feedback admin sections were three separate navigation entries for one coherent coach-quality concern. Fold them into a single "Coach" section that stacks the assistant surface toggles, the server-key + module-availability operator config, and both feedback-quality tables (Coach + Insights). Drop the three old slugs and their section copy; add admin.section.coach.* across all six locales. |
||
|
|
4a1971c0dd |
feat(admin): module availability matrix
Add an operator-side toggle matrix to the admin assistant section that flips each toggleable module on or off server-wide via PATCH /api/admin/settings/module-availability. Mirrors the assistant-flags matrix: one neutral toggle per module, optimistic flip with a revert on failure, and an authMe invalidation so the resolved module map re-gates the session immediately. New admin.modules.* labels across all six locales; core domains never appear. |
||
|
|
40f7196c20 |
feat(medications,coach,admin,perf): era corrections, a truthful weekly status and one card language everywhere
The dashboard snapshot fires the moment login succeeds instead of waiting for the page chunk (start ~670 to ~200 ms throttled). Guided follow-ups are personalised from the user's snapshot and an answer gets the coach's streamed reaction before the next question. Archived schedule eras can be corrected through a superseding revision that keeps the original as an audit trail, era writes serialise per medication, and duplicate dose times collapse in validation. A rolling medication whose next dose is tomorrow can no longer read as heavily overdue — the card status derives strictly from the served next-due — and the adherence bars keep a fixed geometry in every state. The server-wide assistant key gains its admin card, the one duplicated coach preference collapses onto a single owner, twenty-seven admin and settings card headers move onto the shared pattern with sixty containers normalised, and the remaining hydration warnings are gone. |
||
|
|
5b75184649 |
feat(coach,admin): finish the self-context and invite surfaces end to end
Invites move into their own admin section with a real ledger: status badges, expiry, uses, a redemption history of who joined when (new redemption table, revocation is now soft so history survives), and an honest one-time link/QR moment. The assistant self-context grows structured fields for conditions, allergies and focus — encrypted like the free text — merges age and gender from the profile into one fenced prompt block, and asks up to three targeted follow-up questions that surface as tappable chips above the coach composer. A fourth nudge trigger gently suggests a check-up when the self-context is stale and the coach is actually in use. |
||
|
|
51dd614199 |
feat(coach,auth): self-context for the assistant, invite links, proactive nudges
Users can now tell the assistant about themselves in an encrypted free-text profile that feeds the coach and briefing prompts as clearly fenced context, with the coach allowed one targeted follow-up question when the picture is incomplete. Operators on closed registrations can mint expiring invite links with a QR code from the admin area; a valid invite opens registration exactly as many times as it allows and records who redeemed it. A nightly pass watches three deterministic signals — low adherence, blood pressure above its target band, a falling recovery trend — and sends at most one coach nudge per week through the existing notification channels, gated behind the same opt-outs as every other assistant surface. |
||
|
|
7b171cc5e3 |
feat(admin): surface the running version on the overview
Add a dedicated version tile to the admin overview showing the running version, short build SHA, and build timestamp, alongside a best-effort 'update available' hint computed against the latest GitHub release tag. The hint reuses the existing /api/version/check-updates proxy (server-side safeFetch past the CSP) so no new route or OpenAPI schema is introduced; the check is day-stale and never blocks the render. Cookie-admin only by virtue of the admin layout. |
||
|
|
0e2a99f8e7 |
v1.4.36 — perf, charts, AI payload trim, UX punch
Heavy-aggregate skip on the rollup-fresh + fully-covered path, daily chart fetches opt into rollup buckets via source=rollup, Insights extractFeatures swap drops the prompt payload from double-digit MB to the low hundreds of kB on power-user accounts, page-level isLoading gate replaced with per-section early-skeleton paint, trends-row chart slot pinned for CLS, IntakeHistoryListV2 restored on the medication detail page, About card folded into the Admin Console with an auto-checking update badge, cumulative metric tiles read the day's running sum, IP-whois fallback surfaces city + country when the ASN lookup misses. Migration 0068 adds an additive users.insights_exclude_metrics column. Unit suite 4354 passing; typecheck + lint clean. |
||
|
|
5ea813e20c |
chore(release): v1.4.31
Operator-side assistant feature-flag matrix, /insights tab-strip blocking fix on mobile, and the GHCR-propagation-race Coolify auto-deploy fix. Full notes in CHANGELOG. |
||
|
|
05c7f1474d |
feat(coach): per-message thumbs feedback + admin aggregate view
The v1.4.22 prose rewrite shipped without a way for users to signal
whether the new tone is landing. v1.4.23 H7 closes that loop with a
light-touch thumbs row + an admin first-week dashboard.
Schema: extend `RecommendationFeedback` with a polymorphic
`target_type` column (default "recommendation" so legacy rows stay
representative) plus a nullable `reason` field. New index on
(target_type, created_at) so the aggregator can scan per-surface
buckets without a full sweep. Migration `0040_recommendation_feedback_target_type`.
Route: POST /api/insights/chat/messages/:id/feedback. Body
{ rating: "helpful" | "unhelpful", reason?: string }. Verifies
ownership (404 on cross-account messages — no existence leak),
decrypts the message body for the dedup key, and writes the row with
target_type="coach" + a metric-source string encoding the user's
active prefs (`coach:tone=warm:verbosity=default`). Per-user 60/h rate
limit. 409 on a P2002 (already_rated) so a double-click is a no-op.
Aggregator: new `buildCoachFeedbackBuckets()` slices Coach rows by
(promptVersion, tone, verbosity). The summary blob the worker writes
to AppSettings.adminAiInsightsFeedbackSummary now carries both
`buckets` (pre-existing) and `coachBuckets` (H7 addition). Targets
the same singleton row so no second worker handler is needed.
Admin view: `/admin/coach-feedback` (new section slug) renders the
Coach buckets as a small DataTable (prompt version × tone × verbosity
× helpful × not-helpful × n × helpful-rate). Helpful-rate column
tinted via Dracula tokens (green ≥80%, yellow ≥50%, orange below).
First useful question this answers: "Is the v1.4.22 prose rewrite
landing well, or did the warm tone overshoot?" If the helpful-rate
for PROMPT_VERSION 4.23.x drops below 50% within the first 100
ratings, v1.4.24 walks the persona back.
UI: `<ThumbsUp>` / `<ThumbsDown>` buttons (lucide icons, no emoji)
under each persisted assistant bubble. Streaming bubbles + refusals
+ error bubbles skip the row so users can't rate before the message
lands on disk. Localised "Thanks for the signal." after submit.
OpenAPI: spec carries the new POST route + the request schema.
Coverage now includes the H4 + H7 routes — three additions over
v1.4.22 baseline.
Tests +9 unit (aggregator Coach-bucket slicing, admin section
render-paths) plus an integration round-trip for the route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
887928211b |
feat(admin): AI quality preview showing per-(severity x provider) helpful-rate
v1.4.16 phase B5e: ship the `/admin/ai-quality` route that surfaces
the daily aggregator's helpful-rate per (severity x metricSourceType
x provider x prompt-version) bucket. Renders as a compact 7-column
table with severity tinted by clinical-importance colour and the
helpful-rate column tinted green/yellow/orange by 0.8/0.5
thresholds.
New endpoint `GET /api/admin/ai-quality` (requireAdmin-gated) returns
the raw `AppSettings.adminAiInsightsFeedbackSummary` blob so the
v1.4.17 ratchet that consumes the same shape for prompt-tuning can
read what the worker writes without an envelope-reshape step.
Empty state ("no summary yet" / "buckets[] is empty") rendered as a
quiet message rather than a fake zero row so the operator can tell
"no feedback" apart from "all-zeros". Loading + error banners follow
the existing host-metrics pattern.
Section wiring:
- new `ai-quality` slug in ADMIN_SECTION_SLUGS (between
integrations and feedback so the IA flows General → Integrations
→ AI Quality → Feedback Inbox)
- new Sparkles icon entry in admin-shell
- new switch case in [section]/renderer.tsx
- EN+DE i18n keys: admin.section.ai-quality.{title,subtitle} +
admin.aiQuality.{title,loading,loadError,empty,windowLabel,
generatedAtLabel,col*}
Tests:
- 5 new ai-quality-section tests (loading, empty-null, empty-buckets,
table-render with multiple buckets, error)
- 1 admin-sections smoke test ensures the empty branch renders +
1 slug-list parity test
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4cc3d8d45e |
feat(admin): app-log preview surfaces last 1h of structured events with filter + JSON inspector
Wires the in-memory ring buffer (`src/lib/logging/in-memory-buffer.ts`) into `transports.emitEvent()` so every wide event that survives the sampler also lands in a 500-entry per-process buffer. Adds the `app-logs` admin section slug, the renderer case, and the route entry exports needed for the new section component to mount. The buffer is volatile and per-process — a worker-only event won't show up in the web buffer, and a restart drops everything. The section header copy spells that out so admins know to keep Loki configured for durable diagnostics. Egress through `/api/admin/app-logs` runs every event through `redactSecrets()` so a stray Bearer / hlk_ / sk- token in an `error.message` never leaks to the admin UI; storage stays raw to keep the diagnostic value when shipped to Loki. The companion buffer + transports tests live under `src/lib/logging/__tests__/in-memory-buffer.test.ts` and `transports-buffer.test.ts` (committed separately as part of the buffer scaffolding). The route-level tests in `src/app/api/admin/app-logs/__tests__/route.test.ts` cover newest- first ordering, traceId / level / action filters, the `limit` cap, secret redaction on egress, and the Zod-rejected invalid `level` case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a967895808 |
feat(admin): replace overview section-grid with audit-log preview + system snapshot
The `/admin` overview rendered a six-card status grid plus a quick-jump list of every admin section. Both duplicated navigation that already lives in the sidebar (and the global nav after Phase 4b), so the landing page told the admin nothing they didn't already see two clicks away. Replace the duplicated nav with two at-a-glance panes that *only* the overview can show: - Welcome card greets the signed-in admin by username and confirms the admin context (so the role indicator the shell shows is reinforced in the page body, not just the chrome). - `<SystemStatusSummary>` reuses the existing `useSystemStatus()` hook + `/api/version` to print app version, DB up/down, worker state, process start time, build SHA, and built-at. Same data source as `<SystemStatusSection>` so we don't double-fetch. - `<RecentAuditPreview>` lists the latest 10 audit entries from `/api/admin/audit-log?limit=10&filter=auth` with a "View all" link to `/admin/login-overview`. Reuses the same auth-action label map the full section uses. The previous `<StatusCardGrid>` component is now unused — removed entirely. The test file kept its name (`status-card-grid.test.tsx`) so git history follows the rename, but it now asserts the new overview composition: welcome card paints with the admin's username, snapshot heading present, audit preview links to `/admin/login-overview`, and the legacy "Maintenance"/"Monitoring" card titles + "Manage users" CTAs are *not* rendered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
88c8db1abc |
refactor(v1.5): apply simplify-review safe-to-apply suggestions
Drops dead `id` props and `scroll-mt-28` classes from admin section components (load-bearing in v1.4 monolithic admin, dead since the v1.5 phase-4b dynamic-route split). Also removes the unused `LEGACY_ANCHOR_TO_SLUG` map, the `admin-shell` re-export shim, the `BERLIN_TZ` and duplicate `MS_PER_DAY` constants, the verbose `AdminSectionPage` docstring, the `useEffect` paraphrase comment in `ai-section`, and rewrites the `_exhaustive: never` exhaustiveness guard to the modern `slug satisfies never` form. `UserManagementSection` now reads `currentUserId` via `useAuth()` instead of accepting it as a prop. Findings 1, 2, 3, 4, 6, 7, 8, 11, 15, 16, 17 from .planning/phase-6-simplify-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
65cfb2799a |
fix(a11y): label /admin overview quick-jump as a navigation landmark
Wrap the section quick-jump list under `/admin` in a `<nav>` with
`aria-labelledby` pointing at its heading. Without the landmark,
screen-reader users navigating by region heard "list of links" with no
contextual label distinguishing it from the sidebar nav (which already
carries its own `aria-label`).
The `<h2>` keeps its existing copy ("Sections") and now serves as the
nav's accessible name via `aria-labelledby`. Visual is identical.
Picked up from the v1.5 phase-5 friction audit (item #10) — small fix,
clear win, no scope creep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
12b280bb55 |
refactor(admin): move sections to dynamic-route pattern + reduce overview
Each admin section component now accepts an optional `id` prop (was required) so it works under the new /admin/[section] dynamic route without scroll-anchor wrapping. Adds <IntegrationsGroupSection> as the composite for /admin/integrations (Umami + GlitchTip + WebPush + Bug Report). Reduces /admin to a true overview: status-card grid + welcome card + quick-jump menu of sub-sections; the long monolithic scroll is gone. Bundle-size win: each section now ships under its own route bundle, so the overview no longer drags Umami/GlitchTip/Feedback/Users/etc. trees. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d8b71b3848 |
feat(admin): scaffold per-section dynamic routes (admin-shell + [section]/page.tsx)
Mirrors the v1.4 settings split for the v1.5 admin refactor. Adds the shell, the slug constants, the dynamic-route page, and the client-only renderer that picks the right section component for each slug. The existing admin sections continue to live in src/components/admin/*.tsx; this scaffolding wires them into per-route bundles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
26543378b5 |
fix(admin): surface query errors instead of an infinite spinner
P19. `useSystemStatus()` and `useAdminSettings()` already throw on non-OK responses so React Query sets `isError`, but neither caller read it — the system-status panel just kept spinning forever and the settings sections silently rendered defaults as if they were live. Now system-status renders a destructive inline alert when isError, and the admin page top renders a one-line "Failed to load admin settings" banner when the shared settings query fails so the admin notices the toggles below aren't reflecting reality. Both messages have new i18n keys (`admin.systemStatusLoadError`, `admin.adminSettingsLoadError`) in en.json (canonical) + de.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
643e6ec3a0 |
release: v1.4.2 — resurrect v1.4.1 work + bug-fix triple (#152)
* Revert "revert: roll source tree back to v1.4.0 to restore production"
This reverts commit
|
||
|
|
e444a90b10 |
revert: roll source tree back to v1.4.0 to restore production
Production at healthlog.bombeck.io has been 503-ing since the v1.4.1
deploys started landing on apps-01 (Coolify). The container boots —
Next.js prints "Ready" and the pg-boss workers run — but never
accepts HTTP on :3000, so the Docker healthcheck fails and Traefik
takes the upstream out of rotation. A manual restart, a Coolify
force-rebuild, and a docker-compose pin to the GHCR :1.4.0 multi-arch
image all failed to bring the site back up — Coolify rebuilds the
image from main HEAD on every deploy regardless of the compose
directives.
This commit resets the working tree to commit
|
||
|
|
6da94280b5 |
feat(admin): extract per-section components from admin page monolith (#145)
The admin page was a single 2,702-line client component with nineteen inline functions covering everything from system status to feedback inbox. The status-card grid foundation in #133 already moved the at-a-glance dashboard out; this commit moves the inner panels. Split into one file per logical section in src/components/admin/: _shared.tsx 276 LOC shared types, hooks, primitives (PasswordInput, StatusItem, SettingsToggle, useAdminSettings, useUpdateSettings, useSystemStatus, getApiErrorMessage) system-status-section.tsx 155 general-settings-section.tsx 54 services-section.tsx 68 umami-section.tsx 173 glitchtip-section.tsx 172 web-push-vapid-section.tsx 148 bug-report-section.tsx 109 reminders-section.tsx 318 user-management-section.tsx 341 api-token-overview-section.tsx 144 login-overview-section.tsx 181 danger-zone-section.tsx 118 feedback-inbox-section.tsx 511 (inbox + category badge + detail dialog) src/app/admin/page.tsx is now a 77-line shell that imports the 14 sections and the existing status-card-grid.tsx. No behavior change — every section keeps the same DOM, the same query keys, the same i18n keys, the same id="…" anchors so deep-links from the sidebar still scroll into place. UserManagementSection no longer takes a queryClient prop; it grabs its own via useQueryClient() like every other section does. |
||
|
|
725cace865 |
feat(admin): status-first card grid + aggregator endpoint (#133)
Replaces the implicit, fluid status of the legacy admin monolith with a 6-card grid at the top of /admin per ui-guidelines.md §3.4. Each card pairs a §2.6 colored dot with a text label so state never relies on color alone (WCAG 1.4.1). - New aggregator endpoint /api/admin/status-overview returns a single batched Promise.all (no N+1) covering Users, Integrations, Monitoring, Backups, Maintenance, Audit log — with severity precomputed server-side. - New <StatusCardGrid> + reusable <StatusBadge> component in src/components/admin/. Pure buildCards() function makes severity mapping unit-testable. - Tests: 14 new (8 aggregator severity/shape, 6 grid badge + a11y). - Wires the grid above the existing admin sections; per-section extraction will land in follow-ups. Co-authored-by: Marc-André Bombeck <noreply@anthropic.com> |
||
|
|
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.
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |