Commit Graph
36 Commits
Author SHA1 Message Date
Marc-André Bombeck 07f9615dba feat(ops): name 1536 MB as the supported memory floor
The getting-started requirements now say a host needs 3 GB of RAM, 4 GB
recommended, and drop the advice to lower APP_MEMORY_LIMIT to 1g on a
2 GB host. The compose and .env example comments say the same.

At boot the app reads its cgroup memory limit (v2, then v1) and logs one
line when it is below MEMORY_FLOOR_MB. The support report adds the
container limit and a note below the floor, and Admin > System status
shows "below the supported floor" next to the existing low-heap flag.
The app keeps running either way.
2026-10-21 19:36:06 +02:00
Marc-André Bombeck e7ee416e19 refactor(ui): move 62 hand-written empty states onto EmptyState
The admin sections, the cycle, illness, medication, mood, records and
settings cards, the pickers and dialogs said nothing-here-yet in a
hand-written muted paragraph in five sizes and three alignments. List
and section states now render through EmptyState (plain, compact; the
three with a next step carry it as the action), and the six chart
bodies through ChartEmptyState at their former height. The thirteen
that stay are a field without a value or a hint inside an editor, not
a list state. The ratchet ceiling drops from 75 to 13.
2026-10-20 13:31:02 +02:00
Marc-André Bombeck 923249dbf6 fix(ui): draw every failed read through a read-failure primitive
Around thirty surfaces drew a failed read their own way: a bare
destructive paragraph with no retry, a muted line saying the data was
still being learned, a skeleton that never ended, an error card nested
in a tile, or nothing at all. None of them told an ended session from a
refusal. They now use QueryErrorRow or QueryErrorCard with the query's
error and a retry, inset where a card already frames them, and keep
their card and title where the page would otherwise jump. Hydration and
caffeine now fail the same way.

The recovery page no longer points to Integrations while Oura's first
resilience level is simply on its way.

A guard reads every read hook's isError branch and refuses hand-rolled
markup there, and refuses a framed error row inside a card.
2026-10-20 10:10:49 +02:00
Marc-André Bombeck 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.
2026-10-20 09:08:40 +02:00
Marc-André Bombeck 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.
2026-10-20 03:38:57 +02:00
Marc-André Bombeck 1d7077286f feat(admin): flag a heap limit below 700 MB with the setting to raise
The support report and the system status now say in one sentence when the
heap is the 1 GB-container size and point at APP_MEMORY_LIMIT.

Refs #1239
2026-10-18 13:10:20 +02:00
Marc-André Bombeck 470ddeedc4 fix(version): move the finer geo state to the admin status
geoLookup and geoProviderChosen on the public /api/version added one
more way to tell instances apart. They now live on /api/admin/status
(requireAdmin, cookie only) as geo { lookup, providerChosen,
providerHost }, which both admin status views read. offlineGeoEnabled
and geoProviderHost stay public as before.
2026-10-09 10:44:18 +02:00
Marc-André Bombeck 0a6811c03f fix(admin): read a geolocation provider the operator named as a decision, not a warning 2026-10-09 08:38:58 +02:00
Marc-André Bombeck 9e1f64e733 fix(admin): keep the worker-status row readable with no worker present
The admin snapshot paints its worker row in `--destructive` whenever the
process serving the page is not the worker: a split deployment running
`HEALTHLOG_PROCESS_TYPE=web`, or a worker that stopped answering. The token
is AA on a card (4.85:1 in the light theme) but the stat tile lays a
`bg-muted/50` wash over that card, and axe measured the tinted wording at
3.97:1 there, under the 4.5:1 AA floor. `e2e/a11y.spec.ts` went red in
exactly that state and green in every other one, which is why the console
could ship it.

The status grids now name a tone instead of spelling a class at every call
site. Success and warning keep their tint, both clear AA on the tile.
Destructive takes the shape the other status rows already use, the
medication detail row and the invite chips: the token paints a small
indicator and the wording stays in `text-foreground`. The word itself
("Stopped", "Error") states the condition, so nothing hangs on the colour
alone. The database, Umami and GlitchTip rows sit on the same surface with
the same tint and move with it.

The success-affordance guard's two pinned entries for the status grids went
stale in the process: the tint they carried now lives once in the tone
table, so the pins follow it there.

Checked against a standalone build on a throwaway database: the whole a11y
spec green in both themes and both viewports with `HEALTHLOG_PROCESS_TYPE`
at `web` (the failing state) and at `all`. jsdom cannot measure a contrast
ratio, so the new unit test pins the token choice for both arms instead.
2026-09-10 06:56:50 +02:00
Marc-André Bombeck 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.
2026-08-09 04:32:54 +02:00
Marc-André Bombeck 5445bfc1f1 refactor(ui): give Settings and Admin one card rhythm and one page header
The two consoles used the same primitives and still read as two systems,
because everything without a primitive had drifted: the header-to-body
step existed in three competing techniques at once (`mt-4` on the first
body child, `space-y-4` on the card, `mb-4` on the header), the body
started either at the card edge or 28 px in, and both shells hand-rolled
the page title instead of calling `PageHeader`.

