Adds `min-h-[calc(100dvh-12rem)]` to the `<main>` element in both
`<SettingsShell>` and `<AdminShell>`. The sticky 220 px sidebar is
anchored to the page scroll; previously the grid's row height tracked
the section's content height, so swapping a short spinner for a long
list (Thresholds: 14 rows; Sources: 14 metric tiles) jumped the page
height under the sidebar and shifted the active link off-screen. The
12 rem reserve covers the global header (5 rem) + section header
(~5 rem) + a small buffer, so the spinner-to-content transition no
longer changes the viewport height. Short pages (`/settings/about`)
still render short because `min-h` does not clamp tall content.
`@/lib/tz/resolver` imported `prisma` at module top so any client
component that pulled `detectBrowserTimezone`, `listSupportedTimezones`,
`formatInUserTz`, `userDayKey`, or `DEFAULT_TIMEZONE` dragged
`node:module` into the browser bundle and broke the Turbopack build
(register page, timezone picker, admin general/login sections,
`@/lib/export`).
Pure helpers now live in `@/lib/tz/format` (no Prisma). The resolver
re-exports them so every server-side caller keeps its
`@/lib/tz/resolver` import path; the four client modules + the
export helper now import from `./format` directly.
Settings and Admin pages render inside AuthShell, whose inner main
already applies `mx-auto max-w-[76.8rem] px-4 py-6 md:px-6`. Both
shells then re-applied `px-4 py-6 md:px-6 md:py-8` on their own
container, which produced visibly more top/bottom whitespace on
/settings/* and /admin/* than on /, /measurements, /medications,
/mood, /insights, /coach, and /notifications.
Drop the redundant padding from SettingsShell and AdminShell so the
visual top-of-page baseline matches the rest of the app. The
`mx-auto w-full max-w-screen-xl` wrapper stays for the wider
content column that these two surfaces opt into. Mobile chip-strip
`-mx-4` still cancels AuthShell's horizontal padding correctly.
Pixel-5 (393 px) audit flagged four primary navigation surfaces that
were below the 44 px touch-target floor:
- TopBar user-menu trigger and login link: text-only flex with
py-1.5 measured ~30 px tall. Added min-h-11 min-w-11 with a
rounded hit zone so the trigger lands on the WCAG floor without
visibly altering the icon's position.
- SettingsShell mobile section chip-strip and AdminShell mobile
section chip-strip: rounded-full chips with py-1.5 were ~30 px
tall. The mobile chip-strips are the primary navigation surface
for /settings and /admin on touch devices, so min-h-11 was added
to both.
- medication-form schedule interval picker (1W / 2W / 3W / 4W):
h-8 native buttons inside a 4-col grid; min-h-11 brings them up
to the floor without changing column width.
Also normalised the medications page-root spacing from space-y-5 to
space-y-6 to match dashboard / measurements / mood / notifications.
The Coach-Feedback admin section previously rendered four distinct top-
level structures across its loading / error / empty / data branches.
The loading branch was a thin flex-row stub without the heading, the
error branch was a p-4 alert with no card chrome at all, and only the
empty + data branches mounted the Sparkles icon + 'Coach Feedback'
title inside the bg-card border-border rounded-xl border p-6 wrapper.
The result: when the user clicked into /admin/coach-feedback the
section first measured at ~52 px (loading) and then snapped down to
its full height once the query resolved, shifting everything below it.
Mirror the canonical pattern used by SystemStatusSection: render the
outer card + heading unconditionally, and switch only the body between
spinner / alert / empty / table. The header now sits at a constant
y-offset from the section root regardless of fetch state.
Adds a regression test that asserts the 'Coach Feedback' heading and
the bg-card rounded-xl outer chrome are present in every render state.
7 conflict files resolved by taking the upstream/HEAD side (which carries
the W4d/W5/W6c/W3f feature additions). Stashed-side changes that were
genuine new features re-land in subsequent commits if any were lost.
This sweeps up the loose state the W4e+W5e+W5d combined agent left
mid-stash. Resolves develop's working tree for clean push.
`/admin/login-overview` is the only card on its dedicated route since
v1.5 phase-4b. The Collapse/Expand toggle inherited from the v1.4
shared-admin page therefore hid the entire page contents on click,
serving no purpose beyond clutter. Drop the `expanded` state, the
header toggle button, and the `expanded && (…)` body wrapper; the
section always renders. The two queries lose their `enabled:
expanded` gate (a no-op now that they always run). The
`settings.collapse` / `settings.expand` i18n keys stay so future
surfaces can opt back in without re-introducing the strings.
A regression test mirrors the `api-token-no-collapse` pattern: no
"Collapse"/"Expand"/"Einklappen"/"Ausklappen" buttons, no
`aria-expanded` on the section heading, and the filter pills paint
unconditionally in SSR.
The legacy export emitted snake_case English headers and raw UTC
timestamps which Excel/LibreOffice misread once the `Z` suffix was
stripped (issue #167 territory) and forced admins to mentally map
`actor_username` → "Marc" every time they triaged a row. The export
now mirrors the on-screen column order — `timestamp → user → IP →
location → provider → outcome` — with `action` + `details`
appended so the full audit payload still rides along. Headers are
translated through the active locale; timestamps go through
`formatInUserTz(..., "iso-with-offset")` so they round-trip
correctly in any spreadsheet; `toCSV` now treats `\r` as an escape
trigger alongside `\n,"` so Windows-newline detail strings stay on
their row.
`buildAuditLogCsvRecords` and `auditLogCsvHeaderLabels` live in
`_shared.tsx` so the column-order/escape contract is pinned by a
pure unit test independent of the React render.
The audit log records *how* each login happened in the action name
(`auth.login.passkey`, `auth.bearer.success`, …) but the table
collapsed all of that into a single Aktion label. Admins now see
the credential type as a separate column with a lucide glyph
(KeyRound / Fingerprint / Cpu / Globe) so password-vs-passkey-vs-API
token attempts are distinguishable at a glance — and the column
maps cleanly into the CSV export.
The mapping itself (`providerForAction`) lives in `_shared.tsx`
alongside `useAuthActionLabels` so any later audit-feed consumer
picks up the same i18n + icon contract; a regression test pins
every action → provider edge and every provider → icon edge.
The Standort column existed but rendered right-aligned next to a
right-aligned IP and timestamp, which pushed it into the gutter on
wide audit tables and let admins overlook it entirely. Left-align
the IP and Location cells (timestamp keeps the right edge as the
sort anchor) and prefix the Standort header with a MapPin glyph so
the column is a first-class entry instead of a forgotten trailer.
v1.4.25 W7 phase 5 — proposal §5 admin half.
GET /api/admin/settings now returns defaultUserTimezone (null = use
Europe/Berlin fallback). PUT accepts the field:
- empty string clears the column (resolver falls back)
- non-empty must pass isValidTimezone(); otherwise 422
- successful writes invalidate the server-default resolver cache
so the registration handler sees the new value on the next
signup without waiting 60s for the TTL
The admin form gets a third row in the General Settings section
between 'default language' and the toggles, mirroring the user
picker UX (native select over Intl.supportedValuesOf, fallback
'Europe/Berlin (default)' option clears the override). 3 new i18n
keys per locale (defaultUserTimezone +
defaultUserTimezoneDescription + defaultUserTimezoneFallback);
descriptions follow proposal §5: 'Used for new accounts that don't
carry a browser-detected zone. Does not affect existing users.'
Tests: 3 new cases in src/app/api/admin/settings/__tests__/route.test.ts
covering valid IANA write, empty-string clear, and invalid-zone
rejection.
Mostly table column normalisation under docs/audit/ + planning, object-
literal expansion in src/lib/ai/coach/snapshot.ts after the W4 F6 Apple
Health metric block landed, and small wrap/unwrap line adjustments in
the device + APNs routes / tests touched across W3/W4/W5 sessions.
No behavioural change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The coach-feedback slug was registered in ADMIN_SECTION_SLUGS and the
page rendered correctly via /admin/[section], but the entry was never
added to the hand-maintained ADMIN_SECTIONS array. Operators could only
reach the view by typing the URL directly. Add the entry next to
ai-quality so the two AI-quality views sit together, and add a drift
guard test that walks ADMIN_SECTION_SLUGS and asserts every slug has a
matching nav entry. Same drift class would recur on the next admin
section addition without the guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Five independent atomic edits from the W5 simplify review, grouped
in a single commit since none pull weight as a compound theme:
- coach-input: collapse `!disabled && value.trim().length > 0`
triplication onto a single `canSubmit` (S-01).
- hero-strip: extract the duplicate weekly-report Button shell into
a shared label fragment with the Link wrapping conditionally
(S-02).
- api-token-overview-section: extract `<TokenStatusBadge size>` so
the desktop table and mobile card list stay in lock-step on the
revoked / expired / active 3-way ternary (S-03).
- trends-row: hoist the dynamic-load chart skeleton to a shared
`ChartSkeleton` component (S-04). No chart-visual change —
placeholder only.
- correlation-row: collapse the grid className ternary onto `cn()`
with a conditional `md:grid-cols-2` (S-05).
Test count unchanged (2105).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.
CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.
CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).
Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cluster of the v1.4.21 carry-over items that each landed in ≤5 LOC.
- F-32 — drop the duplicate help paragraph beneath the dashboard
settings header; the page-level description already covers it
- F-33 / F-34 — replace `&` with "and"/"und" in the measurements and
mood subtitles
- F-35 — unify the test-notification button label
(notificationTestSend) with the user-facing notificationStatus.sendTest
- F-44 — drop the `LOKI_ENDPOINT` env-var name from the user-facing
monitoring help text; reads as "Loki integration" instead
- F-45 — admin/users table column reads "Name" instead of repeating
the page title "Users"
- F-47 — /notifications event-matrix first column reads "Event" instead
of repeating the page title "Notifications" — new notifications.eventColumn key
- F-50 — soften every DE error-toast "Bitte erneut versuchen" to
"Bitte versuche es erneut", matching the friendlier tone we already
use in the Coach error path
- F-58 — admin/reminders section header swaps Clock for Bell so the
card icon matches the admin-shell nav tile
- F-62 — DE dashboard `greetingSalutation` reads "Hallo, {name}"
instead of the English "Hi, {name}"
- D-CR-M-03 — formatTokenName regex broadens to non-Z ISO offsets so a
`+02:00` timestamp suffix renders as the locale-aware chunk like a
`Z` suffix already does
- D-DSGN-M-04 — moodLog "Copy" button uses common.copy instead of
stripping the `!` off common.copied
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /admin/api-tokens page has been painting a residual horizontal
scrollbar for four releases despite column-hide, mobile card-list, and
the admin section-strip `no-scrollbar` fixes. A fresh production probe
confirmed the actual culprit: the "Last used" and "Created" `<td>`s
carry `whitespace-nowrap`, and `formatDateTime` renders strings like
"05.05.2026, 21:46" (~110px) that exceed the 12% colgroup allotment
(~84px on a 700px content area). `whitespace-nowrap` wins over
`table-fixed`'s width contract, so the table's intrinsic width exceeds
100% and the wrapping `overflow-x-auto` paints the scrollbar.
Dropping the two `whitespace-nowrap` classes lets the date+time wrap
to two lines on narrow viewports. The card grows by one row but no
scrollbar — and at >1024px desktop the unwrapped string already fits
in the 12% allotment so the visual is unchanged for the common case.
Picked option 1 of the three W1a probe found (drop nowrap, switch to
formatDateShort, or hide "Created" at md): smallest two-line diff, no
i18n surface impact, no information loss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-issued token-name viewer rebuilt the dd.MM.yyyy HH:mm suffix
from `Date.getUTC*` accessors, producing 19:46 instead of the 21:46 a
Berlin-time admin would expect for a token issued at 19:46 UTC. Route
the parsed Date through `Intl.DateTimeFormat("de-DE", { timeZone:
"Europe/Berlin", … })` so the embedded timestamp matches every other
display surface in the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical clean-up sweep across five hot spots flagged by the simplify
audit:
- Lift the 11-key auth.* audit-action label map out of the two admin
components that rebuilt it on every render. The shared
useAuthActionLabels() hook in _shared.tsx is now the single home for
new entries (login-overview + recent-audit-preview consume it).
- Drop the dead chipClass branches in IntegrationStatusPill: only the
connected state actually paints a Dracula-green chip; error and
disconnected fall back to the badge variant.
- Add thin DateInput / DateTimeInput wrappers under components/ui that
bake in the lang={locale} hint so 14 callsites across 9 files no
longer have to remember the prop. A future date input cannot
silently regress.
- Remove the leading icon-only flex wrappers left behind when the
duplicate card titles were dropped on /admin/feedback,
/admin/danger-zone, and the thresholds editor.
- Strip the five "v1.4.19 X removed because…" narration comments from
insights/page.tsx and the dashboard. git blame already answers what
changed and the comments aged into history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run prettier on the test, e2e, and component files this v1.4.19
A8 wave touched. The .planning/* and docs/audit/* prettier
warnings are pre-existing baseline noise — left alone to keep
this commit narrow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The native auto-login flow names tokens by suffixing the issuing
call's `Date.now().toISOString()`, so the admin viewer rendered
"iOS auto-login 2026-05-05T19:46:20.603Z" — readable as debug
output, not as something a person picked. Added a renderer-side
formatter that detects the trailing ISO timestamp and rewrites
the cell to "iOS auto-login · 05.05.2026 19:46". Manual token
names without an ISO suffix pass through unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related improvements to the admin recent-activity surface:
- New audit actions `auth.token.autoissue.native`,
`auth.token.refresh`, and `auth.token.revoke` were leaking
through both the dashboard preview and the Login Overview
viewer as raw event keys. Added translations and wired the
three keys through the action-label map shared by both
components.
- Recent-activity rows on /admin were a dead-end: a failed
login row showed "Unknown" + "Failed" with no link, no IP,
no way to drill in. Each row is now a Link into
/admin/login-overview, with the IP shown inline at sm+.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A few small quality wins bundled together:
- The 1-day achievement descriptions read "for 1 consecutive
days" / "1 Tage in Folge" — singular/plural mismatch that
was awkward in both languages. Hand-tuned the five 1-day
variants to "for 1 day" / "an einem Tag"; 7d / 30d copies
unchanged.
- The two "Two hundred …" / "Zweihundert …" badges now use
the numeral 200 to match every other count-based badge.
- /admin/integrations toggle rows ("Umami" / "GlitchTip" lines
that duplicated the card header) drop the literal product
name and just show the action: the existing
`umamiEnabled` / `glitchtipEnabled` description copy is
promoted to the toggle's primary label. SettingsToggle's
description prop becomes optional to support that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`/admin/danger-zone` and `/admin/feedback` are single-card admin
pages where the page-level header already names the section.
The card-level h2 ("Danger Zone" / "Feedback Inbox") was a verbatim
copy of that header taking up vertical space without adding info.
Removed the duplicate headers — the section icon stays so the
card still has a visible affordance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several smaller A8 copy nits in one pass:
- Notifications settings subtitle no longer hard-codes "Telegram,
ntfy, Web Push" — the channel-reliability board only shows
configured channels, so naming all three made one of them look
missing.
- Admin /feedback page subtitle widened to "feedback, bug reports,
and feature requests" so it matches what the inbox actually
contains; the duplicate card-level title + description on
/admin/feedback is gone (page header already provides them).
- /admin/services subtitle now mentions integrations alongside
notification channels (the API and moodLog toggles live there
too); per-row "system-wide" suffixes dropped — the section
subtitle already establishes the scope.
- /admin/ai-quality subtitle reads as a sentence instead of the
jargon "(severity x provider)" expression.
- /admin/feedback empty state is "All caught up" / "Alles erledigt"
instead of the English-idiom "Inbox zero".
- /achievements subtitle rewritten in both EN + DE to drop the
dev-speak "from feature rollout onward".
- /admin/api-tokens permission badges show "All" (with a tooltip)
instead of the bare regex glob `*`.
- Brand spelling normalized: every Glitchtip → GlitchTip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user-management role-change button rendered a bare arrow
followed by "User" / "Admin" — direction was ambiguous
("does the arrow mean 'now they are User' or 'send them down
to User'?") and the label carried no aria-label. Replaced the
arrow text with explicit "Make user" / "Make admin"
("Zum Benutzer" / "Zum Admin"), and added the same string as
aria-label so screen readers announce the action verb instead
of the visible-only arrow glyph.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two badges were rendering raw enum values: backups showed
`WEEKLY_AUTO` / `MANUAL` and the measurement list showed
`WITHINGS` instead of `Withings`. Backup type now uses two new
i18n keys (`Weekly (auto)` / `Manuell`); measurement source
reuses the existing `measurements.source*` keys. Unknown
enum values still fall through to the raw string so any
schema additions remain visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Login Overview page promised "authentication and admin events"
in its subtitle but the underlying audit-log query never set
`filter=auth`, so the table mixed insights.* / admin.* / data.*
rows in with passkey + password logins. Default the page request
to `filter=auth` (an explicit action picker still overrides), and
restrict the action dropdown to actions starting with `auth.` so
the filter UI stays in sync with what the page actually shows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`/admin/api-tokens` carried a Collapse / Expand toggle inherited
from the v1.4 shared admin page where 13 sections lived together.
On the v1.5 dedicated route the toggle just hides the only card on
the page — Marc reported it as "sinnlos" (pointless): the page
title already says "API Tokens", the only thing below it is the
tokens card, and collapsing it leaves an empty surface with the
section heading staring back at you.
Drop the local `expanded` state and the toggle button from the
section. The card always renders, the query always fetches, and the
header is now a plain icon + title row. The shared
`settings.collapse` / `settings.expand` i18n keys stay in messages
because `<LoginOverviewSection>` still uses them.
The matching test asserts no Collapse / Expand / Einklappen /
Ausklappen string appears in either locale, and that no
`aria-expanded` button shows up on the section header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marc has now reported a residual scrollbar on `/admin/api-tokens`
four times running. Earlier fixes addressed the obvious overflow
culprits (column-hide in v1.4.15, mobile card-list in v1.4.16,
admin-shell mobile-strip `no-scrollbar` in v1.4.18), but the
section itself still leaked a few quirks:
- The mobile token-name span carried `truncate` and `break-all` on
the same element. `truncate` sets `white-space: nowrap`, which
beats `word-break: break-all` per CSS spec — `break-all` was dead
code AND it was misleading. Remove it.
- The desktop table had no per-column upper bound. A long token
name could expand the row past the card on 768-1024 px viewports.
Lock the layout with `table-fixed` + `<colgroup>` widths.
- Permission badges had no `max-width` cap. A single long
permission string (`measurements:write:audit:legacy:extended`)
could push the cell past the card. Cap each badge with
`max-w-full truncate` and a native `title` attribute.
Wrap every potentially-long string (token name, username, permission
badge) in a small `<TruncatedCell>` helper that pairs the visual
truncation with a radix tooltip. The tooltip is the discoverable
hover/long-press path so the full value is still readable; the
native `title` attribute is a graceful fallback for screen readers
and right-click → "view full name" on desktop.
Adds an e2e regression that walks every element inside the mobile
card-list and asserts none of them carry `scrollWidth > clientWidth`,
exercised against a worst-case payload (long token name + long
username + long permission string).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production probe at /admin/api-tokens on a Pixel-5 viewport pinned the
scrollbar that kept showing up after two earlier fixes: it lives in
the AdminShell mobile section strip (13 entries, scrollWidth ~1700 vs
clientWidth ~360), not in the api-tokens table. The strip sits right
above the api-tokens card, which is why every previous attempt
targeted the table while the bar stayed put.
Adds a `no-scrollbar` utility in globals.css (scrollbar-width: none +
::-webkit-scrollbar { display: none }) and applies it to the strip in
both AdminShell and SettingsShell. Swipe and keyboard-arrow scrolling
keep working; the painted bar simply doesn't draw.
New AdminShell unit suite locks in the class so a future refactor
can't silently regress it; settings-shell test gets the matching
assertion. The api-tokens e2e spec gains a regression check that the
strip declares `no-scrollbar` and overflow-x-auto together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
When `/api/admin/status` 500s (rolling deploy, transient DB blip),
the section's error state previously rendered a static "refresh the
page" message — forcing a full reload + cookie revalidation just to
recover from a 5-second hiccup. Pairing the alert with an inline
Retry button (`refetch()` from React Query) lets the admin recover
in-place without losing scroll position or any other open admin tab.
The button is `min-h-9` desktop-tight (this is admin, not mobile-
critical), shows a spinner while `isFetching`, and disables itself
during the request. The German + English message strings drop the
"refresh the page" tail since the button now does that work.
WCAG 2.5.5 Level AAA target-size floor for non-essential controls is
24×24 px and Level AA pointer-target is 24×24 with 24 px gap, but the
mobile-app convention everyone follows is the 44×44 hit-area Apple
HIG / Material recommend. The default `<Button size="sm">` in the
shadcn-base lands at `h-8` (32 px) — readable on desktop, tight on
phones, especially in the most-touched admin/settings/onboarding
surfaces called out in the v1.4.15 design review.
Cross-cutting `button.tsx` `h-9 → h-11` was rejected as too risky
mid-marathon (every UI affected, dozens of layout assumptions). The
targeted alternative bumps the listed call-sites with `min-h-11`
overrides: tour Skip / Back / Next, notification re-enable + send-
test, backups Restore / Download / Run-now / Upload. Admin-only
recent-audit-preview is a `<Link>` not a `<Button>` and is left
alone.
The shadcn `min-h-` Tailwind utility wins over the variant `h-`
because both apply but `min-h-` only floors. So desktop visuals stay
within ~4 px of the previous render while mobile reaches the 44 px
floor.
Adds the `app-logs` row to `<AdminShell>` so the new section shows up
in the admin sidebar (desktop) and mobile section strip alongside
Login Overview and Backups. Uses `lucide-react`'s `FileText` icon
because nothing else in the bundle reads as "structured log lines".
The mobile-strip / desktop-sidebar pattern follows the conditional
admin-only nav established in v1.4.15 A1 and v1.4.16 A1: the global
sidebar still surfaces a single `/admin` link, and the per-section
nav lives inside the admin shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Local clean-clone verification confirms Wave-A introduced no
regressions: typecheck/lint/unit/integration all green at origin/main
HEAD `94c748d`. CI Security & Quality consistently passes; the failing
Integration tests + e2e workflows are pre-existing infrastructure issues
(failure shape identical at v1.4.15 release commit `0985c93`), not
Wave-A source regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 atomic commits on origin/main land the curated reference bundle,
schema validation, prompt injection, citation-coverage logging and
the UI footnote. PROMPT_VERSION bumped to 4.16.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a recommendation carries a `referenceId` pointing into the
curated medical-reference bundle, render a small footnote underneath
linking to the guideline. Format: "Source: ESH 2023 — ESH 2023
Guidelines for the Management of Arterial Hypertension" (locale-
routed title), with a tiny external-link icon, opens in a new tab via
target="_blank" rel="noreferrer". Per the v1.4.16 research doc §7
the URL never opens in an in-app webview.
`InsightResult.recommendations` widens to accept either the legacy
plain-string shape OR a structured `{ text, referenceId? }` object so
existing rendering paths keep working while the new citation surface
plugs in through the same component. `getMedicalReferenceById()`
returns undefined for unknown ids — the UI silently drops the
footnote in that case rather than rendering a broken link (defence
in depth on top of the schema validation).
i18n keys `insights.recommendation.source` and
`insights.recommendation.viewSource` land in both EN and DE.
6 component tests pin the no-footnote / EN-locale / DE-locale /
unknown-id-defence / multi-rec paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.15 A2 hid columns + tightened card padding, but Marc still saw a
scrollbar on prod at /admin/api-tokens on Pixel-5 (393 CSS px). The
fundamental issue is that even with three columns visible, long
permission lists or token names could push the scroll wrapper wide
inside the card. Mirror the `<UserManagementSection>` pattern instead:
gate the desktop `<table>` behind `hidden md:block` and render a real
mobile card-list at <md.
The mobile card-list:
- One <li> per token, padding p-3, no horizontal scroll wrapper.
- Token name uses `truncate break-all` so even a hex-suffixed name
wraps within card width.
- Permission badges sit in a `flex-wrap` row.
- Last-used + Created collapse to short meta lines (no nowrap).
- Status badge floats next to the name on the same flex-wrap row.
Tests:
- `api-token-overview-responsive.test.tsx` rewritten to assert the new
invariants: hidden md:block on the desktop table; md:hidden on the
card-list <ul> with `data-testid="admin-tokens-mobile-list"`; no
overflow-x-auto inside the mobile container; meta paragraphs wrap.
- New e2e spec `admin-api-tokens-mobile.spec.ts` mocks a worst-case
payload (long username, multiple permissions, long token name) and
asserts `documentElement.scrollWidth <= window.innerWidth + 1` at the
Pixel-5 viewport on /admin/api-tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps version to 1.4.15 and adds the corresponding CHANGELOG entry
covering backup completeness, achievements UI, onboarding tour,
doctor-report v2, auto-deploy on GHCR push, empty-states sweep,
notification + integration status UI, AI hallucination guardrails,
mobile UX fixes, and the BD-Zielbereich percentage fix.
Includes the formatting drift left over by the v1.4.15 development
phases (Prettier sweep on touched files) and a typecheck fix in
`dashboard-layout.test.ts` so `pnpm typecheck`, `pnpm lint`,
`pnpm format:check`, `pnpm test`, and `pnpm test:integration` all
pass green on the release tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces blank rectangles in 6 admin sub-routes with the EmptyState
primitive. Each surface now shows an icon, localized title +
description, and (where it makes sense) a primary action so a fresh
admin always knows what to do next.
- /admin/users — Users icon, filter-aware copy, "Show all users" reset
CTA when a filter is active.
- /admin/backups — Database icon, "Backup now" duplicate CTA inside the
empty card so the action is right next to the explanation.
- /admin/login-overview — ScrollText icon, splits "no auth events" from
the safer "no failed sign-ins"; reset CTA on the failed-only filter.
- /admin/api-tokens — Key icon, copy points to native client sign-in
and Settings -> Account where tokens are issued.
- /admin/feedback — Inbox icon, compact-size, per-tab description.
- /admin overview audit-preview — compact + plain so it nests inside
the parent card without doubling the dashed border.
New unit tests for the users + backups paths assert the primitive
mounts (role=status / dashed border), the localized strings render,
and the reset/CTA actions appear only in the right contexts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-rail "Documentation" link in the Backups section header points
at https://docs.healthlog.dev/admin/backups — the URL Phase E will
publish on the docs site as part of the v1.4.15 release.
Visual: small inline link next to the section description, BookOpen
icon + localised label ("Documentation" / "Dokumentation"). Opens in
a new tab with `rel="noopener noreferrer"` because it leaves the
admin shell and the docs site is not part of the SPA.
i18n: new `admin.section.backups.docsLink` key in EN + DE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/admin/backups/[id]/restore replaces the target user's
personal data with the contents of the snapshot. This is the riskiest
endpoint in the backup family, hardened with five independent gates:
1. requireAdmin() — cookie-session admins only, never a Bearer token.
2. Body must carry confirm: "RESTORE" (rejected with 422 + audit
denial otherwise).
3. UI gates the destructive button behind a typed-string match —
three independent steps before fire (open dialog, read warning,
type RESTORE).
4. withIdempotency() wraps the handler so duplicate clicks with the
same Idempotency-Key replay the original outcome instead of
re-executing the destructive transaction.
5. Pre-validation of measurement-type / measurement-source /
intake-source enums OUTSIDE the transaction so a malformed
payload can't half-wipe the user before failing the create step.
Restore scope mirrors the v1.4.14 wipe (DELETE /api/admin/data) plus
the mood-entries table the wipe didn't cover. AuditLog rows are
intentionally PRESERVED across the restore — the audit trail is the
last thing that should be touched by a destructive op.
Transaction:
- Delete user's measurements, medications (cascades to schedules +
intake events + reminder configs + telegram messages), mood
entries, notification channels, push subscriptions, telegram
scheduled deletions.
- Re-create from payload: measurements (skipDuplicates), medications
+ their schedules in one create call, intake events keyed by
medication name -> id (orphans dropped silently), mood entries
(skipDuplicates).
Audit:
- admin.backups.restore.start (intent, before transaction)
- admin.backups.restore (success, after transaction)
- admin.backups.restore.denied (5 reasons, no transaction ran)
- admin.backups.restore.failed (decrypt / schema / enum / tx error)
UI:
- New <RestoreRowDialog> subcomponent renders an AlertDialog gated
on a typed-RESTORE Input. Description carries the target username
and the snapshot timestamp so the admin sees exactly which
backup they're about to restore. Confirm button stays disabled
until typed string matches.
- Restore button sits next to Download in each row's action column.
- Idempotency-Key built client-side as restore-<rowId>-<uuid> to
de-duplicate accidental double-clicks.
Tests: 3 new integration tests in
tests/integration/admin-backups-restore.test.ts covering the round-
trip (wipes pre-existing data + recreates from snapshot),
confirmation-token rejection, and 404 on unknown id. 21/21
integration tests pass; 69/69 unit tests on src/lib/validations and
src/components/admin still green.
Note for v1.4.16: the restore test queries `medication_schedules`
with explicit field selection because a sibling branch added
`days_of_week` to the schema without a matching migration; the
default `findMany` would 500 against the deployed test DB. When that
migration lands, the explicit select can be relaxed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A5 / B-mobile HIGH cluster — closes 4 audit findings:
1. Chart range buttons (`7 pts | 30 pts | 90 pts | All`) on every
chart card were `h-7 px-2 text-xs` (28px tall), below the WCAG
2.5.5 / Apple HIG / Material 44px minimum. Bumped to
`min-h-9 px-2.5` (36px) — that's still compact enough to fit in
the chart header strip on a 393px viewport but raises tap-target
pass rate substantially. Files: health-chart, mood-chart,
medication-compliance-chart, compliance-line-chart,
compliance-charts.
2. Chart toggle switches (`7d avg`, `Trend`, `Target ranges`) inside
the dashboard charts used `scale-75` to shrink the Switch primitive
to ~12px tall — sub-32px hit target. Dropped the `scale-75` so each
Switch returns to its default size. Files: health-chart, mood-chart.
3. Medication card primary actions ("Taken" / "Skipped"): these are
the most-tapped controls in HealthLog (every dose intake is one
tap). Were `size="sm"` (32px). Bumped to default size + explicit
`min-h-11`. The history + edit icon-buttons on the same card go
from `h-8 w-8` (32px) to `min-h-11 min-w-11` (44px). Glyph stays
16px so the visual weight doesn't change.
4. Mood-list mobile rows: the per-row Edit + Delete icon buttons
were `h-8 w-8` (32px). Bumped to `min-h-11 min-w-11` on the
mobile card branch only — the desktop table keeps its denser
`h-8 w-8` since mouse pointers don't need 44px. `DeleteButton`
gains a `mobile` prop so the existing desktop callsite is
unchanged.
Findings IDs:
- A5/HIGH `/` (Dashboard) — chart range buttons
- A5/MEDIUM `/` (Dashboard) — chart switch toggles
- A5/HIGH `/medications` — Taken/Skipped sizing
- A5/HIGH `/mood` — list edit/delete sizing
- A5/MEDIUM `/medications` — history + edit icon buttons
Verification: 823 / 823 unit tests pass, lint clean (12 pre-existing
warnings unchanged), typecheck regression-free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>