The carrier chip on the admin login-overview audit table was empty
whenever the offline GeoLite2-ASN MMDB had no entry for the IP. In
practice this is every IP behind a non-DACH or cloud-NAT range — the
free-tier MMDB carries DACH carriers densely but thins out
elsewhere — and the chip went blank even though the city + country
were already populated from the parallel location lookup.
When `entry.carrier` is null but `entry.location` is non-null,
render the chip with the city/country string and a "carrier
unavailable" qualifier. Private/loopback IPs still hide the chip
entirely (both columns null).
The About surface (version + license + source links + update check)
used to live as its own Settings section and the user-card dropdown
exposed it from both desktop and mobile. The three cards collectively
get opened once or twice a year, mostly by the operator confirming
the running version against the latest release.
Move the surface to `/admin/about` and drop the dropdown link from
both the desktop sidebar and the mobile top-bar. The public-facing
landing at `/about` (credits + MaxMind attribution) stays untouched
since it's reachable without a session. The legacy `/settings/about`
route still resolves so external bookmarks don't break, but is no
longer surfaced in any nav.
The existing `<AboutSection>` is reused as-is — no markup changes,
the component owns its own heading so the admin renderer mounts it
without the SectionFrame wrapper.
The Settings + Admin shells centred the active chip via
`scrollIntoView({ inline: "center" })`. On narrow viewports the
strip over-scrolled past the leading chips so the first one or two
were unreachable until the user manually swiped right. `inline:
"start"` lands the active chip at the leading edge so the trail of
preceding chips remains visible.
Five textareas in the app (Coach composer, bug-report description,
admin feedback note, medication-side-effects notes, medication-intake
JSON import) shipped `text-sm` (14 px) on every viewport. iOS Safari
triggers a viewport zoom-on-focus whenever an input renders text
below 16 px — so every tap on these surfaces yanked the page scale
and rebounded on blur. Switched each className to `text-base sm:text-sm`
so the mobile baseline clears 16 px while desktop keeps the compact
look the rest of the form chrome converged on.
No shared <Textarea> primitive exists yet — F-5's deferred work — so
the fix is per-call-site. Five files, one className change each, no
component re-shape.
Eight hotfix-ready UX/a11y findings from the v1.4.34.3 UX audit
land as one batch.
i18n leaks (3 primitives):
- Dialog + Sheet close-X carried hardcoded `<span sr-only>Close</span>`
so a German user heard 'Close, button' on every modal. Both lift
`useTranslations()` and read `common.close`.
- ChartSkeleton's sr-only loading announcement was English-only.
The component is already a client island; reads `charts.loadingLabel`
from each locale now. New key shipped in DE / EN / FR / ES / IT / PL.
- Notifications-section breadcrumb `<nav aria-label="Breadcrumb">`
was hardcoded. Routes through `t("nav.breadcrumb")` with a new
key in each locale.
Focus rings (mobile + tablet nav):
- Bottom-nav primary links + the More overflow trigger relied on a
`text-muted-foreground` → `text-primary` colour transition only;
keyboard / switch-control users saw no focus indicator at all.
Added `focus-visible:ring-ring/50 focus-visible:ring-2
focus-visible:outline-none focus-visible:ring-offset-2` matching
the v1.4.33 sidebar treatment.
- Mobile top-bar trigger had `focus:outline-none` without a
`focus-visible` replacement. Same treatment applied.
Tap-target floor (WCAG 2.5.5):
- Input / NativeSelect / Select / DateTimeInput were locked to
h-10 (40 px) on every viewport. Switched to `h-11 sm:h-10` so
mobile clears the 44 px floor while desktop keeps the 40 px norm
that the Button primitive already converged on in v1.4.33.
Admin chip-strip auto-scroll-into-view:
- The admin shell shipped 13 chips but lacked the
`scrollIntoView({inline: 'center'})` effect the settings shell
got in v1.4.33. Admins tapping a chip past the right edge landed
on the new page with the strip scrolled to the left and the
active chip off-screen. Copied the settings-shell pattern verbatim.
List-page subtitles on mobile:
- Measurements / Mood / Medications pages hid their descriptive
subtitle on `<sm` with `hidden sm:block`. Comparable apps keep
the subtitle on mobile because it doubles as contextual help.
Dropped the hide rule; the H1 still anchors the visual.
Insights empty-state CTA vocabulary:
- `ALLOWED_ADD_TYPES` on `/measurements` accepted tokens (`GLUCOSE`,
`TEMPERATURE`, `HEART_RATE`, `BMI`) that didn't match the
`MEASUREMENT_TYPES` enum the form uses. Several `/insights/*`
empty-state CTAs emitted those mismatched tokens and the dialog
silently failed to open. Replaced with a single source of truth
derived from `MEASUREMENT_TYPES` plus a normalisation map for
legacy tokens. Added BODY_TEMPERATURE to the form's enum row.
New unit test walks every `?add=` token emitted under
`src/app/insights/**` and asserts it resolves to a real form type.
Lighthouse v1.4.33 a11y pass flagged "heading elements not in
sequentially descending order" (score 91/94). Three offenders:
- `<TherapyTimeline>` (insights/medikamente sub-page): sr-only
drug-name heading was an h4, but the parent SubPageShell ships
the only h1 and CardTitle is a <div>. Promoted to h2 so the
outline stays h1 -> h2 without skipping levels.
- `<FeedbackInboxDetailSheet>`: metadata + screenshot section
headings rendered as h4 inside a ResponsiveSheet whose dialog
title is h2 (Radix default). Demoted to h3 to stay sequential
beneath the sheet title.
- `<RemindersSection>`: results heading was h4 inside the admin
SectionFrame's h1. Promoted to h2.
Coach panel headings (IW5 surface) audited but not changed.
Marc-Voice rule — the product never names itself "KI" or "AI" in
copy. Settings -> "KI-Auswertungen" becomes "Auswertungen" / "Insights",
the inner "KI-Insights" card heading collapses into the parent section
heading, "Aktiver KI-Provider" becomes "Aktiver Provider", and the
admin "AI Quality" view becomes "Insights Quality".
Sweep across de / en / es / fr / it / pl. Provider product names
(Anthropic, Claude, ChatGPT, OpenAI) are unchanged — those are
proper nouns, not the "AI" label.
New admin slug `assistant` between `ai-quality` (provider config)
and `coach-feedback` (in-app feedback inbox). The panel renders six
`<SettingsToggle>` rows — master plus five sub-flags — and writes
through a dedicated `PUT /api/admin/settings/assistant-flags`
endpoint. Optimistic UI flips on click; failure surfaces a toast
and re-invalidates the read query.
The dedicated endpoint (separate from the generic `/api/admin/settings`
PUT) lets the panel scope its audit trail to a single
`admin.settings.assistant-flags.update` action and lets the
optimistic UI bind 1:1 to a focused request/response shape. The
PUT handler echoes both the raw column values and the resolved
shape so the panel can render the master vs sub-flag distinction
visually.
Sub-toggles grey out when the master is off (UX-wise) so the
operator can see which sub-flags are individually flipped before
unmuting the master. Writes invalidate the runtime
`/api/feature-flags` cache so the operator sees the toggled
surface react within the same session.
i18n: `admin.section.assistant.{title,subtitle}` and
`admin.assistant.*` keys land in all six locales (DE proper, the
five English-leaning admin locales mirror the English copy as is
the established convention for admin strings).
Both the `/admin` overview snapshot and the full `/admin/system-status`
page now read `offlineGeoEnabled` from `/api/version` and render a
status row with a green chip when the offline databases are loaded or
a yellow chip with the configuration hint when the runtime is on the
ipwho.is fallback. The row only renders when the field is present so
legacy `/api/version` responses do not produce a placeholder slot.
Adds six locale keys per bundle (`admin.offlineGeoLabel`,
`offlineGeoEnabled`, `offlineGeoFallback` plus the three
`admin.overview.snapshotOfflineGeo*` siblings) across all six
locales.
Three single-class polish swaps to retire P0 / P1 drifts the UI-
conformity review caught:
- TrendCard headline number was the only dashboard tile using
font-bold for its hero value; every sibling renders the same
numeric role at font-semibold. Align so the trend strip reads
uniformly.
- account-section password form was the only form in the codebase
on space-y-3 outer rhythm; every other form uses space-y-4. Lift
to match.
- mood-chart YAxis tickMargin sat at 6 while every other line chart
passes 10 from the same canonical recipe. Match so mood labels
sit at the same distance from the axis as siblings.
v1.4.27 MB7 — four settings/admin polish items in one commit:
- CF-53: the Withings credentials grid drops from a 3-column row
(client-id / secret / save) to a 2-column row of inputs with the
Save button lifted into its own row below. The previous invisible-
label hack collided on Galaxy Fold; the new row places the Save
button at `justify-end` on every viewport and fills the column
on `<sm`.
- CF-55: the feedback-inbox TabsList wraps in an `overflow-x-auto`
strip so the 4 status pills + count badges scroll horizontally on
Pixel 5 instead of squashing below the legibility floor.
- CF-56: the shared `SettingsToggle` row stacks vertically on `<sm`
so the German description copy doesn't compete with the switch for
horizontal space. The inline duplicate in
`general-settings-section.tsx` (default-language row) picks up
the same contract.
- CF-57: the integrations action row floors each button to a
`min-w-[10rem]` on `<sm` so the four AlertDialog + test-connection
triggers stack two per row at most and fill their column cleanly.
v1.4.27 MB7 / CF-52 — three settings/admin surfaces each declared a
local `NATIVE_SELECT_CLASS` constant tracing the same visual contract
as the shadcn `<SelectTrigger>`. The copies had drifted slightly so
`AccountSection`, `TimezonePicker`, and `GeneralSettingsSection`
rendered at subtly different focus-ring weights. The new
`<NativeSelect>` primitive owns the contract; all three call sites
import it and the in-file constants retire. The native picker remains
the right experience for the 400-entry timezone list and the
admin-only locale dropdowns, so the primitive deliberately stays on a
styled `<select>` rather than wrapping the radix popover-driven
`<Select>`.
CF-21: the audit-log prev/next controls and summary row used to live
inside the same `overflow-x-auto` wrapper as the table. On narrow
viewports admins had to scroll the table back to its starting offset
before the pagination became visible. Same problem for the app-log
preview summary. Both rows now sit in a sibling block under the
scroll container, while the table itself keeps panning horizontally
on touch. The login-overview CSV export was already outside the
wrapper and stays put. The recent B3 carrier-chip layout is
unchanged.
Bring the same `inputMode`, `enterKeyHint`, `autoComplete`, and
`spellCheck` defaults to the settings and admin surfaces:
- Account: profile email gets `autoComplete="email"` +
`enterKeyHint="next"`, the disabled username carries
`autoComplete="username"` so the password manager can still bind
the credential, and the height field hints the decimal keyboard.
- Telegram card: bot token and chat ID both opt out of autofill,
disable spell-check + autocapitalize, and lead the chained next →
done keyboard order.
- Integrations: Withings client-id / secret and the moodLog URL +
API key share the off-spell + no-capitalize secret-input contract
and chain into the test / save action via `enterKeyHint`.
- Notifications anchors: each card wrapper carries `scroll-mt-28`
so direct anchor navigation to `#telegram`, `#ntfy`, `#web-push`
lands the section header below the safe-area sticky header
instead of under it.
- Thresholds editor: min/max numeric inputs derive `inputMode`
("numeric" for integer ACTIVITY_STEPS, "decimal" elsewhere) and
chain next → done.
- Admin reminders: the late-window and missed-window minute inputs
pin `inputMode="numeric"` and the next → done keyboard order.
Move `<PasswordInput>` from `src/components/settings/password-input.tsx`
to `src/components/ui/password-input.tsx` so the auth surfaces and
every settings card that masks a secret consume one primitive. The
two surfaces previously diverged on toggle markup and aria-label
wiring; the new home in `ui/` keeps them in lock-step.
While the primitive moves, lift the icon-only show/hide toggle to a
44 px hit area per WCAG 2.5.5 — `inline-flex h-11 w-11` wrapper with
the eye icon centred inside. The input itself gets `pr-12` so user
input never collides with the toggle glyph.
Consumer imports updated in:
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/integrations-section.tsx`
- `src/components/settings/ntfy-card.tsx`
- `src/components/settings/telegram-card.tsx`
- `src/components/admin/_shared.tsx`
W10 design-review carry-over items. M7 lifts the medication-form
day-of-week buttons, the more-horizontal trigger, and the schedule
label-input gaps to the 44 px / space-y-2 standard used by every
other settings surface. L1 pairs every animate-spin/pulse/bounce
with motion-reduce:animate-none across the component tree so users
with prefers-reduced-motion get a still surface. L3 adds the
aria-controls + panel-id pairing to the inventory + coach-evidence
<details> blocks for parity with the Health Score provenance
accordion. L4 inserts a sr-only h4 per therapy-timeline entry so
screen-reader users can jump between medication blocks.
Sub-headings in the settings + admin section trees were mixing
font-medium and font-semibold; the password-change dialog used a
1.5-unit label-input gap while every other Settings form runs at 2.
Standardise on font-semibold for every h2 and h3, the card-internal
cadence to space-y-4 on the AI provider and thresholds-editor cards,
and lift the password-dialog field gaps to space-y-2.
Adds a `carrierShortLabel()` helper in `_shared.tsx` that folds the
verbose GeoLite2 `autonomous_system_organization` strings down to
the glanceable DACH short labels Marc asked for: "Telekom",
"Vodafone", "1&1", "O2". Unknown organisations fall through to the
raw string so the chip stays informative.
`LoginOverviewSection` now renders the short label as a smaller
chip below the existing auth-provider chip, gated on
`entry.carrier !== null` so private-IP and offline-miss rows keep
the legacy single-chip layout.
The CSV export grows a matching `carrier` column between `location`
and `provider`. The API selector pulls the new `asn` + `carrier`
fields. New translation keys (`admin.carrier`, `admin.carrierUnknown`)
are referenced in code; bucket B6 lands the JSON entries.
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>