chore(release): v1.4.15

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>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 22:27:13 +02:00
1 parent 1b1214bcb3
commit 4dcaa08b42
32 files changed
+639 -295

No files matched your search

+42 -42
View File
@@ -53,21 +53,21 @@ Reset einplanen.
### Release
| SHA | Summary |
| --------- | ------------------------------------ |
| `e5fae9b` | chore(release): v1.4.14 |
| SHA | Summary |
| --------- | --------------------------------------------------------------- |
| `e5fae9b` | chore(release): v1.4.14 |
| `f0d53fb` | chore(release): rebrand artifacts as v1.4.14 (patch, not minor) |
### Codex-OAuth verification (Phase 1)
| SHA | Summary |
| --------- | ---------------------------------------------------------------------- |
| SHA | Summary |
| --------- | ------------------------------------------------------------------------ |
| `5df74f7` | fix(ai): default Codex model to `gpt-5.3-codex` for ChatGPT-account auth |
### v1.4.6 deferred bugfix backlog (Phase 2)
| SHA | Summary |
| --------- | -------------------------------------------------------------------- |
| SHA | Summary |
| --------- | -------------------------------------------------------------------------------------------- |
| `512a6a6` | fix(admin): wipe-all-data clears notification channels, push subs, telegram deletions (T2.1) |
| `cb6a59a` | fix(insights): correct DST handling in cross-metric daily bucket pairing (T2.2) |
| `5403821` | fix(insights): map AI provider errors to 422/503 instead of 500 (T2.3) |
@@ -75,17 +75,17 @@ Reset einplanen.
### Admin Panel refactor (Phase 4b — folds T2.6 + T2.7)
| SHA | Summary |
| --------- | ------------------------------------------------------------------------ |
| SHA | Summary |
| --------- | ----------------------------------------------------------------------------------- |
| `d8b71b3` | feat(admin): scaffold per-section dynamic routes (admin-shell + [section]/page.tsx) |
| `12b280b` | refactor(admin): move sections to dynamic-route pattern + reduce overview |
| `69c5225` | feat(admin): backups view + manual backup trigger (T2.6 deferred) |
| `5957e18` | feat(admin): users management with filters + force-logout action (T2.7 deferred) |
| `a34cd8c` | refactor(i18n): namespace admin keys under `admin.section.<slug>.*` |
| `73965cd` | feat(admin): expandable sidebar nav for admin sections |
| `961e2a9` | fix(admin): status-card CTAs use real routes (not `#anchor`) |
| `7a47532` | feat(admin): redirect legacy section-anchor paths to dynamic routes |
| `1a7e3d6` | test(admin): tighten status-card-grid + add per-section render smoke |
| `12b280b` | refactor(admin): move sections to dynamic-route pattern + reduce overview |
| `69c5225` | feat(admin): backups view + manual backup trigger (T2.6 deferred) |
| `5957e18` | feat(admin): users management with filters + force-logout action (T2.7 deferred) |
| `a34cd8c` | refactor(i18n): namespace admin keys under `admin.section.<slug>.*` |
| `73965cd` | feat(admin): expandable sidebar nav for admin sections |
| `961e2a9` | fix(admin): status-card CTAs use real routes (not `#anchor`) |
| `7a47532` | feat(admin): redirect legacy section-anchor paths to dynamic routes |
| `1a7e3d6` | test(admin): tighten status-card-grid + add per-section render smoke |
### UX polish (Phase 5)
@@ -100,44 +100,44 @@ Reset einplanen.
### Multi-agent QA reconcile (Phase 6)
| SHA | Summary |
| --------- | ------------------------------------------------------------------------ |
| `88c8db1` | refactor: apply simplify-review safe-to-apply suggestions (11 of 11) |
| `b3f282a` | fix(security): block force-logout from targeting the admin's own session |
| `8e17f22` | fix(admin): rate-limit `/api/admin/backups/run` to 3/min per admin |
| `f0cfd26` | fix(security): redact standalone `hlk_` / `hlr_` native API tokens in logs |
| `bd24f13` | fix(a11y): drop duplicate h2 inside single-section `/admin/<slug>` routes |
| SHA | Summary |
| --------- | ---------------------------------------------------------------------------------- |
| `88c8db1` | refactor: apply simplify-review safe-to-apply suggestions (11 of 11) |
| `b3f282a` | fix(security): block force-logout from targeting the admin's own session |
| `8e17f22` | fix(admin): rate-limit `/api/admin/backups/run` to 3/min per admin |
| `f0cfd26` | fix(security): redact standalone `hlk_` / `hlr_` native API tokens in logs |
| `bd24f13` | fix(a11y): drop duplicate h2 inside single-section `/admin/<slug>` routes |
| `499cfad` | fix(a11y): bump `/admin/users` + `/admin/login-overview` tap targets to WCAG 2.5.5 |
| `ef96e87` | fix(admin): default api-tokens + login-overview routes to expanded |
| `ef96e87` | fix(admin): default api-tokens + login-overview routes to expanded |
### End-to-end test coverage (Phase 3)
| SHA | Summary |
| --------- | -------------------------------------------------------- |
| `535465c` | test(e2e): seeded test user + auth storageState fixture |
| `3999ea7` | test(e2e): authenticated dashboard + add-measurement flow |
| `2b6f959` | test(e2e): mocked Codex device-flow + insights regenerate specs |
| `a106547` | test(e2e): doctor-report PDF + mobile-viewport smoke |
| SHA | Summary |
| --------- | --------------------------------------------------------------------- |
| `535465c` | test(e2e): seeded test user + auth storageState fixture |
| `3999ea7` | test(e2e): authenticated dashboard + add-measurement flow |
| `2b6f959` | test(e2e): mocked Codex device-flow + insights regenerate specs |
| `a106547` | test(e2e): doctor-report PDF + mobile-viewport smoke |
| `ca3c599` | test(e2e): extend axe-core to authenticated surfaces; fix login regex |
| `7af1f59` | fix(a11y): close violations the new e2e axe-core sweep surfaced |
| `7af1f59` | fix(a11y): close violations the new e2e axe-core sweep surfaced |
### Performance (Phase 4)
| SHA | Summary |
| --------- | -------------------------------------------------------- |
| `bb2b1de` | perf(insights): defer Recharts ScatterChart imports via `next/dynamic` |
| SHA | Summary |
| --------- | ----------------------------------------------------------------------- |
| `bb2b1de` | perf(insights): defer Recharts ScatterChart imports via `next/dynamic` |
| `519e36e` | perf(dashboard): skip checklist API fetches once onboarding is complete |
| `1a85da0` | fix(insights): preserve chart visual parity in deferred bundle |
| `41fa203` | docs(audit): v1.4.14 performance audit report |
| `1a85da0` | fix(insights): preserve chart visual parity in deferred bundle |
| `41fa203` | docs(audit): v1.4.14 performance audit report |
### CI / housekeeping
| SHA | Summary |
| --------- | -------------------------------------------------------- |
| SHA | Summary |
| --------- | ----------------------------------------------------------------- |
| `3cacdf2` | fix(tests): share next/headers cookieJar across integration suite |
| `c847b80` | chore(format): trend-card test file prettier sweep |
| `badd380` | style(admin): apply prettier to phase-6 reconcile changes |
| `6b88e56` | chore(format): prettier sweep over phase-2 touched files |
| `c847b80` | chore(format): trend-card test file prettier sweep |
| `badd380` | style(admin): apply prettier to phase-6 reconcile changes |
| `6b88e56` | chore(format): prettier sweep over phase-2 touched files |
---
+6 -6
View File
@@ -42,18 +42,18 @@ The current application config (UUID `pg8wggwogo8c4gc4ks0kk4ss`,
mode for `dockercompose`-typed apps.
- `manual_webhook_secret_github: null` — no webhook secret configured today.
What Coolify *does* support natively (per the Coolify docs):
What Coolify _does_ support natively (per the Coolify docs):
1. **GitHub App auto-deploy**: pushes to `main` trigger an immediate redeploy.
For us this would deploy from a SHA *before* GHCR finishes building, since
For us this would deploy from a SHA _before_ GHCR finishes building, since
GHCR runs in the same push event in parallel. Race-prone.
2. **Deploy webhook URL**: every Coolify resource exposes
`GET https://<coolify-fqdn>/api/v1/deploy?uuid=<resource-uuid>` with
Bearer-token auth. This is a *trigger-only* surface — it queues a
Bearer-token auth. This is a _trigger-only_ surface — it queues a
redeploy. Coolify treats this as an authenticated remote-control command;
the recipe in the docs is "GitHub Actions builds the image, pushes to
GHCR, then `curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}'
--header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'`."
--header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'`."
3. **Notification webhook (outgoing)**: Coolify can POST a deployment-event
payload to a configured URL on success and failure. This is how we get a
notification back into the app for audit logging + admin alerts.
@@ -86,8 +86,8 @@ proves unreliable):** Watchtower with a per-image label
step, add a final `Trigger Coolify deploy` step that runs only on the
publish path (push to `main` or to a `v*` tag — never on PRs). It uses
`secrets.COOLIFY_WEBHOOK` (the full Bearer-protected URL with `?uuid=…`)
+ `secrets.COOLIFY_TOKEN`. Failure of the deploy trigger does **not**
fail the build (the image is already published; deploy is a follow-up).
- `secrets.COOLIFY_TOKEN`. Failure of the deploy trigger does **not**
fail the build (the image is already published; deploy is a follow-up).
- Document required secrets in `.env.example`.
2. **`feat(deploy): admin notification + audit log on deploy success/failure`**
+12 -11
View File
@@ -24,12 +24,12 @@ more than coverage breadth.
## Per-workflow pass-rate
| Workflow | Total | Success | Failure | Cancelled | Completed pass-rate |
| ------------------------------ | ----: | ------: | ------: | --------: | ------------------: |
| Integration tests | 47 | 47 | 0 | 0 | 100 % |
| Security & Quality | 46 | 43 | 3 | 0 | 93 % |
| Build & Publish Docker Image | 60 | 30 | 3 | 26 | 91 % |
| e2e | 47 | 0 | 34 | 13 | 0 % |
| Workflow | Total | Success | Failure | Cancelled | Completed pass-rate |
| ---------------------------- | ----: | ------: | ------: | --------: | ------------------: |
| Integration tests | 47 | 47 | 0 | 0 | 100 % |
| Security & Quality | 46 | 43 | 3 | 0 | 93 % |
| Build & Publish Docker Image | 60 | 30 | 3 | 26 | 91 % |
| e2e | 47 | 0 | 34 | 13 | 0 % |
`Completed pass-rate = success / (success + failure)` — strips out
the "cancelled" entries that come from the `concurrency.cancel-in-progress`
@@ -89,6 +89,7 @@ in 17 minutes. Both runs wrote to the same gha cache scope
(`buildkit` default) with `mode=max`, deadlocking the exporter.
**Fix shipped (`249c42b`)**:
- `cache-to: type=gha,mode=max,scope=build-${{ github.ref_name }}`
- `cache-from:` reads BOTH `build-${ref}` AND `build-main` so
tag/PR runs warm-start off main without contending on writes.
@@ -117,12 +118,12 @@ fragility. No action needed beyond what's already in the workflow.
- [x] **e2e dark colorScheme**: `41945b2`. Unblocks the 0 % gate.
- [x] **docker-publish per-ref cache + timeout**: `249c42b`. Bounds
deadlocks at 30 min and prevents the v1.4.14 hang from recurring.
deadlocks at 30 min and prevents the v1.4.14 hang from recurring.
- [x] **post-publish-verify workflow**: `ffa4aac`. Pulls the just-
published image, boots it against an ephemeral Postgres, probes
`/api/version` + `/api/health`. Informational (no `continue-on-error`
blocking), surfaces "manifest pushed but image won't boot" failures
the build step alone can't see.
published image, boots it against an ephemeral Postgres, probes
`/api/version` + `/api/health`. Informational (no `continue-on-error`
blocking), surfaces "manifest pushed but image won't boot" failures
the build step alone can't see.
### Defer to v1.4.16 (out of C3 scope, owned elsewhere)
+16 -16
View File
@@ -12,21 +12,21 @@ not a single grey sentence floating in a card.
## Findings
| Route / surface | Component | State before | What was wrong | What changed |
| --- | --- | --- | --- | --- |
| `/admin/users` | `UserManagementSection` | Loading-only — when `users` resolved to `[]` the table simply rendered an empty `<tbody>` with the header still visible. | Empty case never specified; on a fresh deployment with one admin self-row it never reproduced for Marc, but the `admin/user` filter did regularly. | New `<EmptyState>` mounts inside the card when `filteredUsers.length === 0`, with `Users` icon, filter-aware copy, and a "Reset filter" CTA when a filter is active. |
| `/admin/backups` | `BackupsSection` | Plain `<p>No backups recorded yet.</p>`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `<EmptyState>` (`Database` icon) that includes a primary "Backup now" button — duplicates the header CTA but inside the empty card so the new admin doesn't need to look up. |
| `/admin/login-overview` | `LoginOverviewSection` | Plain `<p>No entries found.</p>` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `<EmptyState>` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. |
| `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `<p>No API tokens found.</p>`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `<EmptyState>` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. |
| `/admin/feedback` | `FeedbackInboxSection` | Plain `<p>No feedback in this category.</p>` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `<EmptyState size="compact">` with `Inbox` icon. Per-tab description. |
| `/admin` overview | `RecentAuditPreview` | Plain `<p>No recent audit entries.</p>`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `<EmptyState size="compact" variant="plain">` with `ScrollText` icon. |
| `/measurements` | `MeasurementList` | `<div class="border-dashed">{t("measurements.noMeasurements")}</div>`. | No CTA and the top-level "Add measurement" button is the only way out. | `<EmptyState>` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. |
| `/mood` | `MoodList` | `<div class="border-dashed">{t("mood.noEntries")}</div>`. | No CTA. | `<EmptyState>` with `Smile` icon, filter-aware description, "Log your first mood" CTA. |
| `/medications` | `MedicationsPage` | Custom inline icon + button block. | Already friendly but didn't share the new primitive — diverging styles. | Refactored to `<EmptyState>` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. |
| `/insights` (top-level) | `InsightsPage` | Plain `<div class="text-muted-foreground py-20 text-center">{t("common.noData")}</div>` when `data` is undefined. | Looked like a hung page. | `<EmptyState>` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. |
| `/insights` (BMI no-height) | Inline `<p>` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `<EmptyState size="compact" variant="plain">` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. |
| `/achievements` | `AchievementsPage` (no-grouped state) | Plain `<p>No achievements unlocked yet.</p>` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `<EmptyState>` with `Trophy` icon — keeps the same `noneUnlockedYet` string to avoid C4 i18n churn. |
| Dashboard tile-strip / charts | `DashboardPage` | Tile strip + chart row both hidden when every metric has zero data; only `<GettingStartedChecklist>` plus the welcome banner show. The strip itself stays a 0-px row. | Brand-new users see a near-blank page below the welcome banner — feels broken even though the checklist is technically present. | New `<EmptyState>` block rendered when `trendCards.length === 0 && charts.length === 0`. Routed to "Add your first measurement" via the existing quick-entry dialog (no nav). Does not race the checklist (which has its own self-gating). |
| Route / surface | Component | State before | What was wrong | What changed |
| ----------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `/admin/users` | `UserManagementSection` | Loading-only — when `users` resolved to `[]` the table simply rendered an empty `<tbody>` with the header still visible. | Empty case never specified; on a fresh deployment with one admin self-row it never reproduced for Marc, but the `admin/user` filter did regularly. | New `<EmptyState>` mounts inside the card when `filteredUsers.length === 0`, with `Users` icon, filter-aware copy, and a "Reset filter" CTA when a filter is active. |
| `/admin/backups` | `BackupsSection` | Plain `<p>No backups recorded yet.</p>`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `<EmptyState>` (`Database` icon) that includes a primary "Backup now" button — duplicates the header CTA but inside the empty card so the new admin doesn't need to look up. |
| `/admin/login-overview` | `LoginOverviewSection` | Plain `<p>No entries found.</p>` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `<EmptyState>` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. |
| `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `<p>No API tokens found.</p>`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `<EmptyState>` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. |
| `/admin/feedback` | `FeedbackInboxSection` | Plain `<p>No feedback in this category.</p>` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `<EmptyState size="compact">` with `Inbox` icon. Per-tab description. |
| `/admin` overview | `RecentAuditPreview` | Plain `<p>No recent audit entries.</p>`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `<EmptyState size="compact" variant="plain">` with `ScrollText` icon. |
| `/measurements` | `MeasurementList` | `<div class="border-dashed">{t("measurements.noMeasurements")}</div>`. | No CTA and the top-level "Add measurement" button is the only way out. | `<EmptyState>` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. |
| `/mood` | `MoodList` | `<div class="border-dashed">{t("mood.noEntries")}</div>`. | No CTA. | `<EmptyState>` with `Smile` icon, filter-aware description, "Log your first mood" CTA. |
| `/medications` | `MedicationsPage` | Custom inline icon + button block. | Already friendly but didn't share the new primitive — diverging styles. | Refactored to `<EmptyState>` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. |
| `/insights` (top-level) | `InsightsPage` | Plain `<div class="text-muted-foreground py-20 text-center">{t("common.noData")}</div>` when `data` is undefined. | Looked like a hung page. | `<EmptyState>` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. |
| `/insights` (BMI no-height) | Inline `<p>` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `<EmptyState size="compact" variant="plain">` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. |
| `/achievements` | `AchievementsPage` (no-grouped state) | Plain `<p>No achievements unlocked yet.</p>` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `<EmptyState>` with `Trophy` icon — keeps the same `noneUnlockedYet` string to avoid C4 i18n churn. |
| Dashboard tile-strip / charts | `DashboardPage` | Tile strip + chart row both hidden when every metric has zero data; only `<GettingStartedChecklist>` plus the welcome banner show. The strip itself stays a 0-px row. | Brand-new users see a near-blank page below the welcome banner — feels broken even though the checklist is technically present. | New `<EmptyState>` block rendered when `trendCards.length === 0 && charts.length === 0`. Routed to "Add your first measurement" via the existing quick-entry dialog (no nav). Does not race the checklist (which has its own self-gating). |
## Out of scope (deferred)
@@ -43,7 +43,7 @@ not a single grey sentence floating in a card.
with height but no weight is intentionally minimal — the chart
itself shows the empty axis.
- Tab strip `0` badges on `/admin/feedback` use the same `<EmptyState
size="compact">` per panel; not duplicating in this audit.
size="compact">` per panel; not duplicating in this audit.
## Tests added
+16 -16
View File
@@ -59,12 +59,12 @@ These appeared in the grep but are deliberately excluded:
5 entries where both locales return the key's last segment unchanged:
| Path | Value | Reason |
| ------------------------------- | --------- | --------------------------------------------------- |
| `settings.ntfy` | `ntfy` | brand name, not translated |
| `classifications.bp.Optimal` | `Optimal` | identical word in EN and DE |
| `classifications.bp.Normal` | `Normal` | identical |
| `classifications.pulse.Normal` | `Normal` | identical |
| Path | Value | Reason |
| --------------------------------- | --------- | --------------------------------------------------- |
| `settings.ntfy` | `ntfy` | brand name, not translated |
| `classifications.bp.Optimal` | `Optimal` | identical word in EN and DE |
| `classifications.bp.Normal` | `Normal` | identical |
| `classifications.pulse.Normal` | `Normal` | identical |
| `classifications.bodyFat.Fitness` | `Fitness` | identical (anglicism in DE medical/fitness context) |
A further 25 entries are EN==key but DE has an actual translation
@@ -73,16 +73,16 @@ fine.
### v1.4.15 new namespaces — coverage check
| Namespace | Origin | EN keys | DE keys | Drift | Empty |
| ------------------------------- | -------------- | ------: | ------: | ----: | ----: |
| `admin.section.backups.*` | B1 backups | 35 | 35 | 0 | 0 |
| `achievements.*` | B4 | 106 | 106 | 0 | 0 |
| `onboarding.tour.*` | B5 | 18 | 18 | 0 | 0 |
| `admin.overview.*` | A2 | 16 | 16 | 0 | 0 |
| `settings.notificationStatus.*` | B3 | 13 | 13 | 0 | 0 |
| `settings.integrationStatus.*` | B2 | 6 | 6 | 0 | 0 |
| `doctorReport.*` | B6 | 90 | 90 | 0 | 0 |
| `quickAdd.*` (`common.add` etc) | A3 | (`common`) | — | 0 | 0 |
| Namespace | Origin | EN keys | DE keys | Drift | Empty |
| ------------------------------- | ---------- | ---------: | ------: | ----: | ----: |
| `admin.section.backups.*` | B1 backups | 35 | 35 | 0 | 0 |
| `achievements.*` | B4 | 106 | 106 | 0 | 0 |
| `onboarding.tour.*` | B5 | 18 | 18 | 0 | 0 |
| `admin.overview.*` | A2 | 16 | 16 | 0 | 0 |
| `settings.notificationStatus.*` | B3 | 13 | 13 | 0 | 0 |
| `settings.integrationStatus.*` | B2 | 6 | 6 | 0 | 0 |
| `doctorReport.*` | B6 | 90 | 90 | 0 | 0 |
| `quickAdd.*` (`common.add` etc) | A3 | (`common`) | — | 0 | 0 |
### Naming consistency
+6 -6
View File
@@ -34,7 +34,7 @@ NOT done in v1.4.15 (deliberately deferred):
- The `/api/insights/generate` route does NOT yet adopt
`generateInsight()` — the existing UI consumes the rich legacy
shape (`{summary, classification, findings, correlations,
dataQuality, ...}`). Migrating the consumer is v1.4.16 work because
dataQuality, ...}`). Migrating the consumer is v1.4.16 work because
it requires UI changes to `<InsightAdvisorCard />` and
`<InsightsCard />` to render the new strict shape.
- The scope-hardened prompt at `src/lib/ai/prompts/insight-generator.ts`
@@ -61,9 +61,9 @@ guideline document, with a rendered footnote in the UI.
plain language for the German UI.
- DGE healthy-weight (BMI bands).
- AASM 2015 sleep duration consensus.
Each excerpt: `{ id, source, citation, excerpt, locale, validity }`.
Curated by hand (NOT scraped) — no quote longer than 200 characters
to stay inside fair-use.
Each excerpt: `{ id, source, citation, excerpt, locale, validity }`.
Curated by hand (NOT scraped) — no quote longer than 200 characters
to stay inside fair-use.
2. **Inject relevant excerpts as system context per request.** A
pre-flight step `selectRelevantReferences(features)` looks at the
@@ -76,7 +76,7 @@ guideline document, with a rendered footnote in the UI.
`citations[]` entry gains an optional `referenceId` field pointing
into the bundle — the model uses an excerpt's `id` to back its
citation. The UI then renders a footnote `[1] ESH/ESC 2024,
p. 17 — "..."` next to the recommendation.
p. 17 — "..."` next to the recommendation.
4. **Hallucination guard for references.** Schema rejects any
`referenceId` not in the bundle. The model cannot fabricate a
@@ -112,7 +112,7 @@ Goal: a single provider's outage no longer breaks insights generation.
that). Per-request total wall-clock budget: 60 s.
4. **Telemetry.** Wide-Event `ai.cascade.attempts: ["codex","admin-key"]`
+ `ai.cascade.outcome: "success" | "all-failed"`.
- `ai.cascade.outcome: "success" | "all-failed"`.
## v1.4.16 — Per-recommendation explainability
+12 -12
View File
@@ -646,9 +646,9 @@ To eliminate that risk class, HealthLog's `CodexClient` walks an ordered chain o
```ts
const DEFAULT_SLUG_FALLBACK_CHAIN = [
"gpt-5.3-codex", // current allow-list entry (verified 2026-05-09)
"gpt-5-codex", // historical default (rejected as of 2026-05 but safe to retry — backend may flip)
"gpt-5", // bare slug — rejected on ChatGPT-auth as of 2026-05 but lives on api-key auth
"gpt-4o", // ladder-rung for last-ditch capability
"gpt-5-codex", // historical default (rejected as of 2026-05 but safe to retry — backend may flip)
"gpt-5", // bare slug — rejected on ChatGPT-auth as of 2026-05 but lives on api-key auth
"gpt-4o", // ladder-rung for last-ditch capability
];
```
@@ -656,15 +656,15 @@ The chain is overridable via the env var `CODEX_MODEL_FALLBACK_CHAIN` (comma-sep
**Trigger condition** (when to walk to the next slug):
| Status | Action |
| ------------------------------------------------------------------------------------- | ----------- |
| `400` body matches `/not supported when using Codex with a ChatGPT account/i` | walk |
| `400` body contains `model_not_found` | walk |
| `400` body contains `does not exist` AND mentions a model name | walk |
| `404` (any body) | walk |
| `401` first time | refresh and re-try same slug; do NOT walk |
| `401` after refresh, `403`, `429`, `5xx` | propagate (don't walk) |
| `200` with SSE `response.failed.error.code === "invalid_prompt"` | propagate (request shape is wrong, slug is fine) |
| Status | Action |
| ----------------------------------------------------------------------------- | ------------------------------------------------ |
| `400` body matches `/not supported when using Codex with a ChatGPT account/i` | walk |
| `400` body contains `model_not_found` | walk |
| `400` body contains `does not exist` AND mentions a model name | walk |
| `404` (any body) | walk |
| `401` first time | refresh and re-try same slug; do NOT walk |
| `401` after refresh, `403`, `429`, `5xx` | propagate (don't walk) |
| `200` with SSE `response.failed.error.code === "invalid_prompt"` | propagate (request shape is wrong, slug is fine) |
**Positive cache** — when a slug is observed working (HTTP 200 with at least one `response.completed` event), record `(slug, timestamp)` in a process-local Map. On the next request, jump straight to the cached slug. TTL: 1 hour. Cache scope: per-process (no DB write).