Move the decisions into the primitives so a call site cannot re-open them:

- `SettingsCard` is gap-based like `Card` (`flex flex-col gap-4`). The
  header-to-body step is the container's, not a margin somebody remembers.
  A dense card still overrides the gap on the card itself.
- The body starts at the card edge. `pl-7` is gone — off the spacing scale,
  and it mis-aligned action rows against their own forms wherever two
  neighbouring cards disagreed about it.
- The card header's description slot loses its `space-y-1`. That was the
  licence call sites used to stack explainer paragraphs into a muted
  `text-xs` slot; the slot holds one sentence.
- New `SettingsCardActions`: one action row, trailing edge, last thing in
  the card. `align="start"` spells the one exception, a card whose only
  actions are links out.
- New `StatTile` replaces four hand-rolled label/value boxes in Admin.
- `SectionHeading` moves from `insights/` to `ui/`. It is the heading above
  a group of cards, which is what Settings needs; Settings had been using a
  card header with no card, which reads as a card that failed to paint.

Both shells now render their heading through `PageHeader`, which gained
`headingAs="div"` for the second copy each shell paints (mobile above the
nav strip, desktop inside the grid) so only one is a real `<h1>`. The
page explainer is muted everywhere: the primitive said foreground, the
standards and every hand-rolled copy said muted, so pages disagreed with
each other. Medications, Vorsorge, the labs / custom-metric / illness
detail pages and the clinician view drop their hand-rolled title blocks
too, including the mid-word truncation on the medications subtitle.

Tests pin the parts that can be checked exactly: the card's gap, the
description slot's missing rhythm, the action row's two alignments, the
`<h1>`-free shells, and a sweep asserting `pl-7` stays gone.
2026-08-07 19:05:15 +02:00
Marc-André Bombeck 47e50ece39 Wait for content the page owns before scanning it
Several accessibility scans ran against a page that had not finished
arriving, so axe reported violations the app does not have. The worst of
them was the medications case, which waited for a level-one heading and
then checked that a level-one heading exists. A page that really lost its
heading would have waited out the timeout under a misleading name instead
of failing the rule, which makes it a check that cannot fail rather than a
flaky one.

The filter rail was the other bad sentinel. It renders beside the loading
silhouettes rather than after them, so it was on screen while the rows were
still grey rectangles. Under an 8x CPU throttle the rail and the loading
marker resolved in the same 40ms poll and the rows a poll later.

Every case now gates on a marker the real component carries in its
resolved, non-empty branch, and on a component other than the one the scan
asserts. Both the desktop table and the mobile card list carry the same
marker so the wait holds at any viewport. No sentinel names a heading, the
app frame, a busy marker or the filter rail any more.
2026-08-02 18:50:31 +02:00
Marc-André Bombeck 8ece6f39cc fix(status): expose background failures 2026-07-28 14:00:44 +02:00
Marc-André Bombeck f0c5b5ae94 fix(medications): identify an imported dose by when it was taken
The intake import built its replay key from the row's optional `zaehler`
field. That reads as a per-row counter — the documented example showed one —
but nothing required it to be unique and nothing checked. A reported export
carried the dose quantity there instead: 1 tablet, on all 28 rows. Every entry
queued under the same key, one dose reached the ledger, and the other 27 came
back reported as duplicates of a dose they had nothing to do with.

The key is the instant now, and `zaehler` is out of the contract rather than
accepted-and-ignored. Nothing is lost by dropping it: the live-row unique on
`(user_id, medication_id, scheduled_for, source)` already makes an imported
dose unique per instant, so a re-import of a file first imported under the old
key still dedups.

The telling is the other half. Two different facts were counted as one number:
entries of a submission that collide with each other, and doses the ledger
already holds. They are separate reasons now, grouped with a count, and the
dialog renders them through the same classifier and the same outcome line as
the CSV and JSON import cards — so an import that wrote nothing can no longer
answer in success tone. That rule shipped for the settings import panel and
this surface was not part of it; the guard now lists every import surface by
path, so a fourth one has to be added deliberately. `skippedInvalid` is gone:
it was hardcoded to zero on every path and reported a capability the importer
never had.

