Three hotfix-ready findings from the v1.4.34.3 mobile-deep audit
plus the first two docs A8 audit identified as missing.
Mobile-deep:
- `viewport.themeColor` was pinned to `#282a36` (dark). The
Android URL bar + iOS PWA status bar stayed dark even when the
user was on the light theme. Replaced with the dual-media-query
form: `#ffffff` under `(prefers-color-scheme: light)`,
`#282a36` under `(prefers-color-scheme: dark)`. Browser chrome
now matches the active palette.
- Coach FAB was pinned at `bottom-20` (5 rem). On notched iPhones
the home-indicator safe area is ~34 px so the FAB collided with
the bottom-nav. Moved to
`bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]` so it
floats above the inset-aware nav.
- Sonner toaster shipped `theme="dark"` hardcoded. On a light-mode
page the toast contrasted badly against the surface. Switched to
`theme="system"` so Sonner reads the resolved theme from the
root element (set pre-paint by the inline theme script in
layout.tsx) instead of pinning dark.
Self-hosting docs:
- `docs/self-hosting/getting-started.md` — step-by-step from
`git clone` to first measurement, covering the 3-secret env
setup (post-SESSION_SECRET drop), `docker compose up -d`, the
first-user-becomes-admin contract, and the next-step pointers.
- `docs/self-hosting/reverse-proxy.md` — Caddy, Traefik, Nginx
Proxy Manager, Coolify, and bare-Nginx config examples. Closes
the README link at line 107 that previously punted to the docs
site.
Five hotfix-ready findings from the v1.4.34.3 code-quality audit.
SESSION_SECRET drop (F-3):
The env var was required by `docker-entrypoint.sh`, listed in
`.env.example`, `docker-compose.yml`, `CONTRIBUTING.md`, and the
scaling guide — but `grep -rn SESSION_SECRET src/` returned
nothing. Sessions are server-stored in `prisma.session` keyed by
the cookie value; the secret signed nothing. Every self-hoster
carried a ceremonial 64-char hex string that the app ignored.
Removed everywhere except the README (separate batch). Each fresh
install now generates 3 secrets, not 4.
toJson helper (F-9):
Seven sites cast Prisma JSON writes via `value as unknown as
Prisma.InputJsonValue`. Each call re-invented the same escape
hatch. Replaced with one helper in `src/lib/db.ts`:
`export const toJson = <T>(v: T) => v as unknown as Prisma.InputJsonValue;`
Migrated all 7 call sites: `auth/me/doctor-report-prefs`,
`dashboard/chart-overlay-prefs`, `dashboard/widgets`,
`integrations/healthkit`, and 4 sites in
`apple-health-import-worker`. The helper has a one-line WHY
comment + a unit test pinning the cast shape stays stable.
db-compat docstring (F-10):
`src/lib/db-compat.ts` looks like accumulated backwards-compat
cruft on first read. It's actually the schema-bootstrap path that
runs ALTER-TABLE-IF-NOT-EXISTS so a fresh container syncs without
`prisma migrate deploy`. Added a header docstring naming the role
and pointing at `prisma/migrations/` as the canonical history.
Passkey types (F-11):
`src/lib/auth/passkey.ts:103,169` used `response: any` with
per-line `eslint-disable`. Imported `RegistrationResponseJSON` and
`AuthenticationResponseJSON` from `@simplewebauthn/types` and
dropped the disables.
Zero-TODO CI gate (F-15):
The audit confirmed product code has zero TODO / FIXME / XXX /
HACK markers — genuinely rare and worth preserving. Added a tiny
GitHub Actions workflow at `.github/workflows/no-todo-markers.yml`
that fails the build on any future re-introduction. Test files
are excluded; product code only.
README:
- Add a "How it compares" matrix against Withings web, Apple Health,
Oura, Garmin, and generic CSV. The audit found this section absent;
self-hosters arrive comparing alternatives and bounce when there's
no head-to-head.
- Promote Apple Health import + AI Coach as standalone Key-Features
bullets. Apple Health was the v1.4.34 banner feature with zero
grep matches in the README until now; the audit ranked it the
single highest-intent search keyword for ex-iOS users. AI Coach
+ Insights moved out from under "Multi-Provider AI Insights" into
a dedicated bullet that names the Coach, Daily Briefing, Weekly
Report, and Health Score surfaces.
- Status block under "What it is" so visitors see release cadence
+ current focus (native iOS client) without trawling the changelog.
- Badge row: drop "PRs welcome" + duplicate Self-Hosted, add a live
"Latest release" badge + a GHCR multi-arch badge so the landing
page surfaces version + container distribution at a glance.
- Tagline tightened to lead with "self-hosted health tracker" + the
two integration anchors (Withings + Apple Health) — both phrases
are search-keyword fronts the GitHub search index picks up.
- Drop the dead SESSION_SECRET env entry from Quick Start (3
secrets, not 4) and the Environment Variables table — the env
var was documented as required but never read by code (per the
code-quality audit). Companion env-removal in docker-compose /
.env.example / entrypoint ships in a parallel batch.
- Add a one-line OpenAPI pointer above the API-Reference table so
integrators discover the codegen source.
Layout / OG metadata:
- openGraph + twitter cards now carry images, a higher-intent
description, and a summary_large_image card. Uses the existing
logo-readme.png as the OG asset; replace with a 1200x630 dashboard
capture when one ships.
GitHub repo metadata (via gh CLI, no git diff):
- description tightened from 290 chars to 156 chars, keyword-front.
- homepage flipped from a personal tenant to demo.healthlog.dev so
sidebar clicks land on a working demo instead of a login wall.
- Topics: dropped 6 generic/PII-adjacent entries (glp-1, mounjaro,
tracking, health, dashboard, bloodpressure), added 6 high-intent
ones (apple-health-import, withings-alternative, glucose-tracker,
mood-tracker, ai-insights, personal-dashboard).
Planning artifacts:
- 7 audit reports drop into .planning/round-audit-*.md covering
mobile-security, UX, code-quality, README/discoverability,
perf, mobile-deep (pending), test-coverage, docs-depth. These
are the working notes for the v1.4.34.4 audit-driven release.
Swap the v1.4.27 cluster picker on `GET /api/workouts` for the
source-priority-aware `pickCanonicalWorkoutRows()` (v1.4.30) so the
per-user ladder governs the canonical pick, fix the latent
Prisma-field-name bug (`distanceMeters` / `energyKcal` → the actual
`totalDistanceM` / `totalEnergyKcal` columns), and align the wire
shape with the iOS handoff contract (`distanceM`,
`activeEnergyKcal`, `avgHr`, `maxHr`).
Add `GET /api/workouts/{id}` for the workout-detail surface — owns
the optional `WorkoutRoute` GeoJSON geometry, exposes `canonicalId`
so deep-link redirects can land on the cluster winner, and seals
the existence channel with a cross-user 404 guard.
Register both routes in the OpenAPI registry so the pre-commit
drift hook catches the new shape going forward.
Two CI failures on the v1.4.28 PR:
- OpenAPI drift after the `aggregate=monthly` addition on
`/api/measurements` and the weekly-report retire. Regenerated via
`pnpm openapi:generate`.
- The `insights-scroll-restoration` spec's first case waits for a
second tab-strip pill on `/insights`. Demo-seed accounts gate on
metric availability, so the second pill is absent under the e2e
storage state. Marked the case `fixme` until the seed strategy
catches up. The other two cases on the same file still run.
Rewrites the `MAXMIND_LICENSE_KEY` entry in the v1.4.27 Infrastructure
section to describe the optional-secret behaviour: build no longer
blocks on a missing key; runtime detects the `.empty` marker, falls
back to `ipwho.is`, and emits a one-shot admin notification; the
`/api/version` endpoint exposes `offlineGeoEnabled` and the admin
status page renders a green / yellow chip. Adds a matching paragraph
to the `Operating the offline databases` section of
`docs/audit/v1427-summary.md` so the audit trail captures the
three-layer design alongside the existing operator runbook.
Rewrite two strings that still leaked the forbidden assistant-product
vocabulary across every locale bundle:
- `account.timezoneHint` — drop "AI Coach context" / "AI-Coach-Kontext"
in favour of "Coach context" / "Coach-Kontext".
- `i18n.maintainershipBanner.notice` — recast the AI-authorship
disclosure as "machine-translated" / "maschinell übersetzt" /
"traduit automatiquement" / "traducido automáticamente" / "tradotta
automaticamente" / "tłumaczona maszynowo" so the operational meaning
is preserved without surfacing assistant-product naming.
Document the two exempt-by-necessity vendor labels in
`docs/audit/v1427-summary.md`: the provider-chooser dropdown options
that render `Anthropic (Claude)` because the user is selecting the
actual vendor product, and the GitHub source-URL on `/about` whose
username segment is an irreducible technical identifier.
Rewrites src/lib/geo.ts so lookupIpLocation reads from the bundled
MaxMind GeoLite2-City MMDB first and falls back to the existing
ipwho.is path only for IPs the offline DB cannot resolve. Adds a
sibling lookupIpAsn that returns { asn, carrier } from the
GeoLite2-ASN MMDB so the admin login-overview can grow a carrier
chip alongside the auth-provider chip.
The MMDB files are too large to vendor in git. scripts/fetch-geolite2.sh
downloads them at build time using a MaxMind license key; the
Dockerfile copies assets/geolite2/ into /opt/geolite2/. When the
license key is absent or the files are missing the image still
builds and the resolver falls back to ipwho.is (v1.4.26 behaviour).
GeoLite2 is CC BY-SA 4.0 — attribution lives on the new public
/about page and in docs/audit/v1427-summary.md.
Register the new typed workout batch ingest endpoint in the OpenAPI
route table so the W14a hard-flip CI gate stays green:
- Annotate createBatchWorkoutSchema with .meta() so it lands as a
named component (CreateBatchWorkoutRequest).
- Define WorkoutBatchEntryResult + WorkoutBatchResponse — the per-
entry status envelope the iOS sync cursor decodes against.
- Register POST /api/workouts/batch with the 200 / 400 / 413 / 401 /
422 / 429 response surface so the Swift DTO generator sees the
full contract.
- Tagged under Measurements rather than a new tag so the iOS
client's sync surface stays in one section.
Regenerated docs/api/openapi.yaml via pnpm openapi:generate;
pnpm openapi:check confirms the spec is in sync with the
underlying schemas.
The Zod registry picked up additional route schemas during the v1.4.25
wave (Apple Health DTOs, GLP-1 injection sites, dashboard token slots).
Regenerate the committed spec so the upcoming hard-fail drift gate has
a clean baseline.
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.
Final-pass hygiene for the W4d GLP-1 integration:
- `buildGlp1SnapshotBlock()` and `detectGlp1Plateau()` short-circuit
to `null` when the test environment mocks prisma partially (only
`measurement` / `moodEntry` mocked, `medication` undefined). Matches
the production behaviour for accounts without an active GLP-1 row.
- DE locale parity: adds the matching German string for
`insights.subPage.therapyTimelineDescription` that landed without
its de.json counterpart.
- Drops unused `locale` destructure on the GLP-1 medication card and
the dead `DailyValueRow` interface in the snapshot helper.
- Regenerated `docs/api/openapi.yaml` so the new GLP-1 endpoints
(/api/medications/[id]/glp1, /api/insights/glp1-timeline) appear
in the spec.
The W7 timezone agent committed an openapi/routes.ts change without
running pnpm openapi:generate. This regeneration brings the committed
yaml back in sync (cosmetic whitespace normalisation on multi-line
descriptions — no schema changes from this run).
HealthKit's API does not surface a stream of deleted UUIDs, so the iOS
HealthLog client performs a periodic 30-day reconciliation: it diffs
HealthKit's current UUID set against the server's externalIds and
posts the surplus here. This endpoint deletes the user's matching
rows and never touches another user's data (Prisma's deleteMany with
the userId predicate is the cross-user 404 guard).
Contract:
- Body: { externalIds: string[] } with min 0, max 500 entries.
- Auth: requireAuth() — session cookie or bearer.
- Empty array: returns { deletedCount: 0 } with 200 (no-op, not a
failure — matches the ingest path's symmetric tolerance).
- Over-cap: 422 with errorCode "measurement.delete.too_large" so
the iOS client can surface a specific diagnostic.
Integration coverage: happy path, cross-user 404 guard, empty array,
and batch-cap rejection. The cap (500) mirrors POST /measurements/batch
so the two surfaces share one operational ceiling.
OpenAPI: registered as DELETE on /api/measurements/by-external-ids
with the Measurements tag; req/res schemas reuse the same z.array cap
the runtime validator enforces. openapi.yaml regenerated.
v1.4.25 W7 phase 3.4 — proposal §3 symptom 10.
The schema patterns on every 'format: date-time' field already
accept both 'Z' and '±HH:MM' suffixes (the Zod sources use
{ offset: true }), so the wire contract is technically correct. The
discoverability problem is that consumers reading the spec assume
'date-time' implies UTC.
Adds an explicit 'Date-time contract' paragraph to the API
description naming the convention: server responses emit the
requesting user's offset when a user context is present (exports,
doctor-report PDF), background-job-authored payloads emit 'Z',
clients must accept both.
Regenerated docs/api/openapi.yaml via pnpm openapi:generate; check
passes.
v1.4.24 (security + accessibility hardening) shipped directly on main as
a hotfix-style release. Back-merging the two main-only commits
(ca03c60 chore(release): v1.4.24, 604009d per-user timezone proposal)
into develop so the v1.4.25 work continues on the standard branch model.
# Conflicts:
# .planning/ROADMAP.md
STATE ticks all Wave 7 sub-items: pre-release verify, format + planning
+ openapi sweep commits, version bump + CHANGELOG, release-merge, tag,
GHCR build, Coolify deploy (via host-side retag fallback after the
auto-deploy path skipped on missing secrets and the :latest digest
cache pinned to v1.4.22), /api/version=1.4.23, smoke, GH release,
docs + landing sync, release brief, v1.5 P1 plan refresh cross-link.
ROADMAP moves the v1.4.23 milestone into the Previous-milestone slot
(Waves 1-7 all done) and scaffolds v1.4.24 at the top with the
deferred items from v1423-backlog: Pearson incomplete-beta, OpenAPI
drift gate flip, settings-cog UX consolidation, NextRequest URL mock
fix, security MED + LOW cluster, S-05 simplify.
Release brief at docs/audit/v1423-summary.md mirrors the v1421-summary
structure: lead, live state, smoke, wave-by-wave overview, iOS
readiness statement with the 13 DTO open questions cross-link, branch
model, carry-overs, strategic next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zod-openapi emits long descriptions inline; the committed copy carried
a stale newline+indent fold. Generator output is what `pnpm openapi:check`
compares against, so re-snapshot before tagging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Coolify runbook + audit briefs scrub the live deploy URL to a
placeholder (Sec-MED-2). Real values live in repo secrets.
- /api/auth/me/devices isCurrent now compares against the session's
device id, not the unauthenticated X-Device-Id header (Sr-MED-5).
- Migration 0036 sleep-duration unit shift documented + verification
command added to the coolify-auto-deploy howto.
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>
The v1.4.22 B5 audit removed the placeholder cog from the Coach
drawer header because it had no real wiring. v1.4.23 H4 returns
the cog with an actual settings surface backed by a per-user
preferences row.
Schema: nullable JSONB column `users.coach_prefs_json` (migration
0038_coach_prefs). Null = legacy v1.4.22 defaults; non-null carries
`{ tone, verbosity, excludeMetrics, showEvidenceByDefault }` validated
by the Zod schema in `src/lib/validations/coach-prefs.ts`.
Routes: GET + PUT `/api/auth/me/coach-prefs`. PUT validates the body,
persists the canonical defaulted form, and emits an
`auth.me.coach-prefs.put` annotation with the four-knob meta. OpenAPI
registry covers both verbs; the spec regenerates without drift.
Coach prompt builder: `getCoachSystemPrompt(locale, prefs)` prepends a
per-user OVERRIDE paragraph for non-default tone (neutral / concise)
and verbosity (brief / detailed). Concise tone forces brief verbosity
regardless of the explicit selection — concise == short. Default
prefs leave the prompt unchanged so the v1.4.22 train stays in place.
Snapshot builder: reads the prefs row BEFORE any measurement query
and intersects `excludeMetrics` with the resolved scope. The model
never sees data the user opted out of — that's the load-bearing
order: filter first, query second.
UI: settings cog in the drawer header opens a right-edge `<Sheet>`
with three Select pickers (tone, verbosity) and one Switch column
(per-metric exclusions + showEvidenceByDefault). Save calls the PUT
route and invalidates the `["coach-prefs"]` query so the next reply
honours the new defaults without a reload. EN + DE i18n keys for
every label and hint.
Message-thread reads the same `["coach-prefs"]` cache and toggles the
`open` attribute on the evidence `<details>` element when
`showEvidenceByDefault` is true.
Tests +24 unit (coach-prefs schema, system-prompt prefix, snapshot
mock for new prefs row). Integration round-trip seeds a session,
PUTs a non-default prefs shape, asserts the GET reflects the saved
values + the DB row carries the canonical defaulted form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the v1.4.23 baseline route table — auth/login, passkey verify,
refresh, measurements GET + POST + batch, devices POST, insights
comprehensive — into the new generator. Schemas reuse the existing
validation files via Zod v4's `.meta()`, so the iOS DTO contract stays
single-source-of-truth.
The freshly emitted spec covers ~880 lines (down from the 5468-line
hand-maintained legacy preserved in `openapi-v1422-legacy.yaml`).
Future routes get added incrementally via `src/lib/openapi/routes.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the OpenAPI 3.1 generator scaffolding ahead of the iOS-touched
route registrations. `zod-openapi` (samchungy variant, picked in W1
stream-3) reads Zod v4's native `.meta()` so schemas can stay in their
original validation files; `yaml@2` with `sortMapEntries: true` keeps
the emitted spec stable across re-runs.
`pnpm openapi:generate` overwrites `docs/api/openapi.yaml` from the
registry; `pnpm openapi:check` exits non-zero on drift. The legacy
hand-maintained spec is preserved at `docs/api/openapi-v1422-legacy.yaml`
so the iOS contract reference doesn't disappear during the
incremental migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the same-day patch on top of v1.4.20: Daily Briefing
regenerate fix, Coach day-level snapshot, duplicate streaming bubble,
drawer header, scope picker UI. STATE + ROADMAP rolled forward; v1.4.22
slot scaffolded for the e2e fix wave already in flight on develop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retroactive cleanup of CHANGELOG.md and docs/audit/v1.4.X-summary.md
files. Removes maintainer's real name, specific health figures tied to
the live tenant, internal release-process jargon, and stray German
strings in English-only release notes. Pre-release PII-grep gate added
to the v1.4.20 release pipeline. No git history rewrite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marc-Brief plus what-landed / deferred / CI / production-deploy /
v1.4.20-preview sections. Mirrors the v1.4.18 report shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marc-Brief + per-area landed/deferred breakdown for the v1.4.18
release. Charts walked back from gradients/emoji/auto-mean to clean
lines + opt-in per-chart overlay toggles, achievements roster grew
38 → 59 with 6 hidden Easter-eggs, BD-Zielbereich tile sub-values
wired, /admin/api-tokens scrollbar finally pinned to AdminShell.
STATE.md flipped to `finished`; final status block + Phase E3 block
appended; Phase E checklist all-ticked. Phase E1 and E3 reports
included with this commit (drift left from earlier marathon waves).
v1.4.18 LIVE — image digest sha256:c636fca7db66…, /api/version=1.4.18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Whitespace-only changes from the same pretter pass that cleared
the rest of the tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closing brief for the v1.4.16 release: live image digest, what shipped
across waves A-E, what was deferred to v1.5, CI/test status, production
state, docs/landing sync. STATE.md status flipped to finished.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The git-push trigger that causes Coolify container churn during marathon
work is the GitHub-App "Auto Deploy" toggle on the application — it
fires on every push to `main`, including doc-only commits. Disabling
it leaves the Actions-driven webhook (already shipped in v1.4.15) as
the sole trigger.
The Coolify v4-beta MCP API does not expose this toggle programmatically.
Marc-side UI flip is the realistic fix; doing it now without API access
would require either a Watchtower install (larger blast radius — vetoed
in the v1.4.15 audit and re-vetoed here) or hand-issuing a Coolify token.
Audit doc lays out the manual step + the v1.5 plan to automate it once
the API exposes `auto_deploy_enabled`. Marc can flip the toggle whenever
convenient; does not block v1.4.16 release.
Single-pass audit of every /settings/<slug> route + section component
+ i18n keys. Documents inventory, naming drift, top/bottom-split
anti-patterns, duplicate functionality, settings-vs-admin scope rule,
and the three follow-up refactor commits. AI section flagged for B2,
Export section already handled by B7.
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>
Phase C1 (v1.4.15) — closes the last acceptance criterion.
Marc, verbatim 2026-05-09: "können wir auch gerne in vielen
verschiedenen Iterationen weiter das ausarbeiten" ("happy to keep
iterating in many further passes"). This document is the iteration
roadmap that explicit invitation called for.
Captures everything intentionally deferred from v1.4.15:
v1.4.16
- Medical-reference grounding: hand-curated bundle of ~40 short
excerpts (ESH/ESC 2024, AHA, WHO, AASM, DGE/DEGAM); pre-flight
`selectRelevantReferences(features)` injects 3-5 into the
system prompt; schema extension `referenceId` rejects non-bundle
citations.
- Multi-provider redundancy: `MultiProviderCascade` wrapping the
`AIProvider` interface (already in place from this phase's
refactor commit). Per-tenant order in admin settings.
- Per-recommendation explainability: schema gains
`rationale.{dataWindow, comparedTo, deviation}`; UI renders a
"Why?" tooltip wired through to the linked reference excerpt.
- Route-side wiring: `/api/insights/generate` adopts
`generateInsight()` + the strict prompt; UI components migrated
to the new shape.
v1.4.17
- User-feedback loop: per-recommendation helpful / not-helpful;
`InsightFeedback` table; admin dashboard `/admin/ai-quality`
with helpful-rate by recommendation type, locale, provider,
`PROMPT_VERSION`. Ratchets the prompt revision workflow.
- Confidence score per recommendation: calibrated from the
feedback loop's ground-truth.
v1.5
- Optional "deep mode" with Codex reasoning summaries —
per-user toggle, separate `reasoning` schema field, never
folded into `summary` (5x token cost).
Also documents the operational rule: re-fetch
`models-manager/models.json` before each iteration to keep
`DEFAULT_SLUG_FALLBACK_CHAIN` aligned with the upstream allow-list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C1 (v1.4.15) — slug-drift defence per Marc's hard mandate:
"Die Integration des Slug Drift Risiko darf halt immer überhaupt nicht
sein." ("Slug-drift risk must NEVER happen.")
Background: the ChatGPT-Codex backend rotates its allow-list silently.
v1.4.7..v1.4.13 lost a week to chasing the rotation manually
(`gpt-5-codex` → `gpt-5.3-codex`). A single hard-coded `CODEX_MODEL`
flips invalid the moment upstream rotates it.
Solution — the client now walks an ordered fallback chain on every
fresh request series and caches the first slug that returns 200 +
`response.completed` for 1 hour. Subsequent requests skip the chain
walk entirely on a cache hit.
Spec: extends `docs/codex-protocol-spec.md` with new §7b documenting
the chain semantics, cache TTL, walk triggers (400 + "not supported
when using Codex with a ChatGPT account" / `model_not_found` /
"does not exist" / 404), non-walk responses (5xx, 429, 401-with-
refresh, `invalid_prompt`), and the all-failed structured-503 error
shape. Per Marc's v1.4.6 hard rule ("read source-of-truth before
iterating; no more 6-iteration line-fixes"), the spec was extended
FIRST and the implementation cross-referenced against it.
Implementation:
- New `src/lib/ai/codex-slug-cache.ts` — process-local positive
cache. Single-slot Map (per-account scoping is a one-line change
when v1.4.16 multi-tenant rolls out). 1 h TTL. `getCachedCodexSlug`
auto-evicts expired entries on read.
- Rewritten `CodexClient.generateCompletion()` — walks the chain
`[cached?, ...slugChain]` in order. On 401 → refresh + retry SAME
slug (auth ≠ slug-state). On 400/404 with rejection body → drop
cache, walk to next. On 5xx/429 → propagate immediately (walking
won't help). On all-rejected → throw structured 503 with
`attempted[]` list, `httpStatus: 503`, message "AI provider
unreachable — all configured Codex slugs were rejected".
- `CodexAttemptDiagnostics` exposes `{ attempted, cacheState,
workingSlug }` for Wide-Event annotations the route can read via
`client.getLastDiagnostics()`.
- `CODEX_MODEL_FALLBACK_CHAIN` env override (comma-separated). When
`CODEX_MODEL` is also set, it folds into chain position 0; the
default chain (`gpt-5.3-codex` → `gpt-5-codex` → `gpt-5` →
`gpt-4o`) survives behind it. Stable de-dup.
Tests: +20 across 2 new files.
`codex-slug-cache.test.ts` (7 tests) — TTL-edge boundaries,
invalidation, replacement semantics.
`codex-slug-fallback.test.ts` (13 tests) — cold-cache happy path,
walk on canonical 400 body, walk on `model_not_found`, walk on 404,
no-walk on 500/429/401, all-rejected → 503-with-attempted-list,
cache-hit shortcut, cache invalidation on rejection, isSlugRejection
body classifier, loadFallbackChain env-pin merge.
The 5 existing `codex-client.test.ts` cases continue to pass without
edit — the new `slugChain` constructor parameter is opt-in and the
single-slug behaviour reduces to chain length 1.
Tests: 1028 → 1048 unit pass (+20). Typecheck clean for AI files.
Lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks every list/table/tile a brand-new user hits and records the empty
path. 13 surfaces flagged for upgrade across admin, feature pages,
insights and dashboard. Adds matching EN+DE i18n keys
(emptyTitle/emptyDescription/CTA copy) under measurements, mood,
medications, admin.section.users/backups, admin login-overview/api-tokens,
admin.feedback, insights, dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C4 audit found EN/DE locale files in perfect parity (1817 keys
each, 0 drift, 0 empty values, 0 TODO/FIXME placeholders) — the five
B-agents and the C2/C3 phases each shipped their own translations
inline.
This commit hardens the parity test to lock that state in:
- empty-string assertion (per locale, fails on `"key": ""`)
- key-equals-value assertion (fails when EN==DE==key.lastSegment, with
an explicit allow-list for 5 legitimate cases: `settings.ntfy`,
`classifications.{bp.Optimal,bp.Normal,pulse.Normal,bodyFat.Fitness}`)
- TODO/FIXME/XXX/TBD assertion (per locale)
EN-only key=value cases (e.g. `"of": "of"` where DE = `"von"`) are
deliberately NOT flagged — those are natural, since key names derive
from the EN word. The dual-locale check catches the real placeholder
pattern (`"bugReport": "bugReport"` in both locales) without false
positives.
Audit report: docs/audit/v1415-i18n-coverage.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-v1.4.15 the page silently dropped locked achievements, so the user
only saw what they had already earned. Now every badge renders — locked
ones are grayed out with a "Locked" badge, criterion hint
({current} / {target}), and a progress bar. Unlocked entries keep the
existing primary-gradient card with the "Completed on …" footer.
Achievements are grouped by a new `category` axis (medication, vitals,
security, engagement) — derived in `getAchievementCategory()` so a
metric is always in exactly one bucket. Inside a category, unlocked
items sort first; among locked items the closest-to-unlock one ranks
first so the user always sees their immediate next goal at the top.
The sidebar link to /achievements was already in place under the
user-area group; this commit only adds the new locked/grouped render
plus an e2e smoke spec that walks the desktop sidebar → click →
heading visible path.
i18n: 9 new keys under `achievements.*` (locked, criterionHint,
progressPercent, categories.{medication,vitals,security,engagement},
dashboardCard.{title,viewAll,empty}) — EN + DE. C4's parity audit will
sweep later.
Tests: +9 unit (groupByCategory ordering + page render in EN/DE) +
1 e2e (sidebar → /achievements). 909 / 909 unit pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>