Alongside it, the other half of the environment report — a scheduled job that
fails every run has to say so. `recordError()` increments one unnamed counter,
so the admin status page could show a number and not which queue produced it,
and the module's own screen showed a zero day count with nothing to
distinguish "nothing stored yet" from "every run failed". pg-boss has held the
answer the whole time: a job that exhausts its retries keeps its queue name,
its error and its timestamp. The admin status now names the failing queues
with counts and the last error, the environment overview reports its own
account's failed runs, and neither needed a change at any of the thirty
`recordError()` call sites — a visibility fix that has to be remembered per
call site is the same failure one level up.

Refs #650, #646
2026-07-26 14:37:57 +02:00
Marc-André Bombeck e260b2d5c4 perf(charts): one shared async boundary for the whole recharts surface
Every next/dynamic chart import used to point at its own module, so
Turbopack emitted a separate chunk group per boundary — the production
build carried EIGHT ~312 KB copies of the recharts module graph, and
/insights (the worst-LCP route) downloaded two of them (~92 KB gz of
pure duplication, one fully unused at load).

All chart loaders now resolve the single chart-runtime barrel
(src/components/charts/chart-runtime.ts), which owns the only path to
recharts: one shared chunk group, recharts exists exactly once in the
build (verified: 8 -> 1 recharts-fingerprinted chunks), cached across
routes. Charts stay recharts and stay pixel-identical; every call site
keeps its own matching skeleton and ssr:false flag.

Riders of the same rule:
- the mood-trio barrel (mood-charts.ts, the v1.16.7 precedent this
  generalises) folds into the runtime and is removed
- the vitals sparkline body extracts to delta-sparkline.tsx and loads
  through the runtime; the tile keeps the fixed 40 px band, so the
  async gap is an identically-sized empty box
- /cycle's BBT chart (the last static recharts import on a live route)
  defers through the runtime with a card-matching skeleton

H3
2026-07-07 12:32:43 +02:00
Marc-André Bombeck a4b204ffe4 refactor(ui): retire the raw dracula utilities for semantic tokens
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:

- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
  badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
  light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
  update-available pill and offline-geo hint → warning; the admin
  helpful-rate ramp → success/warning/destructive; the confidence
  meter mirrors the coverage-meter precedent (medium + low share the
  caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
  classes (no generated utility) for border-border + bg-muted/50

Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
2026-07-05 21:28:09 +02:00
Marc-André Bombeck 86d80d2c04 refactor(admin): migrate section cards onto the shared card primitives
The admin tree hand-rolled its card surfaces and stepped padding at sm:
instead of md:, so it read denser than every other route on a tablet.
Move the sections onto SettingsCard/SettingsCardHeader and the mini-rows
onto ListRow; no behaviour, data, or copy changes.
2026-07-01 23:44:55 +02:00
Marc-André Bombeck 45ca6ff24b fix(admin): name the configured geo provider in the system snapshot
The admin system snapshot hardcoded "ipwho.is" for the offline-geo-off
state, so an operator who points IP_GEO_LOOKUP_URL at another provider
still read the wrong host. Resolve the actual provider host from the env
and surface it through /api/version, so the snapshot reads
"Online (ip-api.com)" — the host it really queries.
2026-06-30 18:08:57 +02:00
Marc-André Bombeck 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
2026-06-27 20:37:35 +02:00
Marc-André Bombeck 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.
2026-06-11 13:57:15 +02:00
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00
Marc-André Bombeck 04765ea8ac style: one spacing, title and skeleton language across the app
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
2026-06-10 16:54:15 +02:00
Marc-André BombeckandClaude Opus 4.7 8ee7aaac44 chore(release): v1.6.0
Medication editor overhaul, route of administration, today-tile read-flip.

- Route-of-administration column (ORAL | INJECTION | OTHER) decoupled from
  treatmentClass; injection-site picker surfaces for any injection.
  Migration 0088 backfills ORAL via a constant column default.
- One-time injection (oneShot + deliveryForm = INJECTION) editor path.
- Profile-photo upload card on Settings → Account.
- Today-tile read-flip: /api/medications/intake?scope=today and
  /api/dashboard/summary gate through the canonical recurrence engine,
  matching the reminder worker for bi-weekly, rolling, RRULE, and one-time
  cadences.
- Measurement note cap 25 → 200; dashboard range colors on semantic tokens;
  admin sidebar icons + shared-helper consolidation; API-tokens multi-language
  snippets restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:10:12 +02:00
Marc-André Bombeck 6c7fc7dc84 chore(release): v1.4.45 — analytics 9 s perf fix + audit-marathon closure + v1.4.44 hotfix merged
Audit-marathon close: 14 waves + W10 QA + reconcile. Perf fix (analytics 9 s -> 2-3 s), Zod multi-issue rollout (41 routes), Withings parked-state automation, mobile-UI WCAG 2.5.5 close, QoL i18n + offline banner + account delete, security PII drop + replay-injection scan. Migration 0075 required. Tests 4815 -> 5076.
2026-05-21 23:13:21 +02:00
Marc-André Bombeck 9b6e237f5f chore(release): v1.4.42 — Knip enforcing, queryKey factory closed, iOS Workouts dedup, Withings off-response classification
Six touch-disjoint waves landed on develop before this release:

- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
  35 unused exports + 52 unused types triaged to 0/0; shadcn surface
  + zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
  `/api/dashboard/widgets` 422 now returns every Zod issue + worker
  audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
  migrated to the queryKeys factory; 9 new factory entries; ESLint
  rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
  sites (seven status helpers + bucket-series + bp-in-target);
  Suspense double-comment consolidated; doctor-report sanitiser regex
  rewritten to escape-form so diffs are reviewable; pr-detection-
  worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
  dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
  tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
  (transient/reauth/persistent) + WithingsApiError typed throw + the
  `pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
  silent-disable gap the wave was conceived to close.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.

pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
2026-05-21 19:33:07 +02:00
Marc-André Bombeck 4f09c3f862 chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
2026-05-15 22:59:10 +02:00
Marc-André BombeckandClaude Opus 4.7 3048dd6637 style(prettier): sweep formatting across the tree
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>
2026-05-10 11:05:40 +02:00
Marc-André Bombeck 65b4bf9679 fix(admin): system-status load-failed pairs alert with Retry button
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.
2026-05-10 00:37:08 +02:00
Marc-André BombeckandClaude Opus 4.7 8d9f86462f docs(planning): mark Wave-B B5a (medical-reference grounding) complete
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>
2026-05-10 00:05:40 +02:00
Marc-André BombeckandClaude Opus 4.7 bd24f130ee fix(a11y): drop duplicate h2 inside single-section /admin/<slug> routes
Phase 4b's `<SectionFrame>` already paints the canonical h1 + subtitle
for each `/admin/<slug>` route, but every section card kept its v1.4
in-card h2 with the same (or near-same) text. The result was the
section title announcing twice — once as h1, then immediately again as
h2 — both visually redundant and a screen-reader landmark anti-pattern.

Demoting the in-card label to a `<div>` preserves the visual look
(same Tailwind classes) while removing the duplicate heading. The
four sub-cards inside `IntegrationsGroupSection` (umami, glitchtip,
web-push, bug-report) keep their h2 because they live under the
"Integrations" h1 and genuinely deserve a heading level — that is the
non-duplicating case.

Closes H1 from .planning/phase-6-design-findings.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:44:05 +02:00
Marc-André BombeckandClaude Opus 4.7 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>
2026-05-09 17:41:02 +02:00
Marc-André BombeckandClaude Opus 4.7 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>
2026-05-09 16:59:25 +02:00
Marc-André BombeckandClaude Opus 4.7 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>
2026-05-09 02:10:05 +02:00
Marc-André BombeckandMarc-André Bombeck 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.

* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4

Three production bugs surfaced after the v1.4.0 deploy stabilized:

1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
   on /dashboard and /insights for users without weight readings —
   the optional chain stopped at `data` when it needed to also cover
   `data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).

2. `TypeError: y?.filter is not a function` on /medications — the
   medications query value was non-array (likely a stale
   service-worker response from an older API shape). Replaced with
   `Array.isArray(medications) ? medications : []` so the page is
   immune to any future shape drift.

3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
   so 1.4.2 is the single coherent release). The docker-compose
   healthcheck used `localhost` which busybox-wget resolves to IPv6
   ::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
   to 127.0.0.1.

This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:46:31 +02:00
Marc-André Bombeck 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 21bd46d (v1.4.0
release). Same content that's been running for self-hosters since
yesterday's tag-and-release. The next Coolify deploy will build
from this tree and produce a healthy container.

The v1.4.1 work is NOT lost:
  - PRs #144, #145, #137, #146, #147, #148, #149, #150 remain in
    git history.
  - Their commits are still tagged (`v1.4.1`), still on the GHCR
    multi-arch image (`ghcr.io/mbombeck/healthlog:1.4.1`), still in
    the GitHub Release notes.
  - Self-hosters who have already pulled the v1.4.1 image keep it.
  - Local development continues from main HEAD with the v1.4.1
    code — the regression only surfaced under the Coolify build
    flow.

Re-applying v1.4.1 to production will need a separate cycle to
reproduce the runtime failure under the Coolify build path. That
work is tracked in docs/ops/v141-followup-issues.md (added back
when the tree is reapplied) and the deploy gating in
.github/workflows/e2e.yml will catch this class of bug going
forward.

No DB migration. No env-var change. No API contract change.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:15:34 +02:00
Marc-André Bombeck 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.
2026-05-08 15:58:06 +02:00