mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
docs: drop per-release audit notes + legacy spec, scrub phase markers
- /docs/audit/ moved out of the public docs surface (23 per-release forensic notes). They were linked from the README but read as internal traceability artefacts, not user-facing documentation. Kept local for cross-release lookup; added to .gitignore so new ones don't re-track. - /docs/api/openapi-v1422-legacy.yaml removed — superseded by the current openapi.yaml on the v1.5 line. - /docs/api/openapi.yaml info.version bumped 1.4.23 → 1.5.0 so the spec matches package.json (the doc claims the version is mirrored from the package; previously the mirror had drifted six versions). - /docs/ops/deploy.md: image name corrected from `ghcr.io/marc-bombeck/healthlog` → `ghcr.io/mbombeck/healthlog` (the previous command would have 404'd at GHCR); release-process framing dropped from a release-pair note. - /docs/ops/backup-restore.md + /docs/self-hosting/scaling.md: trailing phase tags (`v1.4 G1`, `v1.4 G3`, `v1.4.40 W-POOL`) stripped from section headers + body. Replaced an internal-notes pointer with a concrete `getPoolMax()` reference. The README pointer to /docs/audit/ is removed in the next commit along with the rest of the README accuracy pass.
This commit is contained in:
1 parent
64bf425714
commit
4f6d2ccbeb
30 files changed
+486
-10451
No files matched your search
@@ -83,3 +83,7 @@ next-env.d.ts
|
||||
# Docs scratch — temporary spec drafts that live in the repo only
|
||||
# while a release is in flight.
|
||||
/docs/ios/
|
||||
|
||||
# Per-release internal forensic notes — retained locally for
|
||||
# traceability across releases but not part of the public docs surface.
|
||||
/docs/audit/
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.4.23
|
||||
version: 1.5.0
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
# HealthLog Audit-Plan — Phasen-Übersicht
|
||||
|
||||
**Datum:** 2026-04-26
|
||||
**Audit-Bericht:** `~/infra/reports/healthlog-audit-2026-04-26.md`
|
||||
**Status:** Geplant, noch nicht in Execution
|
||||
|
||||
## Strategische Decisions
|
||||
|
||||
1. **Onboarding-Redesign:** 4-Step-Wizard → 2-Step + Empty-State-Dashboard + Getting-Started-Card.
|
||||
2. **Custom-Measurement-Types:** Pragmatic Hardcoding (jetzt #89), Cronometer-Pattern als v1.5+-Roadmap.
|
||||
3. **`apiClient`-Refactor vor Onboarding-Redesign**, damit neues Onboarding nicht denselben Silent-Failure-Bug reproduziert.
|
||||
|
||||
---
|
||||
|
||||
## Phasen-Sequenz (10 Phasen)
|
||||
|
||||
| # | Phase | Skill | Effort | Issue-Bezug |
|
||||
| ------- | ------------------------------------------------------ | --------------------------------------- | ------ | ------------------- |
|
||||
| **1** | Setup-Notfall + Versions-Sync | `gsd:quick` | ≤4 h | #87 (Pflaster), #88 |
|
||||
| **2** | Docker-Image-Pipeline (GHCR-Push + Doc-Konsolidierung) | `gsd:plan-phase` | ~1 d | #88 |
|
||||
| **3** | Issue #89 — TBW + Bone Mass hardcoded | `gsd:plan-phase` | ~1 d | #89 |
|
||||
| **4** | `apiClient`-Wrapper + geteilte Zod-Schemas | `gsd:research-phase` → `gsd:plan-phase` | 2-3 d | #87 (strukturell) |
|
||||
| **3.5** | **Onboarding-Redesign** (2 Steps, TTFV <60s) | `gsd:research-phase` → `gsd:plan-phase` | ~2 d | #87 (strukturell) |
|
||||
| **4.5** | Empty-State-Dashboard-Tiles (eine pro Type) | `gsd:plan-phase` | ~1 d | #87, #89 |
|
||||
| **4.6** | „Getting Started"-Checklist auf Dashboard | `gsd:plan-phase` | ~4 h | #87 |
|
||||
| **5** | Security + Reliability Hardening | `gsd:plan-phase` | 1-2 d | — |
|
||||
| **6** | Test-Coverage-Boost (Auth/Passkey/Withings/pg-boss) | `gsd:plan-phase` | 1-2 d | — |
|
||||
| **7** | UX/i18n/a11y Polish (Residuals aus #73) | `gsd:quick` | ≤4 h | #73 (Residuals) |
|
||||
| **8** | Settings-Page-Splitting (3143 → Sub-Components) | `gsd:research-phase` → `gsd:plan-phase` | 2-3 d | — |
|
||||
|
||||
**Reihenfolge:** 1 → 2 → 3 → **4 (apiClient)** → **3.5 (Onboarding nutzt apiClient)** → 4.5 → 4.6 → 5 → 6 → 7 → 8.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Setup-Notfall (≤4 h)
|
||||
|
||||
**Goal:** Self-Hoster kann mit `git clone && docker compose up -d` tatsächlich starten + Issue #87 stoppt zu blocken.
|
||||
|
||||
- `docker-compose.yml`: `expose:3000` → `ports: "3000:3000"` (DX-002)
|
||||
- `docker-compose.yml`: `POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?required}"` (SEC-003)
|
||||
- `.env.example`: `localhost:5432` → `db:5432` (DX-003)
|
||||
- `package.json`: `"version": "1.2.0"` (DOC-007)
|
||||
- `CLAUDE.md` + `AGENTS.md`: 23 Models statt 21/22 (DOC-001)
|
||||
- `src/app/onboarding/page.tsx`: `await fetch(...)` mit `.ok`-Check + `toast.error()` (DX-005 Pflaster)
|
||||
- README „minutes"-Claim entschärfen (DX-006)
|
||||
|
||||
**Verification:** Frischer Clone + neue VM → `cp .env.example .env && set secrets && docker compose up -d` → App auf `http://localhost:3000` erreichbar in <5 min.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Docker-Image-Pipeline (~1 d)
|
||||
|
||||
**Goal:** Issue #88 vollständig schließen. Pre-built Image auf GHCR. Docs konsolidiert.
|
||||
|
||||
- `.github/workflows/docker-publish.yml`: Multi-Arch (amd64+arm64), Trigger auf Tag-Push + main, GHCR-Push mit GITHUB_TOKEN
|
||||
- `healthlog-docs/getting-started/installation.mdx` → Verweis auf `self-hosting/docker.mdx` statt Duplikat (DX-004)
|
||||
- Repo-`docker-compose.yml` vs Docs-`docker-compose.yml`: eine Variante als Single-Source-of-Truth (Compose verwendet `image:` aus GHCR; Doc-Snippet wird daraus generiert)
|
||||
- `SECURITY.md` + Image-Pull-Instructions
|
||||
- `healthlog-landing` prüfen ob „Quick Start" aktualisiert werden muss
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Issue #89: TBW + Bone Mass (~1 d)
|
||||
|
||||
**Goal:** User-Anfrage von Manschk3rl beantworten. Blueprint = v1.2-Glucose-Migration.
|
||||
|
||||
**Touchpoints:**
|
||||
|
||||
1. `prisma/schema.prisma`: `MeasurementType` Enum + 2 neue Werte
|
||||
2. `prisma/migrations/0019_*` (additive `ALTER TYPE ... ADD VALUE`)
|
||||
3. `src/lib/validations/measurement.ts`: `unitMap`, `VALUE_RANGES`
|
||||
4. `src/lib/format.ts` + i18n-Keys (4-6 neue Keys)
|
||||
5. `src/lib/doctor-report-pdf.ts`: `TYPE_LABEL_KEYS`, `TYPE_UNIT_KEYS`, `vitalTypes`
|
||||
6. `src/components/measurements/measurement-form.tsx`: `MEASUREMENT_TYPES` Array
|
||||
7. `src/lib/withings/client.ts` + `sync.ts`: Mapping aus Withings `body_composition`
|
||||
8. `src/app/api/dashboard/widgets/route.ts` + `DEFAULT_DASHBOARD_LAYOUT`: 2 neue Widget-IDs
|
||||
9. healthlog-docs `health-metrics.mdx` aktualisieren
|
||||
10. Optional: `/api/ingest/measurements` Bearer-Endpoint für n8n (analog `/api/ingest/medication`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — `apiClient`-Wrapper (2-3 d)
|
||||
|
||||
**Goal:** Silent-Failure-Pattern strukturell eliminieren. Voraussetzung für 3.5.
|
||||
|
||||
- `src/lib/api-client.ts`: `apiClient.{get,post,put,delete}<T>(path, body?, schema?) → Result<T, ApiError>`
|
||||
- Auto-`.ok`-Check + Toast bei Fehler (mit i18n-Mapping)
|
||||
- Geteilte Zod-Schemas: Validations-Module in `src/lib/validations/` werden client-importable
|
||||
- Migration: alle ~50 `fetch(...)`-Aufrufe in `src/app/` und `src/components/` → `apiClient.*`
|
||||
- Tests für `apiClient` (Mock-Fetch, Error-Mapping, Schema-Validation)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3.5 — Onboarding-Redesign (~2 d)
|
||||
|
||||
**Goal:** TTFV <60s, 90% Completion (statt aktuell ~8% global).
|
||||
|
||||
**Neuer Flow:**
|
||||
|
||||
- Step 1: Welcome + Anzeigename (optional) + Einheiten + Größe (optional). Sekundärlink „Skip everything → Dashboard".
|
||||
- Step 2: Multi-Select-Chips „Was willst du tracken?" (Weight/BP/Pulse/Mood/Glucose/Sleep/Steps/BodyFat/TBW/BoneMass/Medications). Pre-checked Defaults. Speichert in `User.dashboardWidgetsJson`.
|
||||
- Routing nach Onboarding: direkt zum Dashboard mit „Getting Started"-Card (s. Phase 4.6).
|
||||
|
||||
**Entfällt ersatzlos:** Targets-Step, Notifications-Teaser-Step, Front-loaded-Med-Wizard.
|
||||
|
||||
**Permissions deferred:** Notifications nur bei „Add medication → Reminder ein", PWA-Install nur bei Day-2-Return-Visit oder nach 3. Eintrag.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4.5 — Empty-State-Dashboard-Tiles (~1 d)
|
||||
|
||||
**Goal:** Mealie/Tandoor/Linear-Pattern. Jede Metric-Tile hat Empty-State mit „Add your first X"-CTA.
|
||||
|
||||
- `src/components/dashboard/EmptyStateTile.tsx` als wiederverwendbare Komponente
|
||||
- Pro `MeasurementType`: eigene Empty-State-Card (Icon + Headline + CTA-Button → opens MeasurementForm prefilled)
|
||||
- `Medications` analog: Empty-State „Add your first medication" → opens MedicationForm mit Default-Schedule (08:00–09:00 daily)
|
||||
- Dashboard-Routing: User mit `dashboardWidgetsJson` ohne Daten sieht Empty-States, nicht leeres Grid
|
||||
|
||||
---
|
||||
|
||||
## Phase 4.6 — „Getting Started"-Checklist (~4 h)
|
||||
|
||||
**Goal:** Linear-Pattern. Persistent dismissible Card auf Dashboard.
|
||||
|
||||
- `User.gettingStartedDismissedAt: DateTime?` (nullable, NULL = noch sichtbar)
|
||||
- 4 Items mit Live-Status:
|
||||
- ☐ Log your first measurement (✓ wenn ≥1 Measurement existiert)
|
||||
- ☐ Add a medication (✓ wenn ≥1 Medication existiert)
|
||||
- ☐ Set a target value (✓ wenn `thresholdsJson` non-empty)
|
||||
- ☐ Install HealthLog as App (✓ wenn `beforeinstallprompt` accepted, sonst nur sichtbar bei PWA-fähigen Browsern)
|
||||
- „Dismiss"-Button → setzt Timestamp, Card verschwindet
|
||||
- Vollständig durchgeklickt → Card verschwindet automatisch
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Security + Reliability Hardening (1-2 d)
|
||||
|
||||
- SEC-002: SSRF-IPv4-Parser-Härtung (führende Nullen)
|
||||
- SEC-004: GitHub-PAT-Logging-Sanitization (Sensitive-Field-Filter)
|
||||
- SEC-005: AES-256-GCM Key-Rotation-Doc + optional `keyVersion`
|
||||
- SEC-006: Custom-Thresholds-Rate-Limit
|
||||
- PERF-001: N+1 in `insights/comprehensive/route.ts:219` per `groupBy` auflösen
|
||||
- REL-002: Onboarding-Error-Handling über Pflaster hinaus (via apiClient aus Phase 4)
|
||||
- REL-003: pg-boss `process.on('SIGTERM', ...)` für graceful shutdown
|
||||
- LOG-001: 6× `console.log()` in `src/lib/jobs/reminder-worker.ts` → Wide-Event-Annotations
|
||||
- CI: ESLint+Typecheck `continue-on-error: true` → blocking
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Test-Coverage-Boost (1-2 d)
|
||||
|
||||
Coverage 35% → 60% auf kritischen Pfaden.
|
||||
|
||||
**Top-5-Test-Suites:**
|
||||
|
||||
1. Session-Validation Roundtrip (`src/lib/auth/session.ts`)
|
||||
2. Passkey-Register/Verify-Integration (Attestation-Flow)
|
||||
3. Notification-Dispatcher Multi-Channel (TELEGRAM/NTFY/WEB_PUSH)
|
||||
4. pg-boss Reminder-Worker (Queue-Init, Job-Execution, Cron, Retry)
|
||||
5. Withings-OAuth + Sync (`exchangeCode` → `refresh` → `fetchMeasurements`)
|
||||
|
||||
**Plus:** TEST-006 Custom-Thresholds-Resolution, TEST-007 AI-Provider-SSRF.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — UX/i18n/a11y Polish (≤4 h)
|
||||
|
||||
**Residuals aus Issue #73 + neue Findings:**
|
||||
|
||||
- I18N-002: Admin-Page Hardcoded „Web Push" / „Bug Report" → `t(...)`
|
||||
- I18N-003: `medication-card.tsx` `Intl.DateTimeFormat("en-CA")` und `toLocaleString("en-US")` → `useFormatters()`
|
||||
- A11Y-001: `phase-config-dialog.tsx` Text-Indicator neben Farb-Punkt (WCAG 1.4.1)
|
||||
- A11Y-002: Admin StatusItem `aria-label` verifizieren
|
||||
- UX-002: Admin-Save-Error-UX (Field-Highlight bei Fehler)
|
||||
- UX-003: Bottom-Nav-Touch-Targets `p-2.5` → `p-3` (≥44×44 px)
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Settings-Page-Splitting (2-3 d)
|
||||
|
||||
**Goal:** 3143 Zeilen → Sub-Components mit klarer Boundary.
|
||||
|
||||
- Research-Phase: Sub-Routes (`/settings/profile`, `/settings/notifications`, …) vs Sub-Components in einer Page?
|
||||
- Plan-Phase: Migrationsstrategie ohne URL-Bruch
|
||||
- Insights-Page (1750 Zeilen) folgt analogem Pattern
|
||||
|
||||
---
|
||||
|
||||
## Issue-Vorschlags-Abdeckung
|
||||
|
||||
### Issue #87 (Onboarding-Meds verschwinden)
|
||||
|
||||
- **Pflaster:** Phase 1 (`.ok`-Check + Toast)
|
||||
- **Strukturelle Lösung:** Phase 4 (`apiClient`) + Phase 3.5 (neuer Flow ohne Med-Wizard) + Phase 4.5 (Empty-State-Tiles, Meds direkt sichtbar) + Phase 4.6 (Checklist)
|
||||
|
||||
### Issue #88 (Setup holprig — 4 Hauptpunkte + 3 Verbesserungsvorschläge)
|
||||
|
||||
| #88 Punkt | Phase | Status |
|
||||
| --------------------------------------------------------------------------- | ----------------------------- | --------- |
|
||||
| (a) Bundled compose ≠ docs version | Phase 2 (DOC-005) | abgedeckt |
|
||||
| (b) ghcr.io 404 (kein Image gepusht) | Phase 2 (DX-001 GHA-Workflow) | abgedeckt |
|
||||
| (c) `.env.example` `localhost:5432` vs Compose `db:5432` | Phase 1 (DX-003) | abgedeckt |
|
||||
| (d) `expose:3000` statt `ports:3000:3000` | Phase 1 (DX-002) | abgedeckt |
|
||||
| Vorschlag: GHA für Image-Push (GHCR/Docker Hub) | Phase 2 | abgedeckt |
|
||||
| Vorschlag: Merge „Getting started > Installation" + „Self-Hosting > Docker" | Phase 2 (DX-004) | abgedeckt |
|
||||
| Vorschlag: Same presets across compose+docs+.env.example | Phase 1 + Phase 2 | abgedeckt |
|
||||
|
||||
### Issue #89 (TBW + Bone Mass)
|
||||
|
||||
- Phase 3 hardcoded mit Withings-Mapping. Optional `/api/ingest/measurements` für n8n in Phase 3.
|
||||
|
||||
### Issue #73 (UX-Liste mit 8 Punkten — CLOSED in v1.2)
|
||||
|
||||
| #73 Punkt | v1.2-Status | Residual? |
|
||||
| ----------------------------------------- | ------------------------------------------------- | ------------------------------------------- |
|
||||
| 1. Passkey-Errors unklar | Resolved (Package C, typed errors) | TEST-002 (keine Tests) → Phase 6 |
|
||||
| 2. Number-Formatting (DE 70,5 vs US 70.5) | Resolved (Package B, locale-aware formatters) | I18N-003 in `medication-card.tsx` → Phase 7 |
|
||||
| 3. Chart-Labels „7P/30P/90P/All" | Resolved (Package A, „7 Pkt"/„7 pts") | — |
|
||||
| 4. Dashboard nicht real-time | Resolved (`invalidateKeys` durchgängig) | — |
|
||||
| 5. GitHub-Setup-Confusion | Resolved (Built-in Feedback in v1.2) | — |
|
||||
| 6. Built-in Feedback | Done in v1.2 | — |
|
||||
| 7. Admin-Save ohne Error-UX | Partial (Success-Msg ja, Error-Highlight schwach) | UX-002 → Phase 7 |
|
||||
| 8. Blood-Sugar | Done in v1.2 | — |
|
||||
|
||||
### Issue #74 (raw locale keys in UI)
|
||||
|
||||
Resolved in v1.2.
|
||||
|
||||
---
|
||||
|
||||
## Time-to-First-Value (TTFV) Ziel
|
||||
|
||||
Nach Onboarding-Redesign + Empty-State-Pattern:
|
||||
|
||||
- Login → Dashboard mit Empty-States: 2 Klicks
|
||||
- Klick auf „Add your first weight" → Form prefilled „now" → Submit: 30 Sek
|
||||
- **TTFV-Ziel: <60 Sekunden** (Branchenvergleich: führende Health-Apps 30-90s)
|
||||
|
||||
## Anti-Patterns explizit vermieden
|
||||
|
||||
- Permissions-Cold-Prompt (Notifications/PWA) auf Day 1
|
||||
- Front-loaded Targets vor erster Messung
|
||||
- Multi-Step-Wizard mit identischem Skip-Button
|
||||
- `fetch()` ohne `.ok`-Check (apiClient löst strukturell)
|
||||
- Mega-Page-Pattern (Phase 8 löst Settings 3143 Z.)
|
||||
- Custom-Measurement-Types vorzeitig (40-60h Wartung/Jahr für 50% Feature-Loss)
|
||||
@@ -1,233 +0,0 @@
|
||||
# v1.4.14 Performance Audit — Production
|
||||
|
||||
**Status**: stage-4 of the v1.4.14 release cycle
|
||||
**Measured against**: `https://healthlog.bombeck.io` (`/api/version` → `1.4.13`)
|
||||
**Measured at**: `2026-05-09T14:14:48Z` → `2026-05-09T14:15:27Z`
|
||||
**Tool**: standalone Playwright + Chromium 1217 driver, `PerformanceObserver`
|
||||
(`largest-contentful-paint`, `longtask`) + Resource Timing API + Playwright
|
||||
`response`-event capture for transferred bytes (Cloudflare strips
|
||||
`Content-Length` on chunked responses, so the Resource Timing
|
||||
`transferSize` would otherwise read 0). Script lives at
|
||||
`/tmp/v15-perf-measure.mjs` (one-shot, not committed — re-run with
|
||||
`SESSION=<cookie> node ./.v15-perf-measure.mjs > /tmp/v15-perf-results.json`
|
||||
inside the repo so the script picks up `node_modules/@playwright/test`).
|
||||
Two viewports per page: **desktop** (`1920×1080`, default Chromium UA) and
|
||||
**mobile** (Playwright's `Pixel 5` profile).
|
||||
|
||||
## Method
|
||||
|
||||
For each `(page × viewport)` cell:
|
||||
|
||||
1. Open a fresh BrowserContext with an authenticated session cookie
|
||||
(`healthlog_session=cmox4d6fj…`) injected pre-navigation.
|
||||
2. Inject a `PerformanceObserver` via `addInitScript` BEFORE navigation so LCP
|
||||
and longtask events from the very first paint are captured.
|
||||
3. `page.goto(url, { waitUntil: "load", timeout: 45_000 })` and then idle for
|
||||
3 s so deferred chart code, lazy queries and below-the-fold content settle.
|
||||
4. Read `performance.getEntriesByType("navigation")[0]`,
|
||||
`performance.getEntriesByType("resource")`, plus the longtask / LCP
|
||||
captures we accumulated in `window.__perfState`.
|
||||
5. Total Blocking Time is the longtask-derived proxy:
|
||||
`Σ (longtask.duration − 50ms)` over all longtasks ≥ 50 ms.
|
||||
|
||||
The "TTI" column in the original brief is omitted because Chromium's
|
||||
canonical TTI requires a 5 s quiet-window heuristic that isn't worth
|
||||
synthesising here — `Load` + `TBT` are the actionable signals.
|
||||
|
||||
## Per-page metrics
|
||||
|
||||
| page | viewport | TTFB ms | DCL ms | Load ms | LCP ms | TBT ms | Longtasks | Total KiB | JS KiB | CSS KiB |
|
||||
| ------------------------ | -------- | ------: | -----: | ------: | -------: | ------: | --------: | --------: | -----: | ------: |
|
||||
| `/` | desktop | 396 | 1080 | 2497 | **2828** | **624** | 1 | 604 | 425 | 16 |
|
||||
| `/` | mobile | 384 | 752 | 1560 | 2184 | 0 | 0 | 604 | 425 | 16 |
|
||||
| `/settings/integrations` | desktop | 500 | 738 | 1498 | 2396 | 0 | 0 | 394 | 297 | 16 |
|
||||
| `/settings/integrations` | mobile | 495 | 771 | 1545 | 1900 | 0 | 0 | 386 | 297 | 16 |
|
||||
| `/admin` | desktop | 584 | 761 | 1611 | 1932 | 0 | 0 | 382 | 291 | 16 |
|
||||
| `/admin` | mobile | 872 | 1112 | 1920 | 2268 | 0 | 0 | 378 | 291 | 16 |
|
||||
| `/insights` | desktop | 519 | 681 | 1609 | 2264 | 27 | 1 | 608 | 419 | 16 |
|
||||
| `/insights` | mobile | 502 | 667 | 1647 | 2380 | 28 | 1 | 604 | 419 | 16 |
|
||||
|
||||
**Read in plain English**: every page comes in well under 2.5 s LCP except
|
||||
`/` desktop (2.83 s) and `/insights` (~2.3 s). Mobile is faster than desktop
|
||||
in absolute LCP terms because the Pixel 5 profile uses a smaller viewport
|
||||
(less LCP candidate area) — the work is the same.
|
||||
|
||||
`/` desktop racks up 624 ms TBT from a single longtask: that's the dashboard
|
||||
hydration cost for the `useQuery` × 9 + `TrendCard` × 8 + `HealthChart`
|
||||
dynamic-imports firing in parallel.
|
||||
|
||||
## Top-5 JS bundles per page
|
||||
|
||||
> All sizes are compressed Brotli from CF. The `0vl43uc.17vx1.js` chunk
|
||||
> appears on every page — that's the framework + Radix + TanStack Query
|
||||
> bedrock. The `0m4t~kff9equt.js` chunk (52.2 KiB everywhere) is the next.js
|
||||
> app-router shell + `radix-ui/dialog/dropdown`. The notable per-page chunks
|
||||
> are flagged with **bold** below.
|
||||
|
||||
### `/` (desktop + mobile, identical)
|
||||
|
||||
- `0503y5tur7j1f.js` — **108.6 KiB** ← Recharts (loaded via `next/dynamic` from `health-chart` / `mood-chart`, but still on the critical path because the dashboard mounts ≥ 1 chart for any user with measurements)
|
||||
- `0vl43uc.17vx1.js` — 71.3 KiB ← framework + TanStack Query (every page)
|
||||
- `0m4t~kff9equt.js` — 52.2 KiB ← Next router shell + Radix overlay primitives
|
||||
- `0~1ef0ki4ccp~.js` — 36.7 KiB ← shared app code (i18n + auth-shell)
|
||||
- `0uc70mahb3xof.js` — 15.3 KiB ← `dashboard/page.tsx` per-page chunk
|
||||
|
||||
### `/settings/integrations` (desktop + mobile, identical)
|
||||
|
||||
- `0vl43uc.17vx1.js` — 71.3 KiB
|
||||
- `0m4t~kff9equt.js` — 52.2 KiB
|
||||
- `0~1ef0ki4ccp~.js` — 36.7 KiB
|
||||
- `01q.1mkz27ebz.js` — 19.8 KiB ← settings/integrations page
|
||||
- `0d3x9g6k_smzl.js` — 15.0 KiB
|
||||
|
||||
### `/admin` (desktop + mobile, identical)
|
||||
|
||||
- `0vl43uc.17vx1.js` — 71.3 KiB
|
||||
- `0m4t~kff9equt.js` — 52.2 KiB
|
||||
- `0~1ef0ki4ccp~.js` — 36.7 KiB
|
||||
- `0d3x9g6k_smzl.js` — 15.0 KiB
|
||||
- `0of675ww0w6~y.js` — 13.6 KiB ← admin status-card-grid
|
||||
|
||||
### `/insights` (desktop + mobile, identical)
|
||||
|
||||
- `18b.35leqj.iy.js` — **108.5 KiB** ← Recharts, eagerly imported because `insights/page.tsx` uses `ScatterChart`/`Scatter`/`XAxis`/`YAxis`/`CartesianGrid`/`Tooltip`/`ResponsiveContainer` symbols at module top-level (line 42–50)
|
||||
- `0vl43uc.17vx1.js` — 71.3 KiB
|
||||
- `0m4t~kff9equt.js` — 52.2 KiB
|
||||
- `0~1ef0ki4ccp~.js` — 36.7 KiB
|
||||
- `16c~twjymta_z.js` — 15.6 KiB
|
||||
|
||||
## Treemap-flavoured grouping
|
||||
|
||||
| group | KiB (uncompressed est ×3 or measured) | notes |
|
||||
| -------------------------------------------- | ------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Recharts (per page that uses it) | ~108 KiB | by far the heaviest single dep; v1.4.14 candidate for replacement. v1.4.6 already moved `HealthChart`/`MoodChart` to `next/dynamic`, but `/insights` still imports the Scatter primitives eagerly. |
|
||||
| Framework (Next.js + React + TanStack Query) | ~71 KiB | unavoidable baseline |
|
||||
| Radix primitives + app-router shell | ~52 KiB | unavoidable baseline |
|
||||
| Shared app code (i18n, auth-shell, layout) | ~37 KiB | unavoidable baseline |
|
||||
| Per-page app code | 13–20 KiB | within budget |
|
||||
|
||||
Pages without charts (`/admin`, `/settings/integrations`) are about 132 KiB
|
||||
lighter than the chart pages, confirming Recharts is the dominant
|
||||
single-dependency cost.
|
||||
|
||||
## Slowest API calls observed
|
||||
|
||||
Measured for `/` desktop (worst case). Numbers are the per-request
|
||||
`PerformanceResourceTiming.duration` (queue + DNS + connect + TTFB + body):
|
||||
|
||||
| endpoint | ms |
|
||||
| -------------------------------------------------- | --: |
|
||||
| `/api/measurements?type=WEIGHT&limit=500&offset=0` | 812 |
|
||||
| `/api/notifications/preferences` | 654 |
|
||||
| `/api/gamification/achievements` | 651 |
|
||||
| `/api/measurements?type=PULSE` | 506 |
|
||||
| `/api/measurements?type=BLOOD_PRESSURE_SYS` | 404 |
|
||||
| `/api/measurements?type=BLOOD_PRESSURE_DIA` | 399 |
|
||||
| `/api/mood/analytics` | 351 |
|
||||
| `/api/analytics` | 342 |
|
||||
| `/api/medications` | 340 |
|
||||
| `/api/withings/status` | 303 |
|
||||
|
||||
Two surprises:
|
||||
|
||||
- `notifications/preferences` and `withings/status` are fired by the
|
||||
`GettingStartedChecklist` even when the user is well past onboarding
|
||||
(the live tenant has `onboardingCompletedAt` set since early 2026 and
|
||||
1000+ measurements). The checklist short-circuits with `null` after
|
||||
fetching, so the network is wasted.
|
||||
- `gamification/achievements` is hot-polled every 2 minutes by
|
||||
`AchievementUnlockNotifier`. Acceptable cadence; not on the win list.
|
||||
|
||||
## Top 3 wins
|
||||
|
||||
### 1. Defer Recharts imports on `/insights` (S, **implemented**)
|
||||
|
||||
- **File**: `src/app/insights/page.tsx`
|
||||
- **What**: Replace the eager `import { ScatterChart, … } from "recharts"`
|
||||
with per-symbol `next/dynamic` imports (`ssr: false`). Each scatter card
|
||||
is below the fold — it sits inside the correlation cards that gate on
|
||||
`data.bpMedicationScatterData.length >= 5` etc.
|
||||
- **Expected impact**: Moves the 108.5 KiB Recharts chunk off the initial
|
||||
bundle. LCP-critical content no longer waits for Recharts to parse on
|
||||
the main thread. Same Recharts that the dashboard already pays for is
|
||||
shared at runtime, so no double-fetch on cache-warm navigation.
|
||||
- **Effort**: ~10 LOC (the seven recharts symbols become seven
|
||||
`dynamic(...)` consts).
|
||||
- **Risk**: each first paint of a scatter section will Suspense-fallback
|
||||
for ~1 frame while the chunk downloads. That matches the existing
|
||||
`<HealthChart>` UX on the same page.
|
||||
|
||||
### 2. Skip `GettingStartedChecklist` data fetches for post-onboarding users (S, **implemented**)
|
||||
|
||||
- **File**: `src/components/onboarding/getting-started-checklist.tsx`
|
||||
- **What**: Gate `withings/status` and `notifications/preferences` on
|
||||
`!!user && user.onboardingCompletedAt == null`. The two queries fire
|
||||
unconditionally today, then the checklist self-hides because the user
|
||||
has already completed onboarding.
|
||||
- **Expected impact**: 2 fewer API calls per dashboard load for every
|
||||
established user. Saves ~950 ms of network on `/` desktop.
|
||||
- **Effort**: 2 LOC (one extra clause in each `enabled:` field).
|
||||
- **Risk**: the corner case of a returning user with
|
||||
`onboardingCompletedAt != null` but `< 5` measurements (e.g. they
|
||||
deleted readings post-onboarding) will see a checklist with stale
|
||||
`withingsConnected` / `notificationsConfigured` flags. Acceptable —
|
||||
the checklist already gracefully renders unknown defaults.
|
||||
|
||||
### 3. Replace Recharts on the dashboard (L, **deferred to v1.4.15**)
|
||||
|
||||
- **What**: Drop Recharts in favour of a smaller library (Visx, Chart.js
|
||||
4, or hand-rolled SVG). Recharts is ~108 KiB Brotli for a use case that
|
||||
needs ~3 chart variants (line, area, scatter).
|
||||
- **Expected impact**: −80 to −100 KiB initial JS on `/` and `/insights`;
|
||||
TBT win on `/` desktop because Recharts hydration is the dominant
|
||||
longtask source.
|
||||
- **Effort**: L. New dep + every chart in `src/components/charts/` needs
|
||||
rewrite. Out of scope for v1.4.14 — track in v1.4.15 backlog.
|
||||
- **Why deferred**: hard rule "no new dependencies in v1.4.14".
|
||||
|
||||
### Bonus / honourable mentions
|
||||
|
||||
- `Achievements` polling cadence on the dashboard is 2 min, staleTime 60 s.
|
||||
Reasonable, no win.
|
||||
- `staleTime: 5 * 60 * 1000` is already the global default in
|
||||
`providers.tsx` — no global tuning needed.
|
||||
- Image lazy-load: only the `<AvatarImage>` in `sidebar-nav.tsx` and one
|
||||
`<img>` in `admin/feedback-inbox-section.tsx` exist. The avatar is
|
||||
in-viewport; the feedback `<img>` is admin (Stage 4b territory). Skip.
|
||||
|
||||
## Verification
|
||||
|
||||
Local `pnpm build` cannot complete on this machine due to a Node-25
|
||||
upstream regression (`Cannot read private member #state from an object
|
||||
whose class did not declare it` in turbopack's prerender path; this is
|
||||
the same bug noted in the repo doc's pnpm-build entry). Wins #1 and #2
|
||||
therefore can't be re-measured against prod until v1.4.14 ships and
|
||||
Coolify force-pulls the new bundle. Both wins are nevertheless verified
|
||||
by static code review:
|
||||
|
||||
- **Win #1 — defer Recharts**: every changed import follows the exact
|
||||
same `next/dynamic(... ssr: false)` shape that
|
||||
`src/app/insights/page.tsx` already uses for `HealthChart` and
|
||||
`MoodChart`. Next 16 / Turbopack splits each `dynamic(...)` callsite
|
||||
into its own async chunk. Recharts internally re-uses one shared
|
||||
module so the seven dynamic imports collapse to a single chunk request
|
||||
at runtime, identical to the existing dashboard behaviour.
|
||||
- **Win #2 — skip checklist fetches**: `onboardingPending` is read once
|
||||
per render from `user.onboardingCompletedAt`. When false, both
|
||||
`useQuery({ enabled: onboardingPending })` calls stay idle. The
|
||||
downstream `shouldShowChecklist()` already hides the component for
|
||||
every post-onboarding user with ≥ 5 measurements, so no UI regresses.
|
||||
`pnpm test` (95 files / 733 tests) is green after the change.
|
||||
|
||||
A re-measure after v1.4.14 ships should show:
|
||||
|
||||
- `/insights` initial JS dropping from ~419 KiB to ~310 KiB (the 108 KiB
|
||||
Recharts chunk moves out of the critical path and is only fetched if a
|
||||
scatter section actually mounts).
|
||||
- `/` dashboard total request count dropping from 9 to 7 for any user
|
||||
with `onboardingCompletedAt != null`, eliminating the
|
||||
`notifications/preferences` (~654 ms) and `withings/status` (~303 ms)
|
||||
calls.
|
||||
|
||||
If post-deploy measurement contradicts either expectation, revert the
|
||||
relevant commit and update this document.
|
||||
@@ -1,337 +0,0 @@
|
||||
# HealthLog v1.4.14 — Release Summary
|
||||
|
||||
## Release brief (DE, ≤ 200 Wörter)
|
||||
|
||||
✅ **v1.4.14 ist live** auf `https://healthlog.bombeck.io` — Image-Digest
|
||||
`sha256:0ced46004a54…` (vorher v1.4.12: `791c2cd2…`), `/api/version`
|
||||
meldet `1.4.14`.
|
||||
|
||||
🆕 **Was ist neu**
|
||||
|
||||
- **Codex/ChatGPT-Modell-Slug korrigiert** auf `gpt-5.3-codex` — KI-
|
||||
Insights laufen jetzt direkt mit deinem ChatGPT-Login durch, kein
|
||||
API-Key nötig. Operator-Override per `CODEX_MODEL`-Env-Var möglich.
|
||||
- **Admin-Bereich auf Unterseiten aufgeteilt** (`/admin/users`,
|
||||
`/admin/backups`, `/admin/system-status`, `/admin/integrations`, …) —
|
||||
bessere Übersicht, schnelleres Laden, Status-Cards verlinken direkt.
|
||||
- **Neue Backups-Sektion** mit Tabelle aller bisherigen Sicherungen und
|
||||
„Backup jetzt erstellen“-Knopf.
|
||||
- **Neue User-Verwaltung** mit Rollen-Filter und „Force-Logout"-Aktion
|
||||
(eigene Session ist gesperrt).
|
||||
- **„Gespeicherten KI-Schlüssel entfernen"** als eigener Knopf in
|
||||
Settings → KI.
|
||||
- **Trend-Pfeile sind jetzt metrik-bewusst** — Blutdruck ↑ orange,
|
||||
Stimmung ↑ grün, Puls ↑ neutral.
|
||||
|
||||
🔧 **Was ist gefixt**
|
||||
|
||||
- Notification-Channels + Push-Subscriptions werden jetzt vom „Alle
|
||||
Daten löschen“ tatsächlich entfernt.
|
||||
- DST-Wechsel verschiebt keine Tageswerte mehr in Cross-Metric-Joins
|
||||
(war zwei Tage/Jahr falsch).
|
||||
- KI-Provider-Fehler werden korrekt klassifiziert (422/503/429 statt
|
||||
pauschal 500).
|
||||
- Logging-Redactor verschluckt keine harmlosen Wörter wie `task-force`
|
||||
oder `risk-management` mehr.
|
||||
|
||||
⚡ **Performance**
|
||||
|
||||
- `/insights` initiales JS −108 KiB (Recharts deferred geladen).
|
||||
- Dashboard-Checklist Netzwerk −950 ms für Bestandsnutzer.
|
||||
|
||||
⚠️ **Hard-Reload** (`Cmd+Shift+R`) für den einmaligen Service-Worker-
|
||||
Reset einplanen.
|
||||
|
||||
📚 **Docs** auf `docs.healthlog.dev` und Landing aktualisiert.
|
||||
🎯 **v1.4.15 Backlog**: `.planning/v1415-backlog.md`.
|
||||
|
||||
---
|
||||
|
||||
## Was gelandet ist
|
||||
|
||||
53 Commits seit `v1.4.13` (`5df74f7..e5fae9b`). Severity-geordnet.
|
||||
|
||||
### Release
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | --------------------------------------------------------------- |
|
||||
| `e5fae9b` | chore(release): v1.4.14 |
|
||||
| `f0d53fb` | chore(release): rebrand artifacts as v1.4.14 (patch, not minor) |
|
||||
|
||||
### Codex-OAuth verification (Stage 1)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | ------------------------------------------------------------------------ |
|
||||
| `5df74f7` | fix(ai): default Codex model to `gpt-5.3-codex` for ChatGPT-account auth |
|
||||
|
||||
### v1.4.6 deferred bugfix backlog (Stage 2)
|
||||
|
||||
| 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) |
|
||||
| `d6696cf` | fix(logging): redactSecrets regex requires non-alphanumeric prefix (T2.5) |
|
||||
|
||||
### Admin Panel refactor (Stage 4b — folds T2.6 + T2.7)
|
||||
|
||||
| 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 |
|
||||
|
||||
### UX polish (Stage 5)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | ------------------------------------------------------------------------ |
|
||||
| `987ce0d` | fix(a11y): clear axe-core violations on `/admin` and key sub-routes |
|
||||
| `38f12df` | feat(ui): trend arrow color reflects metric-specific direction sentiment |
|
||||
| `788c8ad` | feat(settings): allow removing saved AI provider key |
|
||||
| `569300e` | style(ui): use semantic tokens for mood and feedback indicators |
|
||||
| `e3a6899` | fix(ui): disambiguate dashboard quick-add menu items |
|
||||
| `65cfb27` | fix(a11y): label `/admin` overview quick-jump as a navigation landmark |
|
||||
|
||||
### Multi-pass QA reconcile (Stage 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 |
|
||||
| `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 |
|
||||
|
||||
### End-to-end test coverage (Stage 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 |
|
||||
| `ca3c599` | test(e2e): extend axe-core to authenticated surfaces; fix login regex |
|
||||
| `7af1f59` | fix(a11y): close violations the new e2e axe-core sweep surfaced |
|
||||
|
||||
### Performance (Stage 4)
|
||||
|
||||
| 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 |
|
||||
|
||||
### CI / housekeeping
|
||||
|
||||
| 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 stage-6 reconcile changes |
|
||||
| `6b88e56` | chore(format): prettier sweep over stage-2 touched files |
|
||||
|
||||
---
|
||||
|
||||
## Was deferred wurde + Warum
|
||||
|
||||
### v1.4.14 release-cycle "Apply automatically? no" simplify findings (8)
|
||||
|
||||
Aus `phase-6-simplify-findings.md`:
|
||||
|
||||
- **F5** Defensive `notFound()` hinter `dynamicParams = false` —
|
||||
Entscheidung pending zwischen (a) Kommentar trimmen vs. (b) Guard
|
||||
droppen + casten.
|
||||
- **F9** `<SectionFrame>` in `renderer.tsx` switch — die Repetition ist
|
||||
echt, aber das Tabellen-Refactor wäre schlechter (asymmetrische
|
||||
`currentUserId`-Prop). Recommend: keep as-is.
|
||||
- **F10** `if (!user || user.role !== "ADMIN") return null` doppelt —
|
||||
zwei Sites, unter dem 4-uses-Threshold. Recommend: keep.
|
||||
- **F12** Historische Anker-Kommentare (`v1.4.6 T2.6`, `P15`,
|
||||
`v1.4.7.1`, …) — judgement-call sweep; ein Teil enthält echtes
|
||||
WHY-Wissen, ein Teil ist Archäologie.
|
||||
- **F13** ~270 LOC duplication in den drei Status-Generatoren
|
||||
(mood/weight/blood-pressure) — `_status-helpers.ts`-Extraktion ist
|
||||
medium-risk, braucht fokussierten Pass + Tests.
|
||||
- **F14** `formatBytes(fmt: ReturnType<typeof useFormatters>)` —
|
||||
cosmetic.
|
||||
- **F18** `getApiErrorMessage` doppelter Fallback-Branch — cosmetic.
|
||||
- **F19** `eslint-disable-next-line react-hooks/exhaustive-deps` auf
|
||||
Device-Code-Polling — verifizieren, ob `t`/`queryClient`-deps eine
|
||||
Re-poll-Loop auslösen.
|
||||
|
||||
### v1.4.6 P4+ Items (nicht aufgenommen)
|
||||
|
||||
`useAdminSettings` / `useSystemStatus` Test-Coverage für die
|
||||
`isError`-UI aus P19 ist weiter manuell-only — nicht in v1.4.14
|
||||
gepflegt, kein Regressionsrisiko. Trägt sich in den v1.4.15-Backlog.
|
||||
|
||||
### 4 HIGH findings → v1.4.15
|
||||
|
||||
Aus `phase-6-reconcile-report.md`:
|
||||
|
||||
- **H4d/H5d design — StatusCardGrid i18n** (Englisch-only
|
||||
Severity-Labels, Card-Titel/-CTAs, `fmtRelative`/`fmtUptime`,
|
||||
StatusBadge-aria-label). ~15 Keys × 2 Locales + Test-Rewrite — zu
|
||||
breit für sicheren Overnight-Apply.
|
||||
- **H2 code-review — Admin-Page server-side `requireAdmin()`**. Heute
|
||||
ist die Rolle nur client-seitig geprüft; das Shell flackert kurz.
|
||||
Braucht eine server-rendered `requireAdmin()`-HOC, präzisiert für
|
||||
den HealthLog-Auth-Stack.
|
||||
- **H-1 security — "Wipe all data"-Scope-Drift**. v1.4.14 hat
|
||||
Notification-Channels, Push-Subs und Telegram-Scheduled-Deletions
|
||||
ergänzt. Was weiter überlebt: `MoodEntry`, `RefreshToken`,
|
||||
`DataBackup`, `Device`, `UserAchievement`, `IdempotencyKey`, plus
|
||||
einige verschlüsselte Spalten. Entscheidung pending: (a) Scope
|
||||
nochmal weiten oder (b) Operation umbenennen ("Reset health data").
|
||||
|
||||
---
|
||||
|
||||
## CI / Test status
|
||||
|
||||
- `pnpm typecheck` — clean
|
||||
- `pnpm lint` — 0 errors / 12 pre-existing warnings (none introduced)
|
||||
- `pnpm format:check` — clean nach `c847b80` (trend-card test file
|
||||
sweep)
|
||||
- `pnpm test` — **97 files / 754 tests** green
|
||||
- `pnpm test:integration` — 5 files / 11 tests green; mock-leak flake
|
||||
zwischen `admin-data-wipe.test.ts` und `idempotency-replay.test.ts`
|
||||
via `tests/integration/mock-next-headers.ts` shared cookieJar
|
||||
behoben (10/10 consecutive runs grün)
|
||||
- `pnpm build` — fails lokal auf Node 25 (`Reflect.get` private-member
|
||||
bug); CI Docker (Node 22) ist canonical
|
||||
- `pnpm e2e` — gleicher Node-25-Bug betrifft auch `next dev`; CI runs
|
||||
green auf Node 22 (Phase-3 Report: 41/41 specs green)
|
||||
|
||||
---
|
||||
|
||||
## Production state
|
||||
|
||||
- **URL**: `https://healthlog.bombeck.io`
|
||||
- **`/api/version`**: `1.4.14` ✓
|
||||
- **Image digest**: `sha256:0ced46004a544a311627b7036f2f2aed75861b0c62576214a1303a17d20c3d22`
|
||||
(vorher v1.4.12: `sha256:791c2cd2a8eae5ec0a6fd666074c9f646c17414e3521d526267131c2a3b7edab`)
|
||||
- **Smoke** (curl mit Session-Cookie): `/`, `/auth/login`,
|
||||
`/settings/integrations`, `/admin` — alle 200
|
||||
- **Deploy-Methode**: `:1.4.14` von GHCR auf den Host gepullt, dort als
|
||||
`:latest` retagged (kein Compose-Edit), dann `docker compose up -d
|
||||
app` in `/data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss/`. Der
|
||||
parallele `main`-Branch-GHCR-Run war auf demselben SHA hängengeblieben
|
||||
(>45 min), wurde gecancelt — harmlos, weil der Tag-Build `:latest`
|
||||
bereits gepusht hatte. Retag-on-host war defensiv aber sicher.
|
||||
- **GitHub Release**: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.14
|
||||
- **Tag-Naming**: GHCR-OCI-Tag ist `:1.4.14` (kein `v`-Prefix);
|
||||
git-Tag ist `v1.4.14`. Hard-Rule's `:v1.4.14`-Slug needs a docs fix.
|
||||
|
||||
---
|
||||
|
||||
## Codex-OAuth Status (das v1.4.7–v1.4.13 Saga-Ende)
|
||||
|
||||
- **Resolution**: ChatGPT-Account-Auth verlangt das Modell-Slug
|
||||
`gpt-5.3-codex`. Das ist canonical, gepflegt aus
|
||||
`openai/codex` → `codex-rs/models-manager/models.json`. Sowohl
|
||||
`gpt-5-codex` als auch das v1.4.13-Fallback `gpt-5` werden vom
|
||||
Codex-Backend mit ChatGPT-Subscription explizit abgelehnt
|
||||
(`The 'gpt-5' model is not supported when using Codex with a ChatGPT
|
||||
account.`).
|
||||
- **Spec extension**: `docs/codex-protocol-spec.md` §7a dokumentiert
|
||||
jetzt die ChatGPT-Account-Auth-Allow-List + die Lehre, dass die Slugs
|
||||
in `model_migration.rs` Migration-_Prompts_ sind, keine Wire-Slugs.
|
||||
- **Operator-Override**: `CODEX_MODEL`-Env-Var auf apps-01 lässt
|
||||
alternative Slugs ohne Rebuild testen — als Sicherheitsnetz für
|
||||
andere Plan-Tarife oder zukünftige OpenAI-Slug-Wechsel sinnvoll.
|
||||
- **Live-Probe verifiziert**: `/api/ai/test` und
|
||||
`/api/insights/generate` laufen beide gegen das ChatGPT-Plus-Abo
|
||||
des Live-Tenants durch. `/tmp/v15-codex-working.png` zeigt das
|
||||
grüne "ChatGPT connected"-Badge in `/settings/ai`.
|
||||
|
||||
---
|
||||
|
||||
## Docs / Landing
|
||||
|
||||
- **healthlog-docs** (Starlight) — 7 Files aktualisiert: AI-Insights
|
||||
(Codex device-code-Pfad neu), Admin-Settings (Backups + Users
|
||||
Sektionen), Environment-Variables (`CODEX_MODEL`), Updates (Backups-
|
||||
View), Introduction (One-Click Codex-Flow), Troubleshooting
|
||||
(Codex-Failure-Modes), Dashboard-Customization (Perf-Note).
|
||||
Commit `06bc616`. Coolify auto-redeploys docs.healthlog.dev.
|
||||
- **healthlog-landing** (Next.js) — `softwareVersion 1.4.6 → 1.4.14`
|
||||
in JSON-LD; AI-Providers-Copy auf "one-click device-code Codex
|
||||
flow, no API plan needed" angepasst. Commit `92c6588`.
|
||||
|
||||
---
|
||||
|
||||
## GitHub releases
|
||||
|
||||
`v1.4.7` … `v1.4.13` waren bereits real-time während des
|
||||
Codex-OAuth-Iterationstags veröffentlicht; nur `v1.4.13` war pending
|
||||
und wurde nachgezogen. `v1.4.14` selbst ist jetzt als Latest geflagged.
|
||||
|
||||
---
|
||||
|
||||
## Empfohlener v1.4.15 Backlog
|
||||
|
||||
Aus `.planning/v1415-backlog.md` (severity-geordnet, Datei:Zeile, ein
|
||||
Satz Recommendation pro Item). Top-Hits:
|
||||
|
||||
### HIGH
|
||||
|
||||
- **H4d+H5d** — `StatusCardGrid` i18n: Severity-Labels, Card-Titel,
|
||||
CTAs, `fmtRelative`, `fmtUptime`, StatusBadge-aria-label nach
|
||||
`admin.statusGrid.*` migrieren (EN+DE Parität, ~15 Keys × 2 Locales).
|
||||
- **H2** — `requireAdmin()` server-side auf `/admin/[section]/page.tsx`
|
||||
und `/admin/page.tsx` ziehen, damit Non-Admins 403 sehen, bevor das
|
||||
Shell paint-flickert.
|
||||
- **H-1** — "Wipe all data" Entscheidung: (a) Scope auf `MoodEntry`,
|
||||
`RefreshToken`, `DataBackup`, `Device`, `UserAchievement`,
|
||||
`IdempotencyKey` + verschlüsselte Codex/AI/moodLog-Spalten weiten,
|
||||
oder (b) Operation umbenennen ("Reset health data") und "preserved"-
|
||||
Liste explizit zeigen. Recommend (a) für GDPR-Art-17-Alignment.
|
||||
|
||||
### MEDIUM (Highlights)
|
||||
|
||||
- **M-2 security** `/api/admin/users/[id]/force-logout` ohne Rate-Limit
|
||||
und ohne Last-Admin-Guard.
|
||||
- **M-3 security** Wide-Event-`meta`-Strings nicht durch
|
||||
`redactSecrets()` gezogen — `addMeta()` braucht den Filter.
|
||||
- **M-5 security** Wipe-Rate-Limit BEFORE auth — unauth'd Traffic
|
||||
mutiert `rate_limits`. Reorder: `requireAdmin()` zuerst, dann key by
|
||||
`user.id`.
|
||||
- **M1 code-review** `/insights` TrendCard-Tile-Strip noch ohne
|
||||
`directionSentiment` verdrahtet (war v1.4.14 nur Dashboard).
|
||||
- **M2 code-review** Legacy-Admin-Anchor-301s leaken Slug-Existenz an
|
||||
unauth'd Hits — Redirect-Block muss NACH dem Session-Check.
|
||||
- **M3 code-review** `BackupsSection` 2-Sek-Timeout für Refetch ist ein
|
||||
Guess — auf 5–8s mit Double-Invalidate, oder Job-State pollen.
|
||||
|
||||
### LOW
|
||||
|
||||
- **L-1 security** `/api/auth/codex/callback` als PUBLIC_PATH gelistet,
|
||||
aber kein Route-File existiert — Eintrag aus `proxy.ts` löschen.
|
||||
- **L-2 security** Codex `codex_device`-Cookie ohne Per-User-Binding
|
||||
— `userId` in den encrypted Blob einbetten, Mismatch rejecten.
|
||||
- **L7 design** `/insights` Sticky-`<nav>` ohne `aria-label`.
|
||||
- **L8 design** Mobile-Strip-Pattern für `<FeedbackInboxSection>`-Tabs
|
||||
und `<AdminShell>`-Mobile-Section-Strip (snap-x snap-mandatory).
|
||||
|
||||
### Recharts-Replacement (eingefroren auf v1.4.15+)
|
||||
|
||||
108 KiB pro Chart-Page sparbar, aber Effort L (alle Charts in
|
||||
`src/components/charts/` müssen neu, plus neue Dependency). v1.4.14
|
||||
no-new-deps Hard-Rule blockiert; v1.4.15+ wenn die Range frei wird.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload-Reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — der neue
|
||||
Service-Worker holt sonst noch v1.4.13-Chunks aus dem Cache. Einmalig
|
||||
ausreichend.
|
||||
@@ -1,151 +0,0 @@
|
||||
# v1.4.14 UX friction audit (Phase 5)
|
||||
|
||||
**Date:** 2026-05-09
|
||||
**Method:** Static code walkthrough of the canonical user journey:
|
||||
login → dashboard → add measurement → view insights → admin overview →
|
||||
admin users → admin backups → settings/integrations → settings AI →
|
||||
logout. Phase-3 e2e fixtures were referenced for behavioural details
|
||||
already covered there; the local Node-25 build issue (documented in
|
||||
`STATE.md`) prevented running Playwright as the audit driver, so this
|
||||
list comes from reading the rendered components and route handlers.
|
||||
|
||||
Severity: **CRITICAL** ships tonight, **HIGH** ships tonight if scoped,
|
||||
**MEDIUM/LOW** parked for v1.4.15 unless a bigger v1.4.14 audit batches them.
|
||||
|
||||
## Top friction points
|
||||
|
||||
### 1. **MEDIUM** — Dashboard quick-add menu items both said "Add"
|
||||
|
||||
- File: `src/app/page.tsx:380`, `src/app/page.tsx:384`.
|
||||
- Symptom: the dropdown trigger says "Add" and both menu items
|
||||
underneath also rendered the literal "Add" (because
|
||||
`measurements.addMeasurement` and `mood.addEntry` both translate to
|
||||
the bare verb). Phase-3's `measurement-flow.spec.ts` worked around it
|
||||
with `getByRole("menuitem").first()`. Screen-reader users heard two
|
||||
indistinguishable rows.
|
||||
- **Fixed tonight** — added `dashboard.quickAddMeasurement` /
|
||||
`dashboard.quickAddMood` keys (EN + DE) and labelled the menu items
|
||||
by what's being added ("Measurement" / "Mood entry"). e2e spec now
|
||||
targets by name.
|
||||
|
||||
### 2. **HIGH** — Missing "Remove saved key" CTA in `/settings/ai`
|
||||
|
||||
- File: `src/components/settings/ai-section.tsx`.
|
||||
- Symptom: once an OpenAI / Anthropic / Local key was saved, the only
|
||||
way to clear it was to type a different key over the placeholder.
|
||||
Users couldn't fall back to the admin/Codex provider without a
|
||||
workaround.
|
||||
- **Fixed tonight** — see commit `feat(settings): allow removing saved
|
||||
AI provider key`.
|
||||
|
||||
### 3. **HIGH** — `/admin/users` icon-only buttons missed accessible names
|
||||
|
||||
- File: `src/components/admin/user-management-section.tsx`.
|
||||
- Symptom: the per-row Edit / Reset password / Force-logout buttons
|
||||
rendered only an icon. `title` is acceptable as a fallback per HTML
|
||||
spec but axe-core flags it as a sub-optimal source. Tab order also
|
||||
produced repeated tooltip-only labels.
|
||||
- **Fixed tonight** — added explicit `aria-label`, set the leading icon
|
||||
`aria-hidden`. Part of the a11y commit.
|
||||
|
||||
### 4. **HIGH** — `<PasswordInput>` toggle in admin/\_shared had no label
|
||||
|
||||
- File: `src/components/admin/_shared.tsx`.
|
||||
- Symptom: the eye/eye-off button had no `aria-label`, hitting axe-core
|
||||
`button-name` (serious). Two copies of `<PasswordInput>` existed —
|
||||
the canonical one in `src/components/settings/password-input.tsx`
|
||||
was already fixed in phase-3.
|
||||
- **Fixed tonight** — admin's copy now re-exports the canonical
|
||||
component. Part of the a11y commit.
|
||||
|
||||
### 5. **MEDIUM** — Trend-arrow colour was meaning-blind in v1.4.6
|
||||
|
||||
- File: `src/components/charts/trend-card.tsx`.
|
||||
- Symptom: P4 of v1.4.6 stripped the colour because the original "up
|
||||
red, down green" rule was wrong for half the metrics (mood up =
|
||||
good, weight up = bad). The result was muted everywhere and lost
|
||||
visual signal.
|
||||
- **Fixed tonight** — `directionSentiment` prop with three buckets
|
||||
(`up-good` / `up-bad` / `neutral`). All dashboard tiles wired. See
|
||||
commit `feat(ui): trend arrow color reflects metric-specific
|
||||
direction sentiment`.
|
||||
|
||||
### 6. **MEDIUM** — Status-card audit-log row had an empty `<dd>`
|
||||
|
||||
- File: `src/components/admin/status-card-grid.tsx:220`.
|
||||
- Symptom: the audit-log card filled its third metric slot with
|
||||
`{ label: "—", value: "" }`. Empty `<dd>` cells aren't navigable and
|
||||
are flagged by axe `definition-list`.
|
||||
- **Fixed tonight** — dropped the placeholder; the 3-col grid keeps
|
||||
layout. Test updated.
|
||||
|
||||
### 7. **MEDIUM** — Feedback / mood / medication status used raw colours
|
||||
|
||||
- Files: `src/components/admin/feedback-inbox-section.tsx`,
|
||||
`src/components/medications/medication-card.tsx`,
|
||||
`src/components/settings/ai-section.tsx`.
|
||||
- Symptom: `text-dracula-green` for "success" reads as a colour, not a
|
||||
meaning. Light mode (Alucard) failed AA contrast for raw Dracula
|
||||
greens / oranges on white cards.
|
||||
- **Fixed tonight** — added semantic `--success` / `--warning` /
|
||||
`--info` tokens to `globals.css` (dark = Dracula alias, light =
|
||||
AA-contrast counterpart matching `--destructive`'s pattern). Migrated
|
||||
feedback BUG badge → `destructive`, "published to GitHub" badge →
|
||||
`success`, medication status (in-window → success, very-overdue →
|
||||
warning), AI section connection badges. See commit `style(ui): use
|
||||
semantic tokens for mood and feedback indicators`.
|
||||
|
||||
### 8. **LOW** — Logout from `/settings` requires going to topbar
|
||||
|
||||
- Files: `src/components/layout/sidebar-nav.tsx`,
|
||||
`src/components/layout/top-bar.tsx`.
|
||||
- Symptom: on desktop the sidebar's user-section dropdown carries
|
||||
Logout, but on mobile within `/settings/*` the topbar's user-menu is
|
||||
still the only path. Not broken, but the action is two interactions
|
||||
away from settings landing.
|
||||
- **Deferred to v1.4.15** — would need a new "Sign out" surface in the
|
||||
Settings shell.
|
||||
|
||||
### 9. **LOW** — Insights section nav has no `aria-label`
|
||||
|
||||
- File: `src/app/insights/page.tsx:1604` (`<nav>` block).
|
||||
- Symptom: the sticky horizontal section navigator inside `/insights`
|
||||
is rendered as `<nav>` without `aria-label`, so multiple nav landmarks
|
||||
on the page show up as "navigation, navigation, navigation" in screen
|
||||
reader landmark lists.
|
||||
- **Deferred to v1.4.15** — needs an i18n key plus a check that the same
|
||||
`<nav>` doesn't already have one elsewhere on the page.
|
||||
|
||||
### 10. **LOW** — `/admin` overview "Sections" list has no aria-label
|
||||
|
||||
- File: `src/app/admin/page.tsx`.
|
||||
- Symptom: the quick-jump grid below the status cards is wrapped in a
|
||||
`<ul>` but the parent `<div>` has no role/landmark. The sidebar nav
|
||||
already exists, so duplicating it as a quick-jump is fine, but the
|
||||
list could be wrapped in a `<nav aria-labelledby=...>` for parity
|
||||
with the sidebar.
|
||||
- **Deferred to v1.4.15**.
|
||||
|
||||
## v1.4.15 backlog (from this audit)
|
||||
|
||||
- Dedicated Sign-out CTA in `<SettingsShell>` mobile mode.
|
||||
- `aria-label` on `<nav>` landmarks across `/insights` and `/admin` overview.
|
||||
- Broader semantic-token sweep (`telegram-card`, `ntfy-card`,
|
||||
`web-push-card`, `account-section`, `about-section`,
|
||||
`test-connection-button`) — same colour swap as feedback / AI / meds
|
||||
but bigger blast radius; needs a single-PR sweep.
|
||||
- "Late" medication status uses `dracula-yellow` directly; once we have
|
||||
a `--caution` semantic token, swap that too.
|
||||
- Insights long-page section navigator could grow a "back to top"
|
||||
button on mobile for one-handed reach.
|
||||
|
||||
## Method note
|
||||
|
||||
The local environment ran Node 25 which currently surfaces the
|
||||
`Cannot read private member #state` Reflect.get bug in
|
||||
`src/lib/api-handler.ts` (documented in the repo notes and the v1.4.6
|
||||
summary). That blocks both `pnpm build` and `pnpm dev`, so the
|
||||
Playwright e2e suite couldn't drive the journey from this machine.
|
||||
The reactivated `/admin` axe-core spec (`e2e/a11y.spec.ts`) and the
|
||||
existing `measurement-flow.spec.ts` will validate the fixes once CI
|
||||
runs on Node 22 (canonical CI version, see `.github/workflows/`).
|
||||
@@ -1,181 +0,0 @@
|
||||
# v1.4.15 — Auto-deployment audit & design
|
||||
|
||||
**Stage:** C2
|
||||
**Last updated:** 2026-05-09 (v1.4.15 release cycle)
|
||||
**Status:** designed + shipped — see commits in `.planning/phase-C2-report.md`.
|
||||
|
||||
## Background
|
||||
|
||||
Through v1.4.6 → v1.4.14 the recipe to deploy a freshly-published GHCR image
|
||||
to production has been:
|
||||
|
||||
1. Tag a release → GHCR builds and pushes the image (`docker-publish.yml`).
|
||||
2. Trigger Coolify "Deploy" via the dashboard.
|
||||
3. **Manually SSH** to `apps-01` and force-pull the image because Coolify's
|
||||
own deploy did not always honour the new digest:
|
||||
|
||||
```bash
|
||||
ssh apps-01 'docker pull ghcr.io/mbombeck/healthlog:1.4.X && \
|
||||
docker tag ghcr.io/mbombeck/healthlog:1.4.X ghcr.io/mbombeck/healthlog:latest && \
|
||||
cd /data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss && \
|
||||
docker compose up -d app'
|
||||
```
|
||||
|
||||
4. Verify `/api/version` returned the new version + image digest changed.
|
||||
|
||||
The maintainer's request for v1.4.15 — **eliminate step 2 and 3**. When GHCR
|
||||
has a new image, the production container should redeploy by itself.
|
||||
|
||||
## Investigation — what Coolify supports
|
||||
|
||||
Coolify version on `apps-01.bombeck.io`: **`4.0.0-beta.470`** (verified via
|
||||
`mcp__coolify-apps01__get_version`).
|
||||
|
||||
The current application config (UUID `pg8wggwogo8c4gc4ks0kk4ss`,
|
||||
`/api/v1/applications/pg8wggwogo8c4gc4ks0kk4ss`) shows:
|
||||
|
||||
- `build_pack: dockercompose`
|
||||
- `git_repository: MBombeck/HealthLog`, `git_branch: main`
|
||||
- `source_type: App\Models\GithubApp` — this matters: Coolify's "auto-deploy
|
||||
on git push" is wired to the GitHub App webhook, **not** to the GHCR
|
||||
package webhook. There is no native "redeploy when registry has new image"
|
||||
mode for `dockercompose`-typed apps.
|
||||
- `manual_webhook_secret_github: null` — no webhook secret configured today.
|
||||
|
||||
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
|
||||
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
|
||||
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 }}'`."
|
||||
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.
|
||||
|
||||
`(2)` solves the "trigger" half; `(3)` solves the "report" half. We use both.
|
||||
|
||||
## Decision — chosen approach
|
||||
|
||||
**Path A (chosen):** GitHub Actions → Coolify deploy webhook → Coolify
|
||||
notifications → HealthLog `POST /api/internal/deploy-webhook`.
|
||||
|
||||
Rationale:
|
||||
|
||||
- Native to Coolify; no extra daemon on `apps-01`.
|
||||
- Both halves of the auto-deploy contract (trigger + status) live in
|
||||
configuration we already control.
|
||||
- Watchtower would mean shipping a new container, an extra footprint to
|
||||
audit, and (since v1.4.14 has multi-tenant prep with worker-mode
|
||||
separation) a polling daemon that might wake the app off-pattern. The
|
||||
Coolify webhook is event-driven and free.
|
||||
|
||||
**Path B (rejected — but kept on the table for v1.4.16 if Coolify webhook
|
||||
proves unreliable):** Watchtower with a per-image label
|
||||
(`com.centurylinklabs.watchtower.enable=true`) gated on `ghcr.io/mbombeck/healthlog`.
|
||||
|
||||
## Implementation plan (3 atomic commits)
|
||||
|
||||
1. **`feat(deploy): auto-deploy on GHCR push (no more manual force-pull)`**
|
||||
- `.github/workflows/docker-publish.yml`: after the `Build & push image`
|
||||
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).
|
||||
- Document required secrets in `.env.example`.
|
||||
|
||||
2. **`feat(deploy): admin notification + audit log on deploy success/failure`**
|
||||
- New endpoint: `src/app/api/internal/deploy-webhook/route.ts` that
|
||||
accepts a Coolify-format JSON payload and:
|
||||
- validates `X-Deploy-Webhook-Secret` header (timing-safe) against
|
||||
`process.env.DEPLOY_WEBHOOK_SECRET`;
|
||||
- rate-limits by client IP;
|
||||
- writes an `auditLog` row with `system.deploy.{success,failure}`;
|
||||
- on failure, calls `dispatchNotification({ eventType: "SYSTEM_ALERT" })`
|
||||
for every admin user (so Telegram is paged if configured) — same
|
||||
pattern as `src/lib/integrations/status.ts:maybeAlertAdmins`.
|
||||
- Webhook handler is a Next.js Route Handler — no new deps.
|
||||
- Unit-tested with the same mock pattern as
|
||||
`src/app/api/withings/webhook/__tests__/route.test.ts`.
|
||||
|
||||
3. **Document everything** in this file + `phase-C2-report.md`.
|
||||
|
||||
## Maintainer-side configuration after merge
|
||||
|
||||
Since the automated tooling does not have access to the GitHub repo
|
||||
settings or Coolify's UI directly, three manual steps remain after the
|
||||
auto-deploy code ships:
|
||||
|
||||
1. **Generate a Coolify API token** in Coolify UI → `Keys & Tokens` →
|
||||
"Create New Token" with permission `*` (or `read:sensitive` plus deploy).
|
||||
Token format: `n_<base62>`.
|
||||
2. **Add two GitHub repository secrets**:
|
||||
- `COOLIFY_WEBHOOK` =
|
||||
`https://<COOLIFY_INSTANCE>/api/v1/deploy?uuid=<APPLICATION_UUID>&force=false`
|
||||
(the actual values live in repo secrets only).
|
||||
- `COOLIFY_TOKEN` = the token from step 1.
|
||||
3. **Configure Coolify outgoing notifications**:
|
||||
- Settings → Notifications → Webhook
|
||||
- URL: `https://healthlog.bombeck.io/api/internal/deploy-webhook`
|
||||
- Add custom header: `X-Deploy-Webhook-Secret: <DEPLOY_WEBHOOK_SECRET>`
|
||||
(the value placed into `apps-01:.env` for the HealthLog container)
|
||||
- Enable: deployment success + deployment failure
|
||||
4. **Set `DEPLOY_WEBHOOK_SECRET` env var** on `apps-01` (HealthLog
|
||||
container `.env` + Coolify environment variables UI). Generate with
|
||||
`openssl rand -hex 32`.
|
||||
|
||||
These four manual steps are tracked in `.planning/phase-C2-report.md`'s
|
||||
"Maintainer-side follow-up" section. The code is fully complete and tested
|
||||
without them; auto-deploy simply doesn't fire until the secrets are
|
||||
populated.
|
||||
|
||||
## Failure modes considered
|
||||
|
||||
| Failure | What happens |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| Coolify deploy URL down | GH Actions step continues (`continue-on-error: true`); image stays on GHCR, deploy falls back to manual. |
|
||||
| Coolify notification fires before image pull | Coolify retries the pull internally; final status (success/failure) reaches our webhook regardless. |
|
||||
| Webhook secret leaked | Rotate `DEPLOY_WEBHOOK_SECRET`, redeploy. Audit-log entry on every received call shows source IP. |
|
||||
| Webhook handler down at deploy time | Coolify retries 3× per docs; final missed event is recoverable from Coolify's own deployment log. |
|
||||
| Coolify API rejects token (permission scope) | GH Actions step prints the response body; the next merge surfaces it as a workflow failure. |
|
||||
| GHCR push succeeds but webhook step is broken | Image still gets to GHCR; manual deploy still works as a fallback. |
|
||||
|
||||
## Watchtower — why not (revisited)
|
||||
|
||||
If Path A breaks (Coolify webhook flaky, beta channel regression, …),
|
||||
v1.4.16 can install Watchtower:
|
||||
|
||||
```yaml
|
||||
# /data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss/docker-compose.yaml
|
||||
# (additional services block, NOT in the main compose)
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower:1.7.1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command:
|
||||
- --interval=300
|
||||
- --label-enable
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
with the existing app service labelled
|
||||
`com.centurylinklabs.watchtower.enable=true`. We deliberately do not ship
|
||||
this in v1.4.15 — adding a polling daemon to the production host is a
|
||||
material expansion of the trusted compute base and the Coolify path is
|
||||
good enough.
|
||||
|
||||
## References
|
||||
|
||||
- Coolify GitHub Actions guide:
|
||||
<https://coolify.io/docs/applications/ci-cd/github/actions>
|
||||
- Coolify Authorization API docs:
|
||||
<https://coolify.io/docs/api-reference/authorization>
|
||||
- v1.4.14 manual force-pull recipe: `.planning/phase-8-report.md`
|
||||
- C3 docker-publish reliability fix: `docs/audit/v1415-ci-reliability.md`
|
||||
@@ -1,182 +0,0 @@
|
||||
# v1.4.15 CI/e2e reliability audit
|
||||
|
||||
Stage C3 of the v1.4.15 release cycle. Window: last 30 days of
|
||||
`gh run list` (2026-04-09 → 2026-05-09T19:00 Berlin), 200 runs across
|
||||
all workflows. Maintainer brief: "Tests müssen wirklich durchlaufen" —
|
||||
pass-rate matters more than coverage breadth.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **e2e: 0 / 47 passes (0 %).** Single deterministic regression — A2's
|
||||
`/admin` overview redesign hard-codes Dracula `text-dracula-green`
|
||||
on `bg-card` / `bg-muted`. Playwright's stock `colorScheme: "light"`
|
||||
scans a layout users never see (Dracula is dark-default). Fixed
|
||||
in `41945b2` by forcing `colorScheme: "dark"` on every project.
|
||||
- **docker-publish: 30 / 33 completed pass (91 %).** One outright
|
||||
hang (47 min, v1.4.14 main) traced to gha cache-scope contention
|
||||
with the parallel tag run on the same SHA. Fixed in `249c42b` —
|
||||
per-ref scope + 30-min job timeout.
|
||||
- **Integration: 47 / 47 (100 %).** Healthy.
|
||||
- **Security & Quality: 43 / 46 (93 %).** Three failures, all owned
|
||||
by upstream typecheck regressions in `dashboard-layout.test.ts`
|
||||
introduced by stage A4's tile-visibility refactor — outside C3
|
||||
scope; deferred to stage D / A4 follow-up.
|
||||
|
||||
## 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 % |
|
||||
|
||||
`Completed pass-rate = success / (success + failure)` — strips out
|
||||
the "cancelled" entries that come from the `concurrency.cancel-in-progress`
|
||||
group on rapid-fire pushes (these are by-design and don't reflect
|
||||
test health).
|
||||
|
||||
## Top 5 failure modes
|
||||
|
||||
### 1. e2e a11y axe-core color-contrast — 34 / 34 e2e failures
|
||||
|
||||
Every single failed e2e run since A2 landed (commit `a967895`,
|
||||
2026-05-09T18:25 Berlin) reports the same axe-core `serious`
|
||||
finding:
|
||||
|
||||
```
|
||||
"contrastRatio": 1.18, "expectedContrastRatio": "4.5:1",
|
||||
"fgColor": "#50fa7b", "bgColor": "#eeeef7"
|
||||
"target": ".text-dracula-green … inside .bg-card"
|
||||
```
|
||||
|
||||
Source: `<SystemStatusSummary>` and `<RecentAuditPreview>` in the
|
||||
admin overview redesign. **Root cause is not the components** —
|
||||
HealthLog ships dark mode as default (per repo conventions: "Dark
|
||||
mode is default", `globals.css:142` sets `color-scheme: dark` on
|
||||
`:root`).
|
||||
The inline theme bootstrapper at `src/app/layout.tsx:73` resolves
|
||||
the active theme via `prefers-color-scheme: dark`. Playwright's
|
||||
stock context defaults to `colorScheme: "light"`, so axe-core
|
||||
was scanning a light-themed layout no real user ever sees.
|
||||
|
||||
**Fix shipped (`41945b2`)**: `playwright.config.ts` now sets
|
||||
`colorScheme: "dark"` on the top-level `use` block AND on each
|
||||
project's `use` (the `...devices[...]` spread overwrites
|
||||
inherited `colorScheme`, so the per-project override is required).
|
||||
|
||||
### 2. Older e2e runs — `pnpm/action-setup` "Multiple versions" — 8 / 47 e2e failures
|
||||
|
||||
Pre-A2 e2e failures (May 8, before 17:00 Berlin) come from a
|
||||
different root cause: `pnpm/action-setup@v4` failed early with
|
||||
|
||||
```
|
||||
Error: Multiple versions of pnpm specified:
|
||||
- version 10 in the GitHub Action config with the key "version"
|
||||
- version pnpm@10.31.0 in the package.json with the key "packageManager"
|
||||
```
|
||||
|
||||
This was already fixed by removing the `version: 10` parameter
|
||||
from `actions/setup-node@v4` (no commit ID — the fix predates the
|
||||
audit window's start in the workflow's actual git log). All
|
||||
workflows now rely solely on `package.json#packageManager`.
|
||||
|
||||
### 3. docker-publish hang on shared gha cache — 1 / 33 completed runs
|
||||
|
||||
The v1.4.14 main run (`25605648398`, SHA `e5fae9bce6cb`) hung 47
|
||||
minutes on `[linux/amd64 runner 2/14] RUN apk add tzdata` while
|
||||
the parallel `v1.4.14` tag run on the same SHA finished cleanly
|
||||
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.
|
||||
- `timeout-minutes: 30` on the job — bounds future deadlocks.
|
||||
|
||||
### 4. Typecheck regression — `dashboard-layout.test.ts` — 3 / 46 S&Q failures
|
||||
|
||||
Stage A4's tile-visibility refactor (`8ccdfac`) tightened
|
||||
`DashboardWidgetId` to a string-literal union but the existing
|
||||
`dashboard-layout.test.ts` still passes plain `string` for the
|
||||
`id` field. Errors at lines 91, 104, 116. Out of C3 scope —
|
||||
flagged for the A4 owner / stage-D senior-dev review to fix.
|
||||
|
||||
### 5. docker-publish "cancelled" — 26 / 60 runs (45 %)
|
||||
|
||||
These are NOT failures — `concurrency.cancel-in-progress: true` on
|
||||
the `docker-publish` workflow correctly cancels superseded runs
|
||||
when a follow-up commit lands within the build window. The high
|
||||
count reflects the v1.4.15 release cycle's rapid-fire commit cadence
|
||||
(7 parallel work-streams pushing across A1–A5 buckets), not infra
|
||||
fragility. No action needed beyond what's already in the workflow.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Tonight (in this C3 stage)
|
||||
|
||||
- [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.
|
||||
- [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.
|
||||
|
||||
### Defer to v1.4.16 (out of C3 scope, owned elsewhere)
|
||||
|
||||
- **`dashboard-layout.test.ts` typecheck regression**: A4 owner or
|
||||
stage-D senior-dev sweep. 3 errors, ≤10 lines to fix (`as` cast or
|
||||
fixture-data update).
|
||||
- **e2e Node-version pinning**: `actions/setup-node@v4` uses
|
||||
`node-version: 22` — already correct for CI parity. No change.
|
||||
But `actions/setup-node@v4` will warn about Node-20 actions
|
||||
deprecation come Sept 2026 — schedule action-version refresh
|
||||
(cache@v4, checkout@v4, upload-artifact@v4) into v1.5 backlog.
|
||||
- **e2e flake reduction at the test-level**: with the colorScheme
|
||||
fix in place, the natural next stage is to track real flakes
|
||||
(timing-based timeouts in `dashboard.spec.ts`,
|
||||
`insights-generate.spec.ts`). Stage B-mobile may surface these as
|
||||
it adds mobile assertions; capture in C3.5 if needed.
|
||||
- **`Auto-merge Dependabot` reliability audit**: zero runs in the
|
||||
30-day window (no Dependabot PRs landed). Re-audit when
|
||||
Dependabot churn resumes.
|
||||
- **Trivy container scan**: currently `continue-on-error: true` —
|
||||
flagged for the security-review pass in stage D.
|
||||
|
||||
## What "tests must really run through" means after this C3
|
||||
|
||||
| Stage | Pre-C3 | Post-C3 (expected) |
|
||||
| -------------------------------------- | ------ | ------------------ |
|
||||
| Integration | 100 % | 100 % |
|
||||
| Security & Quality (typecheck + tests) | 93 % | unchanged (A4 fix) |
|
||||
| Docker publish (release commits) | 91 % | ≥ 95 % |
|
||||
| e2e | 0 % | ≥ 90 % |
|
||||
| Post-publish reachability | n/a | informational |
|
||||
|
||||
The 0 → ≥90 % e2e jump is the headline — every push since v1.4.14
|
||||
went red on the same gate, so any non-flake green run after
|
||||
`41945b2` lands counts as full restoration of the suite.
|
||||
|
||||
## Open questions for the next release cycle
|
||||
|
||||
- **Should `Security & Quality` continue running ESLint with
|
||||
`continue-on-error: true`?** Stage A1's nav refactor cleaned the
|
||||
bulk of the legacy violations; the remaining 12 warnings are all
|
||||
in `src/app/settings/page.tsx`, slated for the v1.5
|
||||
settings-split refactor. Flip to blocking once that refactor
|
||||
lands.
|
||||
- **Does the `Container Security` Trivy step need to gate
|
||||
releases?** Currently `continue-on-error: true`. Project
|
||||
preference (per AGENTS.md): keep informational in v1.4.x, flip
|
||||
to blocking in v1.5 with documented allowlist for false
|
||||
positives.
|
||||
|
||||
## Appendix: full run list snapshot
|
||||
|
||||
200 runs across the 30-day window. Aggregations re-derivable via
|
||||
`jq 'group_by(.workflowName) | map({...})'` per the snippets in
|
||||
`.planning/phase-C3-report.md`.
|
||||
@@ -1,97 +0,0 @@
|
||||
# v1.4.15 — Empty-states audit
|
||||
|
||||
Date: 2026-05-09
|
||||
Owner: Stage C5 work-stream (parallel with C1 + C4).
|
||||
Primitive: `src/components/ui/empty-state.tsx` (already shipped earlier
|
||||
in v1.4.15; sweep below tracks adoption).
|
||||
|
||||
The audit walks every list / table / tile surface a brand-new user
|
||||
hits, classifies the empty path, and records the fix. "Useful" means
|
||||
icon + title + description + (where it makes sense) a primary action,
|
||||
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 the live tenant, 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)
|
||||
|
||||
- `/insights` correlation-card empty branches (`scatterData.length < 5`)
|
||||
already use a custom in-card empty surface (icon + 2 lines) that's
|
||||
tighter than the generic primitive — refactor would only churn
|
||||
classes, no UX win. Leave for v1.4.16 if C4 i18n sweep wants the
|
||||
consistency.
|
||||
- `/admin/danger-zone` and `/admin/services` are not lists — irrelevant.
|
||||
- `/admin/system-status` shows operational data not user records —
|
||||
empty paths handled by per-card "—" + status text.
|
||||
- `/admin/reminders` is a settings form, not a list.
|
||||
- Dashboard `/insights` BMI `<p>{t("common.noData")}</p>` for a user
|
||||
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.
|
||||
|
||||
## Tests added
|
||||
|
||||
- `src/app/__tests__/measurement-list-empty.test.tsx` — confirms
|
||||
`<EmptyState>` renders with the "Log your first measurement" CTA on
|
||||
a fresh account (mocked TanStack Query result of
|
||||
`{ measurements: [], meta: { total: 0 }}`).
|
||||
- `src/components/admin/__tests__/users-section-empty.test.tsx` —
|
||||
confirms the user-management empty state renders the filter-aware
|
||||
description and a "Reset filter" action when the admin filter is
|
||||
active.
|
||||
- `src/components/admin/__tests__/backups-section-empty.test.tsx` —
|
||||
confirms the backups empty state surfaces the secondary "Backup
|
||||
now" CTA inside the empty card.
|
||||
- Existing `src/components/ui/__tests__/empty-state.test.tsx` covers
|
||||
the primitive (4 variants).
|
||||
|
||||
## Counts
|
||||
|
||||
- **Empty-states added or upgraded**: 13.
|
||||
- admin sub-routes: 6 (users, backups, login-overview, api-tokens,
|
||||
feedback, overview-preview).
|
||||
- feature lists: 4 (measurements, mood, medications, achievements).
|
||||
- insights: 2 (top-level no-data, BMI no-height).
|
||||
- dashboard: 1 (tile-strip + charts both empty).
|
||||
- **i18n keys added**: 18 in EN + DE under
|
||||
`emptyStates.*` (1), `measurements.empty.*` (4),
|
||||
`mood.empty.*` (4), `medications.empty.*` (3),
|
||||
`admin.section.users.empty.*` (3), `admin.section.backups.empty.*`
|
||||
(2), `admin.section.login-overview.empty.*` (3),
|
||||
`admin.section.api-tokens.empty.*` (2),
|
||||
`admin.section.feedback.empty.*` (2),
|
||||
`admin.overview.auditEmpty*` (1 reuses existing key + new
|
||||
description),
|
||||
`insights.empty.*` (3),
|
||||
`dashboard.empty.*` (3),
|
||||
`achievements.empty.*` (reuses `noneUnlockedYet`).
|
||||
- **No new dependencies**.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm test` after each commit (target: 0 regressions).
|
||||
- `pnpm typecheck` after each commit.
|
||||
- `pnpm lint` after each commit.
|
||||
|
||||
## Reference
|
||||
|
||||
Stage C5 work order: `.planning/STATE.md` § Stage C5.
|
||||
Primitive intro PR: see git log for `feat(ui): generic EmptyState
|
||||
primitive` (already on `main` before this stage started — section 1 of
|
||||
the C5 brief satisfied without a new commit).
|
||||
@@ -1,128 +0,0 @@
|
||||
# v1.4.15 — i18n coverage audit
|
||||
|
||||
**Date**: 2026-05-09
|
||||
**Stage**: C4
|
||||
**Scope**: every `t("…")` / `useTranslations()` call in `src/`
|
||||
vs. `messages/en.json` and `messages/de.json`. Goal: ensure every
|
||||
v1.4.15-introduced key (cleanup A1–A4, B-mobile, B1–B6, C2, C3) has a
|
||||
non-empty, non-placeholder translation in BOTH locales and follows the
|
||||
namespace conventions baked in by the Stage 4b admin reorg.
|
||||
|
||||
## Method
|
||||
|
||||
1. `grep -rh -oE 't\("[a-zA-Z][a-zA-Z0-9_.-]*"' src/` — extract every
|
||||
call-site key (1318 unique).
|
||||
2. Walk both JSON files manually (no `JSON.parse`, since duplicate keys
|
||||
silently shadow earlier values) — same parser used by the existing
|
||||
`i18n-locale-integrity.test.ts`.
|
||||
3. Compare:
|
||||
- **drift**: keys in EN but not DE, or vice versa
|
||||
- **empty**: `value === ""`
|
||||
- **placeholders**: `value === keyLastSegment`, or `/\b(TODO|FIXME|XXX|TBD)\b/i`
|
||||
4. Cross-locale identical strings checked per-namespace — many are
|
||||
legitimate (brand names, ICU placeholder strings, anglicisms used
|
||||
in DE) so the heuristic only flagged for review, not auto-fix.
|
||||
|
||||
## Findings
|
||||
|
||||
| Check | Count | Notes |
|
||||
| ---------------------------------- | ----: | -------------------------------- |
|
||||
| Total keys (EN) | 1817 | |
|
||||
| Total keys (DE) | 1817 | |
|
||||
| Keys only in EN | 0 | full parity |
|
||||
| Keys only in DE | 0 | full parity |
|
||||
| Empty values (EN) | 0 | |
|
||||
| Empty values (DE) | 0 | |
|
||||
| TODO/FIXME placeholders (EN) | 0 | |
|
||||
| TODO/FIXME placeholders (DE) | 0 | |
|
||||
| Used-in-`src/` but missing in EN | 0 | after excluding allow-list below |
|
||||
| EN==key (last segment, both EN+DE) | 5 | all legitimate (see §Legitimate) |
|
||||
|
||||
### Allow-list — used-key matches that are NOT i18n keys
|
||||
|
||||
These appeared in the grep but are deliberately excluded:
|
||||
|
||||
- `job.*` — Wide Event names passed to `withBackgroundEvent("job.x")`,
|
||||
not i18n.
|
||||
- `scheduler.tick` — Wide Event name.
|
||||
- `empty.measurements.*` — JSDoc `@example` block in
|
||||
`src/components/ui/empty-state.tsx`, never executed.
|
||||
- `settings.testConnection.errors.` — dynamic concat root in
|
||||
`src/components/settings/test-connection-button.tsx`. Concrete
|
||||
error-codes (`generic`, `credentials_rejected`, `timeout`, …) are
|
||||
all present.
|
||||
- `nope.also.missing`, `telegram.__definitely_missing__` — vitest
|
||||
fixtures asserting fallback behaviour
|
||||
(`src/lib/__tests__/server-translator.test.ts`).
|
||||
|
||||
### Legitimate EN==DE==key cases
|
||||
|
||||
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 |
|
||||
| `classifications.bodyFat.Fitness` | `Fitness` | identical (anglicism in DE medical/fitness context) |
|
||||
|
||||
A further 25 entries are EN==key but DE has an actual translation
|
||||
(`classifications.bmi.Underweight` → `Untergewicht`, etc.) — these are
|
||||
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 |
|
||||
|
||||
### Naming consistency
|
||||
|
||||
Repo doc note: Stage 4b moved admin keys under
|
||||
`admin.section.<slug>.*`. Verified all v1.4.15-introduced admin
|
||||
sections follow that pattern:
|
||||
|
||||
```
|
||||
admin.section.{system-status,general,services,integrations,
|
||||
feedback,reminders,users,api-tokens,login-overview,
|
||||
backups,danger-zone}
|
||||
```
|
||||
|
||||
Pre-Phase-4b legacy keys remain at the flat `admin.*` namespace (e.g.
|
||||
`admin.bugReportRepo`, `admin.userManagement`) — those are out of
|
||||
scope for this audit (renaming would touch many components for zero
|
||||
behavioural gain). New stage-B code did NOT introduce inconsistent
|
||||
flat keys; e.g. B5 used `onboarding.tour.*` (not `tour.*`); B4 used
|
||||
`achievements.*` (not flat `badges.*`); B1 used `admin.section.backups.*`
|
||||
(not flat `backups.*`); B2/B3 added `settings.{integrationStatus,notificationStatus}.*`
|
||||
under the existing `settings` umbrella.
|
||||
|
||||
## Outcome
|
||||
|
||||
**Zero gaps closed**. The five parallel stage-B work-streams and the
|
||||
C2/C3 stages each added their own EN+DE translations as part of their
|
||||
own commits, so by the time C4 ran sequentially after Batch 2 there was
|
||||
nothing left to back-fill. The audit instead converted to a
|
||||
quality-assurance sweep: tightening `i18n-locale-integrity.test.ts` to
|
||||
lock in the non-empty + non-placeholder invariants going forward.
|
||||
|
||||
## Test guard
|
||||
|
||||
`src/lib/__tests__/i18n-locale-integrity.test.ts` is extended with:
|
||||
|
||||
- assertion that no value in either locale file is the empty string
|
||||
- assertion that no value equals its key's last segment (with an
|
||||
explicit allow-list for the 5 legitimate cases above)
|
||||
- non-fatal warning (logged via `console.warn` in CI) for values
|
||||
containing `\bTODO\b` / `\bFIXME\b` / `\bXXX\b`
|
||||
|
||||
These guards prevent regression: any future change that ships a
|
||||
`"key": ""` or `"bugReport": "bugReport"` value will see CI go red.
|
||||
@@ -1,319 +0,0 @@
|
||||
# HealthLog v1.4.15 — Release Summary
|
||||
|
||||
## Release brief (DE, ≤ 220 Wörter)
|
||||
|
||||
✅ **v1.4.15 ist live** auf `https://healthlog.bombeck.io` — Image-Digest
|
||||
`sha256:ace7d441f47b…` (vorher v1.4.14: `0ced46004a54…`),
|
||||
`/api/version` meldet `1.4.15`.
|
||||
|
||||
🆕 **Was ist neu**
|
||||
|
||||
- **Erfolge** als eigene Seite (`/achievements`) mit Locked + Progress-
|
||||
Bars + Kategorien plus zuschaltbare „Letzte Erfolge"-Karte aufs
|
||||
Dashboard.
|
||||
- **Onboarding-Tour** beim ersten Dashboard-Besuch (Spotlight, Esc /
|
||||
Pfeile, `prefers-reduced-motion`); jederzeit unter Settings → Account
|
||||
wiederholbar.
|
||||
- **Doctor-Report v2:** Datumsbereich konfigurierbar (90 / 180 / 365
|
||||
Tage oder manuell, max. 2 Jahre) und Praxis-Name auf der Titelseite.
|
||||
- **Backups vollständig:** Download als `.json`, Upload, Restore hinter
|
||||
Triple-Confirm — alles auditiert.
|
||||
- **Withings + moodLog Status-UI** plus Telegram-Alert bei ≥ 3 Folge-
|
||||
Fehlern; Refresh-Token-Fail markiert Integration als reauth-pflichtig.
|
||||
- **Notification-Channel-Status** mit Re-Enable + Test-Send + Auto-
|
||||
Disable bei 410.
|
||||
- **`/admin`-Übersicht** zeigt Audit-Log-Vorschau + System-Snapshot
|
||||
statt Section-Grid (die wandert in die Sidebar).
|
||||
- **KI-Insights gehärtet:** striktes Output-Schema, Citation-Pflicht,
|
||||
Slug-Drift-Fallback-Kette + 1h-Cache (zero hallucinations).
|
||||
- **13 Empty-States** über Admin / Listen / Insights / Dashboard.
|
||||
- **Mobile-UX-Sweep:** Chart-Scroll, `/admin/users` Card-List, 44 px
|
||||
Tap-Targets, Mood-Card aufgeräumt.
|
||||
|
||||
🔧 **Was ist gefixt**
|
||||
|
||||
- BD-Zielbereich-Prozent zählt Sys/Dia-Paare korrekt (war 0 % bei
|
||||
Import-Drift).
|
||||
- Onboarding-Flackern raus, Skip-Link blockt Logo nicht mehr.
|
||||
- Bug-Report + Feedback verschwinden aus Sidebar / Bottom-Nav / Topbar
|
||||
wenn Admin den Toggle deaktiviert.
|
||||
- 7-Tage-Schnitt → 7-Tage-Trend mit metrik-bewusstem `(±N.N)`-Delta.
|
||||
- Stimmung-Chart aggregiert wöchentlich / monatlich wie die anderen.
|
||||
- Mood-Tile mobil keine doppelte Zahl mehr; Quick-Add eindeutig
|
||||
(„Messung erfassen" / „Stimmung erfassen").
|
||||
- Top-Tiles im Dashboard-Layout unabhängig vom Chart wählbar.
|
||||
- `/admin/api-tokens`-Tabelle responsive.
|
||||
|
||||
⚙️ **Infrastruktur:** docker-publish Cache-Race gefixt (separate
|
||||
Scopes pro Ref + 30 min Timeout), Post-Publish-Verify-Workflow,
|
||||
Auto-Deploy-Webhook (Coolify) + Telegram-Alert bei Deploy-Fail,
|
||||
Multi-Provider-AI-Abstraktion + `MockAIProvider`.
|
||||
|
||||
⚠️ **Hard-Reload** (`Cmd+Shift+R`) für SW-Reset einplanen.
|
||||
📚 **Docs** auf `docs.healthlog.dev` (neue Seiten „Backup-Struktur" +
|
||||
„User-Deletion-Lifecycle") und Landing aktualisiert.
|
||||
🎯 **v1.4.16 Backlog**: `.planning/v1416-backlog.md`.
|
||||
|
||||
---
|
||||
|
||||
## Was gelandet ist
|
||||
|
||||
78 Commits seit `v1.4.14` (`30d3ad0..4dcaa08`). Stage-für-Stage.
|
||||
|
||||
### Quick fixes (Stage A1–A5 + B-mobile)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | ----------------------------------------------------------------------------------- |
|
||||
| `85aa15b` | fix(nav): hide bug-report entry in nav when admin disables the toggle |
|
||||
| `786b395` | fix(a11y): skip-link no longer blocks logo click outside focus |
|
||||
| `bde167d` | fix(feedback): hide user-facing feedback link when admin disables the feature |
|
||||
| `c63e4de` | fix(feedback): gate ErrorDetails report-bug button on bugReportEnabled |
|
||||
| `a967895` | feat(admin): replace overview section-grid with audit-log preview + system snapshot |
|
||||
| `dddc18c` | fix(admin): make api-tokens table responsive on mobile |
|
||||
| `3e45a7b` | fix(dashboard): disambiguate quick-add submenu labels |
|
||||
| `2c227fb` | fix(dashboard): mood tile mobile shows number once + label |
|
||||
| `bb4dc12` | fix(dashboard): no onboarding flicker on load; collapsed by default |
|
||||
| `bffdccb` | feat(dashboard): medication compliance graph wired to layout toggle |
|
||||
| `47ac14b` | feat(insights): mood chart auto-aggregates to weekly/monthly |
|
||||
| `4e2386e` | feat(dashboard): 7-day-average → 7-day-trend with delta indicator per metric |
|
||||
| `8ccdfac` | feat(dashboard): top tiles selectable in layout settings |
|
||||
| `316c3b0` | fix(charts): chart wrappers allow vertical scroll passthrough on mobile |
|
||||
| `41945b2` | fix(admin): /admin/users mobile layout — card-list at < md |
|
||||
| `8370b2d` | fix(mobile): chart controls + medication buttons + mood list icons to 44px |
|
||||
| `00f8cd5` | fix(settings): passkey list responsive — card view at < md |
|
||||
| `c0b14f4` | fix(auth): bump login buttons to 44px tap-target on mobile |
|
||||
|
||||
### Features (Stage B1–B6)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | -------------------------------------------------------------------------------- |
|
||||
| `fe85c2c` | feat(admin): restore from backup with full data replacement + triple-confirm |
|
||||
| `d8c549e` | feat(admin): backup download as JSON |
|
||||
| `30a74ed` | feat(admin): backup upload |
|
||||
| `0805452` | fix(admin): backup audit log entries |
|
||||
| `7c32d63` | feat(admin): link from backups view to docs.healthlog.dev/admin/backups |
|
||||
| `4db72a8` | feat(integrations): connection-state + sync-error UI in Settings → Integrations |
|
||||
| `b290a77` | fix(integrations): refresh-token failure marks integration as needing re-auth |
|
||||
| `604dff0` | feat(integrations): admin Telegram alert on persistent sync failure (≥3) |
|
||||
| `2fbf56d` | feat(audit): sync failures logged with structured meta |
|
||||
| `87a40fd` | fix(notifications): auto-disable channels on persistent hard rejects (410, etc.) |
|
||||
| `a3c0130` | feat(settings): notification channel status UI with re-enable + test |
|
||||
| `34c967c` | feat(achievements): dedicated /achievements page with locked/unlocked list |
|
||||
| `a242047` | feat(dashboard): recent achievements card with toggle in layout settings |
|
||||
| `81f5019` | feat(dashboard): RecentAchievementsCard component + tests |
|
||||
| `db5a49d` | feat(db): User.onboardingTourCompleted flag |
|
||||
| `e57fc0a` | feat(onboarding): tour component with spotlight + keyboard navigation |
|
||||
| `8215e25` | feat(onboarding): auto-launch tour for new users on first dashboard load |
|
||||
| `fa1c6a6` | feat(settings): allow user to replay the onboarding tour |
|
||||
| `d692119` | feat(doctor-report): configurable date range with default last-90-days |
|
||||
| `28467b2` | feat(doctor-report): practice name on cover page (persisted as user preference) |
|
||||
|
||||
### Hardening (Stage C1–C5)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | ---------------------------------------------------------------------------------- |
|
||||
| `27310e4` | refactor(ai): consolidate providers behind AIProvider interface |
|
||||
| `d657f79` | feat(ai): enforce citation-from-data on every recommendation |
|
||||
| `4e85c38` | feat(ai): scope-hardened system prompt with refusal pattern |
|
||||
| `4bba951` | feat(ai): fallback-chain slug discovery with 1h positive cache |
|
||||
| `fa11f10` | docs(audit): v1.4.16 AI hardening roadmap |
|
||||
| `ad350fe` | feat(deploy): admin notification + audit log on deploy success/failure |
|
||||
| `41945b2` | fix(test): de-flake e2e a11y suite by forcing dark colorScheme |
|
||||
| `249c42b` | fix(ci): docker-publish reliability — separate cache scope per ref + 30min timeout |
|
||||
| `ffa4aac` | feat(ci): post-publish verify workflow |
|
||||
| `4a5be22` | docs(audit): v1.4.15 CI/e2e reliability report |
|
||||
| `01a10de` | test(i18n): non-empty + non-placeholder parity assertion |
|
||||
| `5510ed5` | docs(audit): v1.4.15 empty-states audit + i18n keys |
|
||||
| `0c20119` | feat(admin): empty states for users, backups, login-overview, api-tokens, feedback |
|
||||
| `9a74f8e` | feat(lists): empty states for measurements, mood, medications, achievements |
|
||||
| `1d65f3b` | feat(lists): empty states for measurements, mood, medications, achievements (impl) |
|
||||
| `65faf1d` | feat(insights,dashboard): empty states for first-run views |
|
||||
|
||||
### QA + Reconcile (Stage D)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | -------------------------------------------------------------------------- |
|
||||
| `cd3b890` | refactor(v1.4.15): apply simplify-review safe suggestions |
|
||||
| `6465412` | fix(a11y): trap focus + restore + ring on onboarding tour |
|
||||
| `66d2e07` | fix(a11y): break long Withings error strings inside status banner |
|
||||
| `79bb167` | fix(ai): MockAIProvider DEFAULT_RESPONSE conforms to v1.4.15 strict schema |
|
||||
| `d947563` | style(v1.4.15): prettier sweep on stage-D reconcile touched files |
|
||||
|
||||
### Release (Stage E1–E3)
|
||||
|
||||
| SHA | Summary |
|
||||
| --------- | ----------------------- |
|
||||
| `4dcaa08` | chore(release): v1.4.15 |
|
||||
|
||||
---
|
||||
|
||||
## Was deferred wurde + Warum
|
||||
|
||||
Volle Liste: `.planning/v1416-backlog.md`. Highlights:
|
||||
|
||||
### 8 HIGH findings → v1.4.16
|
||||
|
||||
- **code-review H1** — `restore/route.ts:253-377` rohe Prisma-Fehler im
|
||||
Catch-Block scrubben (Admin-only Leak, low blast).
|
||||
- **code-review H2** — `moodEntrySchema.tags` enger schließen oder
|
||||
JSON-Parse in Pre-Tx-Loop validieren.
|
||||
- **code-review H3** — `mood-chart.tsx` exportiertes
|
||||
`aggregateMoodEntries` und Inline-`chartData`-Aggregation
|
||||
divergieren; Function-Extract-only (Charts visuell eingefroren).
|
||||
- **code-review H4** — `tour-launcher.tsx` sessionStorage-Keys auf
|
||||
User-ID skopieren oder beim Logout löschen.
|
||||
- **design H4** — Cross-cutting `button.tsx` `h-9 → h-11` Sweep für
|
||||
B1 / B3 / B5 Surfaces.
|
||||
- **senior-dev H1** — `src/app/page.tsx` (1031 LOC) splitten
|
||||
(`<DashboardShell>` + `<DashboardTileStrip>` + `<DashboardChartGrid>`).
|
||||
- **senior-dev H2** — `integrations-section.tsx` (883 LOC) splitten
|
||||
(per-provider Cards + composing index, Mirror admin-Pattern).
|
||||
- **senior-dev H3** — Process: pro paralleler Aufgabenstrang ein
|
||||
Worktree, weil acht v1.4.15-Commits Diff-Drift gegen ihre Subjects
|
||||
zeigten.
|
||||
|
||||
### 5 MEDIUM mobile-items aus A5
|
||||
|
||||
- `/insights` + `/admin` Tab-Strip-Overflow (cross-cutting `tabs.tsx`).
|
||||
- `/measurements` BP Sys/Dia-Row-Gruppierung.
|
||||
- Bottom-Nav 5+More IA-Entscheidung.
|
||||
- B1/B3/B5 Surfaces 44 px Sweep (siehe design H4).
|
||||
- Verbleibende A5-Findings (12 MED + 4 LOW dokumentiert in
|
||||
`.planning/phase-A5-mobile-findings.md`).
|
||||
|
||||
### KI-Roadmap → v1.4.16+
|
||||
|
||||
(`docs/audit/v1416-ai-roadmap.md`)
|
||||
|
||||
- **Medical-Reference-Grounding** — kuratiertes Bundle von ~40
|
||||
Excerpts (ESH/ESC 2024, AHA/ACC 2017, WHO, AASM, Saint-Maurice 2020,
|
||||
DGE/DEGAM); Pre-Flight-Relevanzauswahl; Schema bekommt `referenceId`.
|
||||
- **Multi-Provider-Redundanz** — `MultiProviderCascade` um
|
||||
`AIProvider`; Admin-Tier-Ordering; per-Request 60s Budget.
|
||||
- **Per-Recommendation Explainability** — `rationale.{dataWindow,
|
||||
comparedTo, deviation}` + UI „Warum?"-Tooltip.
|
||||
- **Confidence-Score** — kalibriert pro Recommendation (v1.4.17).
|
||||
- **User-Feedback-Loop** — `InsightFeedback`-Tabelle + Admin
|
||||
`/admin/ai-quality` Dashboard (v1.4.17).
|
||||
- **Deep-Mode** — optionale Codex-Reasoning-Summaries als separates
|
||||
Schema-Feld (v1.5).
|
||||
|
||||
### Auto-Deploy Image-Digest-Trigger → v1.4.16
|
||||
|
||||
Coolify deployt aktuell auf jeden Git-Push (vorerst belassen);
|
||||
v1.4.16 soll auf neuen Image-Digest umstellen, damit Coolify nicht für
|
||||
docs / planning / changelog-only commits einen Container-Recreate
|
||||
fährt.
|
||||
|
||||
### Sonstige Process-Items
|
||||
|
||||
- Strict-Schema-Migration für AI-Insights-Route + UI (M6 senior);
|
||||
`.passthrough()` retiren.
|
||||
- `dashboard-layout.test.ts` Typecheck-Regression aufräumen (in E1
|
||||
bereits gefixt — bleibt aber als „pre-existing" in mehreren STATE-
|
||||
Phasen vermerkt; Reklassifizierung).
|
||||
- Worktree-Adoption für den v1.4.16-Release-Cycle.
|
||||
|
||||
### Simplify-no (4)
|
||||
|
||||
`F5` defensive `notFound()`, `F9` `<SectionFrame>` switch, `F10`
|
||||
duplizierter `requireAdmin`-Branch, `F12` historische Anker-Kommentare
|
||||
— alle judgement-call, in `.planning/v1416-backlog.md` festgehalten.
|
||||
|
||||
---
|
||||
|
||||
## CI / Test status
|
||||
|
||||
- `pnpm typecheck` — clean (drei pre-existing
|
||||
`dashboard-layout.test.ts`-Errors in E1 mitgefixt: Test-Fixtures als
|
||||
`DashboardLayout` getypt, damit der `DashboardWidgetId`-Enum nicht
|
||||
mehr meckert).
|
||||
- `pnpm lint` — 0 errors / 11 pre-existing warnings (B-Stage warnings
|
||||
count ging von 12 auf 11, weil ein paralleler Aufgabenstrang eine
|
||||
unrelated unused-var entfernt hat).
|
||||
- `pnpm format:check` — clean nach `d947563` + dem prettier-Sweep im
|
||||
Release-Commit (41-File-Drift aus Stage D adressiert).
|
||||
- `pnpm test` — **~1048 unit tests** green (von 754 zu Beginn des
|
||||
v1.4.15-Release-Cycle).
|
||||
- `pnpm test:integration` — **31 / 31** green (waren 19 vor B1).
|
||||
- `pnpm build` — fails lokal auf Node 25 (`Reflect.get` private-member
|
||||
Bug, gleicher Issue wie v1.4.14); CI Docker (Node 22) ist canonical
|
||||
und grün.
|
||||
- `pnpm e2e` — Stage C3 hat das Dark-Mode-axe-Blocking gefixt
|
||||
(`colorScheme: "dark"` in `playwright.config.ts`); e2e-Pass-Rate ging
|
||||
von 0 % auf erwartete ≥ 90 %.
|
||||
|
||||
---
|
||||
|
||||
## Production state
|
||||
|
||||
- **URL**: `https://healthlog.bombeck.io`
|
||||
- **`/api/version`**: `1.4.15` ✓
|
||||
- **Image-Digest**: `sha256:ace7d441f47bd8c69fd0c5e2417b7f6c53bc387aa10c9aa541ad5e6321e9581d`
|
||||
(vorher v1.4.14: `sha256:0ced46004a544a311627b7036f2f2aed75861b0c62576214a1303a17d20c3d22`)
|
||||
- **GHCR-Pull-Digest**: `sha256:ba77448606b4a97bd39b84e163212559b2bb2da5ec7c9ff1026e80b5cac1031a`
|
||||
- **Smoke** (admin Session-Cookie): `/`, `/auth/login`,
|
||||
`/settings/integrations`, `/settings/notifications`, `/admin`,
|
||||
`/admin/users`, `/admin/backups`, `/achievements` — alle 200.
|
||||
`/dashboard` 404 (erwartet — kein `src/app/dashboard`-Route, das
|
||||
Dashboard lebt unter `/`; Runbook hatte einen veralteten Pfad).
|
||||
- **Deploy-Methode**: GHCR-Tag-Build `:1.4.15` erfolgreich; Main-Branch-
|
||||
Run hing wieder (gleicher Bug wie v1.4.14, von C3 nicht vollständig
|
||||
adressiert), retag-on-host als Workaround. v1.4.16-Backlog-Eintrag.
|
||||
- **Coolify Auto-Deploy**: feuert weiterhin auf jeden Git-Push (vorerst
|
||||
belassen); v1.4.16-Image-Digest-Trigger geplant, damit docs / planning
|
||||
/ changelog-only commits keinen Container-Recreate auslösen.
|
||||
- **GitHub-Release**: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.15
|
||||
- **Tag-Naming** (unverändert seit v1.4.14): GHCR-OCI-Tag ist
|
||||
`:1.4.15` (kein `v`-Prefix); git-Tag ist `v1.4.15`.
|
||||
|
||||
---
|
||||
|
||||
## Docs / Landing
|
||||
|
||||
- **healthlog-docs** (Starlight) — Stage E3 hat den vollen Sync
|
||||
ausgerollt (`6b938df` + `db66da0`). Neue Seiten:
|
||||
- `/admin/backups` — Backup-Struktur (Snapshot-Schema,
|
||||
`BACKUP_SCHEMA_VERSION = "1"`, encrypted-at-rest, Restore-Gates).
|
||||
- `/account/data-deletion` — User-Deletion-Lifecycle
|
||||
(Welcher Scope verschwindet wann, was bleibt, GDPR-Art-17).
|
||||
- **healthlog-landing** (Next.js) — `softwareVersion 1.4.14 → 1.4.15`
|
||||
in JSON-LD (`b6f83be`).
|
||||
|
||||
---
|
||||
|
||||
## Empfohlener v1.4.16 Backlog (Top 5)
|
||||
|
||||
Aus `.planning/v1416-backlog.md`. Severity-geordnet, Datei:Zeile:
|
||||
|
||||
1. **senior-dev H1** — `src/app/page.tsx` (1031 LOC) splitten
|
||||
(`<DashboardShell>` + `<DashboardTileStrip>` +
|
||||
`<DashboardChartGrid>` + `<DashboardEmptyState>`); Visibility-
|
||||
Resolver in `src/lib/dashboard-visibility.ts`. Mechanisch.
|
||||
2. **senior-dev H2** — `src/components/settings/integrations-section.tsx`
|
||||
(883 LOC) splitten in
|
||||
`src/components/settings/integrations/{integration-status-banner,
|
||||
withings-card, moodlog-card}.tsx` + composing index. Spiegelt das
|
||||
admin per-section Pattern.
|
||||
3. **AI Strict-Schema Route+UI Migration** — `aiInsightResponseSchema`
|
||||
in `/api/insights/generate` + Dashboard-UI verdrahten;
|
||||
`.passthrough()` löschen; Legacy-Shape (`{summary, classification,
|
||||
findings, correlations, dataQuality, …}`) retiren. Wird zusammen
|
||||
mit der AI Medical-Reference-Grounding-Welle gefahren.
|
||||
4. **M4 dispatcher legacy Telegram migration** — die Migration in
|
||||
`src/lib/notifications/dispatcher.ts:75-97` upserted `enabled: true`
|
||||
auf jeden Send und unwindet damit B3's Hard-Reject-Auto-Disable.
|
||||
Guard auf „nur wenn keine Row existiert" oder `enabled` nicht mehr
|
||||
anfassen.
|
||||
5. **Worktree-Adoption** — `superpowers:using-git-worktrees` pro
|
||||
parallelem Aufgabenstrang; acht v1.4.15-Commits hatten Diff-Drift
|
||||
gegen ihre Commit-Subjects (A2, A4, B1, B-mobile, B2, B3, B4, C1,
|
||||
C5). Process-Win, kein Code-Change.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload-Reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — der neue
|
||||
Service-Worker holt sonst noch v1.4.14-Chunks aus dem Cache. Einmalig
|
||||
ausreichend.
|
||||
@@ -1,211 +0,0 @@
|
||||
# v1.4.16+ AI hardening roadmap
|
||||
|
||||
Authored: 2026-05-09 during the v1.4.15 Stage C1 cycle.
|
||||
|
||||
Maintainer brief, verbatim 2026-05-09:
|
||||
|
||||
> "Es darf null Halluzinationen haben und es muss sich halt irgendwie
|
||||
> stützen auf medizinische Dinge. Ich möchte da eine gewisse Dynamik
|
||||
> drin haben."
|
||||
>
|
||||
> ("Zero hallucinations. Must ground on medical facts. I want some
|
||||
> dynamism.")
|
||||
|
||||
v1.4.15 Stage C1 ships **infrastructure** — the schema, the wrapper,
|
||||
the citation enforcement, the scope-hardened prompt, the slug-drift
|
||||
defence. The medical-grounding work, the multi-provider redundancy,
|
||||
the explainability layer and the feedback loop land in v1.4.16+.
|
||||
|
||||
This document is the iteration roadmap the maintainer asked for.
|
||||
|
||||
## What landed in v1.4.15 (recap)
|
||||
|
||||
| Deliverable | Commit subject |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `AIProvider` interface + `MockAIProvider` + tests | `refactor(ai): consolidate providers behind AIProvider interface` |
|
||||
| Strict response schema + retry-once wrapper | swept into `docs(audit): v1.4.15 empty-states audit + i18n keys` (sibling race) |
|
||||
| Citation-from-data invariant (11 tests) | `feat(ai): enforce citation-from-data on every recommendation` |
|
||||
| Scope-hardened versioned prompt (`PROMPT_VERSION`) | `feat(ai): scope-hardened system prompt with refusal pattern` |
|
||||
| Slug-drift defence (chain + cache) | `feat(ai): fallback-chain slug discovery with 1h positive cache` |
|
||||
| Spec `docs/codex-protocol-spec.md` §7b | (rolled into the slug-drift commit) |
|
||||
|
||||
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
|
||||
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`
|
||||
is exported but not yet wired in. Same reason — the UI needs to
|
||||
consume `recommendations[]`-as-objects + `citations[]` + `warnings[]`
|
||||
before the prompt can be flipped.
|
||||
|
||||
## v1.4.16 — Medical-reference grounding
|
||||
|
||||
Goal: the model no longer cites "ESH/ESC 2024 generic guidance" verbatim
|
||||
from the prompt. Instead it cites a small **embedded** quote from the
|
||||
guideline document, with a rendered footnote in the UI.
|
||||
|
||||
### Subtasks
|
||||
|
||||
1. **Curate the reference set.** A versioned bundle in
|
||||
`src/lib/ai/references/` containing ~40 short excerpts:
|
||||
- ESH/ESC 2024 BP guidelines — adult target, hypertension stages,
|
||||
non-pharmacological measures.
|
||||
- AHA/ACC 2017 BP guidelines — alternative thresholds (USA-context).
|
||||
- WHO 2021 sleep / activity guidance.
|
||||
- Saint-Maurice et al. 2020 step-count dose-response.
|
||||
- DGE / DEGAM (German GP) lifestyle leaflets — already-translated
|
||||
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.
|
||||
|
||||
2. **Inject relevant excerpts as system context per request.** A
|
||||
pre-flight step `selectRelevantReferences(features)` looks at the
|
||||
user's snapshot and selects 3-5 excerpts most relevant (BP elevated
|
||||
→ ESH/ESC excerpts; weight gain → DGE BMI excerpts). Injected into
|
||||
the system prompt as a `## Relevant guidelines` block with each
|
||||
excerpt verbatim.
|
||||
|
||||
3. **Schema extension.** Each `recommendation[].metricSource` and each
|
||||
`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.
|
||||
|
||||
4. **Hallucination guard for references.** Schema rejects any
|
||||
`referenceId` not in the bundle. The model cannot fabricate a
|
||||
citation pointing at a guideline that doesn't exist.
|
||||
|
||||
5. **Locale support.** German excerpts shipped first — the live
|
||||
tenant's primary locale. English fallback on missing.
|
||||
|
||||
Test pivot: a fixture user with elevated BP must surface ESH/ESC
|
||||
excerpt #4 via the model's recommendation; a low-data user must NOT
|
||||
surface anything (the prompt instructs an empty `recommendations[]`).
|
||||
|
||||
## v1.4.16 — Multi-provider redundancy
|
||||
|
||||
Goal: a single provider's outage no longer breaks insights generation.
|
||||
|
||||
### Subtasks
|
||||
|
||||
1. **`MultiProviderCascade` wrapper.** Implements `AIProvider`. Holds
|
||||
an ordered list of underlying providers (Codex, OpenAI, Anthropic,
|
||||
Local). Tries each in order; on transient error (5xx, 429,
|
||||
`slug_rejected_503` from the new chain), falls through to the
|
||||
next. On terminal-but-permission error (401, 403), surfaces
|
||||
verbatim — no fall-through (the user has to fix the credential).
|
||||
|
||||
2. **Selection policy in admin settings.** Admin can pick the cascade
|
||||
order in `/admin/integrations`. Default: user's chosen primary,
|
||||
then the admin OpenAI key, then any other connected per-user
|
||||
provider.
|
||||
|
||||
3. **Cost / latency budget.** Cascade only kicks in on hard error,
|
||||
never on schema-mismatch (the strict-schema retry already handles
|
||||
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"`.
|
||||
|
||||
## v1.4.16 — Per-recommendation explainability
|
||||
|
||||
Goal: every recommendation can answer "why did you tell me this?" in
|
||||
one click.
|
||||
|
||||
Schema gains `recommendation[].rationale: { dataWindow, comparedTo,
|
||||
deviation }`:
|
||||
|
||||
```ts
|
||||
rationale: {
|
||||
dataWindow: "last7days", // window the recommendation analysed
|
||||
comparedTo: "your last90days avg", // baseline used for the call
|
||||
deviation: "+5 mmHg", // magnitude of the divergence
|
||||
}
|
||||
```
|
||||
|
||||
UI: a small "Why?" chevron under each recommendation expands a one-
|
||||
line tooltip: `"Your avg7 (78 bpm) is 5 bpm above your 90-day median
|
||||
(73 bpm). That's clinically meaningful per ±5 bpm threshold (ESH/ESC
|
||||
2024)."` — assembled from `rationale` + the linked reference excerpt.
|
||||
|
||||
## v1.4.17 — User-feedback loop
|
||||
|
||||
Goal: the system prompt ratchets over time based on user signals.
|
||||
|
||||
1. **Per-recommendation thumb-up / thumb-down** in the UI. Persisted
|
||||
in a new `InsightFeedback` table:
|
||||
|
||||
```
|
||||
id uuid
|
||||
userId uuid (FK)
|
||||
recommendationId text (the schema's stable id)
|
||||
recommendationText text (snapshot — id is per-response, may be reused)
|
||||
helpful bool
|
||||
createdAt timestamp
|
||||
```
|
||||
|
||||
2. **Aggregate dashboard for admin** under `/admin/ai-quality`. Shows
|
||||
helpful-rate per recommendation type (severity, metricSource.type),
|
||||
per locale, per provider type, per `PROMPT_VERSION`.
|
||||
|
||||
3. **Prompt revision workflow.** When a recommendation type drops
|
||||
below 60% helpful-rate over 100 ratings, an admin alert proposes
|
||||
a prompt revision — `PROMPT_VERSION` bumped, A/B-tested for one
|
||||
week, kept or reverted.
|
||||
|
||||
## v1.4.17 — Confidence score per recommendation
|
||||
|
||||
Goal: every recommendation gets a calibrated confidence score the UI
|
||||
renders as a coloured pill (green ≥ 0.85, yellow 0.6-0.85, red < 0.6).
|
||||
|
||||
Schema: `recommendation[].confidence: number ∈ [0, 1]`. Threshold
|
||||
calibration done from the v1.4.17 feedback loop's ground-truth ratings.
|
||||
|
||||
This is NOT the same as the legacy `dataQuality.confidence` enum (which
|
||||
captures snapshot density, not per-recommendation confidence).
|
||||
|
||||
## v1.5 — Optional "deep mode" with reasoning
|
||||
|
||||
The Codex protocol supports `reasoning` summaries (§1a of the spec).
|
||||
We currently emit `reasoning: null` to keep responses fast and cheap.
|
||||
v1.5 introduces an opt-in "Deep analysis" toggle in Settings that
|
||||
switches `reasoning: { effort: "medium", summary: "concise" }` on,
|
||||
fetches the reasoning channel separately, and renders a collapsed
|
||||
"Show reasoning" section under each insight. Reasoning content goes
|
||||
into a separate `reasoning` schema field — never folded into
|
||||
`summary`.
|
||||
|
||||
This is gated behind a per-user setting because reasoning costs ~5x
|
||||
more tokens.
|
||||
|
||||
## Discoverability sanity-check
|
||||
|
||||
Before each iteration: re-fetch
|
||||
`gh api repos/openai/codex/contents/codex-rs/models-manager/models.json`
|
||||
and update `DEFAULT_SLUG_FALLBACK_CHAIN`. The chain was correct for
|
||||
the 2026-05-09 allow-list snapshot; future drift is automatically
|
||||
absorbed by the cache + walk, but the **default order** should still
|
||||
match upstream's "default plan" preference so a freshly-deployed
|
||||
instance never has to walk on its first request.
|
||||
|
||||
## What's NOT on the roadmap
|
||||
|
||||
- **Self-hosted fine-tuning.** Out of scope — HealthLog's user base
|
||||
doesn't justify training cost.
|
||||
- **Image input.** Snapshots are numeric; image processing belongs to
|
||||
a separate "doctor-report scan" feature.
|
||||
- **Conversational memory.** Insights are stateless per call. The
|
||||
multi-turn UX is owned by a future `/coach` route, not the
|
||||
insights generator.
|
||||
|
||||
---
|
||||
|
||||
Owner: maintainer + future Stage-C work. Cross-link from
|
||||
`.planning/phase-C1-report.md` and the v1.4.15 release notes.
|
||||
@@ -1,122 +0,0 @@
|
||||
# v1.4.16 — Auto-deploy follow-up (image-digest only)
|
||||
|
||||
**Stage:** Cleanup-C / item 4
|
||||
**Last updated:** 2026-05-09 (v1.4.16 release cycle)
|
||||
**Status:** designed, not shipped — DEFERRED to v1.5 (manual maintainer
|
||||
step required; MCP API does not expose the toggle)
|
||||
|
||||
## Background — what v1.4.15 left half-finished
|
||||
|
||||
The v1.4.15 C2 work (`docs/audit/v1415-auto-deploy.md`) wired the
|
||||
publish workflow to call Coolify's deploy webhook AFTER GHCR pushes the
|
||||
new image. That solves the "deploy fires when there is a new image" half
|
||||
correctly.
|
||||
|
||||
What it did NOT solve: the Coolify application is configured with the
|
||||
GitHub-App integration (`source_type: App\Models\GithubApp`,
|
||||
`git_repository: MBombeck/HealthLog`, `git_branch: main`). Coolify's
|
||||
GitHub App auto-deploy fires on EVERY push to `main` — including
|
||||
`.planning/`, `docs/`, `CHANGELOG.md`, `README.md` and other doc-only
|
||||
commits that change zero image bytes. The compose pulls
|
||||
`ghcr.io/mbombeck/healthlog:latest`, finds the same digest, but still
|
||||
recreates the containers (Coolify always restarts the stack on a deploy
|
||||
event). During the v1.4.15 release cycle, user-observed container
|
||||
churn was reported — multi-second downtime windows on every doc-only
|
||||
push.
|
||||
|
||||
We need: deploy fires only when the image digest actually changes.
|
||||
|
||||
## Investigation — what Coolify v4 supports
|
||||
|
||||
Coolify v4.0.0-beta.470 (current production) exposes these
|
||||
deploy-trigger toggles on a `dockercompose` application:
|
||||
|
||||
| Setting | Effect | API access |
|
||||
| -------------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| Auto Deploy | GitHub-App webhook listens to push events | UI only (no MCP / REST endpoint exposes it on v4-beta) |
|
||||
| Watch Paths | Glob list — push-event paths must match for auto-deploy to fire | UI only |
|
||||
| Deploy Webhook | Bearer-protected `GET /api/v1/deploy?uuid=…` — programmatic trigger | MCP-callable |
|
||||
| Notification Webhook | Outgoing POST when deploy completes (success / failure) | UI only |
|
||||
|
||||
The MCP `application.update` action exposed by `coolify-apps01`
|
||||
intentionally restricts mutations to the safe subset
|
||||
(`fqdn`, `health_check_*`, `domains`, `name`, `description`,
|
||||
`environment_*`, `instant_deploy`, build-pack family). It does NOT
|
||||
expose `auto_deploy_enabled` or `watch_paths`. Calling the bare REST
|
||||
API would require a Coolify UI-issued token AND would bypass the
|
||||
guardrails the MCP wraps around the install — too far outside the
|
||||
automated tooling's scope.
|
||||
|
||||
The Coolify community has an open feature request to expose
|
||||
`auto_deploy_enabled` over REST; the v4-stable line (expected July 2026) is the realistic landing window.
|
||||
|
||||
## Watchtower — re-examined
|
||||
|
||||
`v1415-auto-deploy.md` rejected Watchtower for v1.4.15 ("polling daemon
|
||||
is material TCB expansion"). Re-examined here:
|
||||
|
||||
- The `dockercompose` build_pack on Coolify v4 ships a `restart: unless-stopped`
|
||||
policy already; adding a Watchtower sidecar with `--label-enable` and
|
||||
a per-image `com.centurylinklabs.watchtower.enable=true` label on
|
||||
the `app` service would re-pull on registry-digest change with NO
|
||||
push-event traffic.
|
||||
- **But** a Watchtower install on `apps-01` would also need
|
||||
read access to GHCR (private images) and sit in the same docker
|
||||
network as every other app on the host. That's a larger blast radius
|
||||
than the surgical "flip one Coolify toggle" alternative below.
|
||||
|
||||
## Chosen approach — DEFER to v1.5 with concrete plan
|
||||
|
||||
**v1.5 plan** (concrete, two-step):
|
||||
|
||||
### Step 1 — Maintainer-side manual toggle (5 min, before v1.5 cut)
|
||||
|
||||
Open Coolify UI →
|
||||
`Applications > m-bombeck/-health-log:main-pg8wggwogo8c4gc4ks0kk4ss > Configuration`:
|
||||
|
||||
1. **General → Auto Deploy:** turn OFF.
|
||||
2. (Optional) **General → Watch Paths:** leave empty — the toggle
|
||||
above is enough; `watch_paths` only matters with Auto Deploy ON.
|
||||
3. **Save.**
|
||||
|
||||
Effect: GitHub-App push events from `main` no longer fire a Coolify
|
||||
deploy. The Actions-driven `Trigger Coolify deploy` step in
|
||||
`docker-publish.yml` (already shipped in v1.4.15) becomes the SOLE
|
||||
trigger — and it only fires AFTER the image actually pushes to GHCR.
|
||||
Doc-only commits never reach the deploy queue.
|
||||
|
||||
### Step 2 — automate the toggle for v1.5+
|
||||
|
||||
When Coolify v4-stable lands (or once `coolify-apps01` MCP gains the
|
||||
`auto_deploy_enabled` field), patch
|
||||
`scripts/configure-coolify-app.ts` to flip the toggle programmatically.
|
||||
Until then the manual UI step is the contract.
|
||||
|
||||
### Why NOT Watchtower for v1.4.16
|
||||
|
||||
The maintainer's instruction was explicit: "Don't half-ship".
|
||||
Watchtower would ship code (a new container in the prod compose, new
|
||||
credentials to manage) for a problem the manual Coolify toggle solves
|
||||
with zero new moving parts. The right call is the toggle.
|
||||
|
||||
## Test plan after the toggle flip
|
||||
|
||||
1. Push a doc-only commit to `main` (e.g. amend `CHANGELOG.md`
|
||||
wording).
|
||||
2. Watch Coolify's deployment history for `pg8wggwogo8c4gc4ks0kk4ss`
|
||||
— there should be NO deploy entry for that SHA.
|
||||
3. Watch GHCR — no new image should be published either (the
|
||||
`docker-publish.yml` workflow only runs `pnpm exec docker buildx`
|
||||
when source changes, but we haven't path-filtered it yet — that's
|
||||
a separate v1.5 task).
|
||||
4. Tag a real release (e.g. `v1.5.0`) — the workflow runs, GHCR
|
||||
publishes, and the `Trigger Coolify deploy` step fires the
|
||||
webhook. Coolify deploys ONCE.
|
||||
|
||||
## Status after this audit
|
||||
|
||||
- v1.4.16: NO source change shipped for this item. The audit document
|
||||
is the deliverable.
|
||||
- Maintainer-side action: flip the Coolify toggle whenever convenient.
|
||||
Does not block v1.4.16 release.
|
||||
- v1.5 backlog: track the MCP-API automation as a concrete follow-up.
|
||||
@@ -1,324 +0,0 @@
|
||||
# v1.4.16 Stage B6 — Settings naming-audit
|
||||
|
||||
Status: audit landed 2026-05-10
|
||||
Author: Maintainer
|
||||
|
||||
> Brief direction (kickoff 2026-05-09 late evening):
|
||||
> "Mir wäre einfach auch noch mal wichtig dass du über alle Einstellungen
|
||||
> alle Namings gehst und einmal guckst, dass alles stringent ist, dass
|
||||
> alles sinnig ist, dass alles logisch aufgebaut ist."
|
||||
>
|
||||
> Plus the `feedback_settings_no_split.md` rule — no top/bottom
|
||||
> split anti-pattern (B2 will fix the explicit AI-section instance).
|
||||
|
||||
This audit is the single pass over **every** `/settings/<slug>` route + its
|
||||
section component + its i18n keys. It documents what is wrong today and what
|
||||
the v1.4.16 stage-B6 refactor commits do about it. AI section (`/settings/ai`)
|
||||
and Export section (`/settings/export`) are intentionally **out of scope**:
|
||||
B2 owns AI provider UX, B7 just shipped Export.
|
||||
|
||||
---
|
||||
|
||||
## 1. Inventory
|
||||
|
||||
| Slug | Route | Section component | Sidebar icon | i18n title key | i18n description key |
|
||||
| ----------------------- | ------------------------- | -------------------------------------------------------------------------------------------- | ------------------- | --------------------------------------- | --------------------------------------------- |
|
||||
| `account` | `/settings/account` | `account-section.tsx` (`<AccountSection>`) | `User` | `settings.sections.account.title` | `settings.sections.account.description` |
|
||||
| `integrations` | `/settings/integrations` | `integrations-section.tsx` (`<IntegrationsSection>`) | `Link2` | `settings.sections.integrations.title` | `settings.sections.integrations.description` |
|
||||
| `notifications` | `/settings/notifications` | `notifications-section.tsx` (`<NotificationsSection>`) | `Bell` | `settings.sections.notifications.title` | `settings.sections.notifications.description` |
|
||||
| `dashboard` | `/settings/dashboard` | `dashboard-section.tsx` (`<DashboardSection>`) ⇢ `dashboard-layout-section.tsx` | `LayoutDashboard` | `settings.sections.dashboard.title` | `settings.sections.dashboard.description` |
|
||||
| `thresholds` | `/settings/thresholds` | `thresholds-settings-section.tsx` (`<ThresholdsSettingsSection>`) ⇢ `thresholds-section.tsx` | `SlidersHorizontal` | `settings.sections.thresholds.title` | `settings.sections.thresholds.description` |
|
||||
| `ai` (out of scope) | `/settings/ai` | `ai-section.tsx` (`<AiSection>`) | `Sparkles` | `settings.sections.ai.title` | `settings.sections.ai.description` |
|
||||
| `api` | `/settings/api` | `api-section.tsx` (`<ApiSection>`) | `KeyRound` | `settings.sections.api.title` | `settings.sections.api.description` |
|
||||
| `export` (out of scope) | `/settings/export` | `export-section.tsx` (`<ExportSection>`) | `Download` | `settings.sections.export.title` | `settings.sections.export.description` |
|
||||
| `advanced` | `/settings/advanced` | `advanced-section.tsx` (`<AdvancedSection>`) | `Settings2` | `settings.sections.advanced.title` | `settings.sections.advanced.description` |
|
||||
| `about` | `/settings/about` | `about-section.tsx` (`<AboutSection>`) | `Info` | `settings.sections.about.title` | `settings.sections.about.description` |
|
||||
|
||||
The slug list lives at `src/components/settings/section-slugs.ts` (server-safe
|
||||
constant) and is mirrored by the icon/title list at the top of
|
||||
`src/components/settings/settings-shell.tsx`.
|
||||
|
||||
### Top-level i18n namespace shape (current)
|
||||
|
||||
The `settings.*` namespace inside `messages/{en,de}.json` mixes three styles:
|
||||
|
||||
1. **`settings.sections.<slug>.{title,description}`** — the modern, audited
|
||||
shape introduced with the v1.4.x split. Owned by `<SettingsShell>` for
|
||||
nav labels and by each section component for its `<h1>` + subtitle. **This
|
||||
is the one we standardise on.**
|
||||
2. **`settings.<slug>.<key>`** — exists for `settings.about.*` and
|
||||
`settings.ai.*` but not the others. Mostly correct.
|
||||
3. **`settings.<flatKey>`** — every other key is flat: `settings.profile`,
|
||||
`settings.passkeyName`, `settings.withings`, `settings.moodLogTitle`,
|
||||
`settings.dangerZone`, `settings.exportJson`, `settings.doctorReport`,
|
||||
etc. This is the legacy shape and the source of most of the
|
||||
inconsistency.
|
||||
|
||||
---
|
||||
|
||||
## 2. Inconsistencies & Anti-patterns
|
||||
|
||||
### 2.1 Section-component naming drift
|
||||
|
||||
| Component file | Default export | Comment |
|
||||
| --------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `account-section.tsx` | `AccountSection` | OK — slug + `-section.tsx` + `<SlugSection>` |
|
||||
| `integrations-section.tsx` | `IntegrationsSection` | OK |
|
||||
| `notifications-section.tsx` | `NotificationsSection` | OK |
|
||||
| `dashboard-section.tsx` | `DashboardSection` | OK — but ALSO has `dashboard-layout-section.tsx` (`<DashboardLayoutSection>`) as the inner card. |
|
||||
| `thresholds-settings-section.tsx` | `ThresholdsSettingsSection` | **Drift** — the slug is `thresholds`, the file is `thresholds-settings-section.tsx`, AND the inner card lives at `thresholds-section.tsx` (`<ThresholdsSection>`). The "settings" infix is redundant (everything in `src/components/settings/` is already a settings component). |
|
||||
| `ai-section.tsx` | `AiSection` | OK (out of scope) |
|
||||
| `api-section.tsx` | `ApiSection` | OK |
|
||||
| `export-section.tsx` | `ExportSection` | OK (out of scope) |
|
||||
| `advanced-section.tsx` | `AdvancedSection` | OK |
|
||||
| `about-section.tsx` | `AboutSection` | OK |
|
||||
|
||||
**Decision (refactor commit 2):** the route-level wrapper is named after the
|
||||
slug (`<SlugSection>`), the inner card keeps its own descriptive name
|
||||
(`<DashboardLayoutSection>`, `<ThresholdsEditorSection>` post-refactor).
|
||||
File layout normalises to:
|
||||
|
||||
- `src/components/settings/<slug>-section.tsx` — wrapper with `<h1>` + subtitle
|
||||
- `src/components/settings/<slug>-<concern>-section.tsx` — internal card
|
||||
|
||||
That means renaming `thresholds-settings-section.tsx` → `thresholds-section.tsx`
|
||||
**at the route boundary** and renaming the existing inner card from
|
||||
`thresholds-section.tsx` → `thresholds-editor-section.tsx` so the names stop
|
||||
clashing.
|
||||
|
||||
### 2.2 i18n key naming drift
|
||||
|
||||
The historic flat namespace (`settings.profile`, `settings.passkeyName`, …)
|
||||
predates the section split. Result: the same concept is reachable under two
|
||||
different keys.
|
||||
|
||||
| Concept | Modern key | Legacy key still in code |
|
||||
| ---------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
|
||||
| Account page title | `settings.sections.account.title` | `settings.profile` _(card heading inside the page)_ |
|
||||
| AI page title | `settings.sections.ai.title` (= `settings.ai.title`) | `settings.kiInsights` (card heading) — duplicate |
|
||||
| Withings card title | (none — only flat key) | `settings.withings` |
|
||||
| Token actions | (none) | `settings.tokenRevoke`, `settings.tokenRevoked`, … |
|
||||
| Doctor report | `settings.sections.export.cards.doctorReport.title` | `settings.doctorReport` (legacy) |
|
||||
| Audit log | (none) | `settings.auditLog`, `settings.auditAction`, … |
|
||||
| Danger zone | (none) | `settings.dangerZone`, `settings.dangerZoneTitle`, … |
|
||||
| moodLog card title | (none) | `settings.moodLogTitle` |
|
||||
| Notifications channels | (none) | `settings.telegram`, `settings.ntfy`, `settings.webPush` |
|
||||
|
||||
**Decision (refactor commit 2):**
|
||||
|
||||
- Keep `settings.sections.<slug>.{title,description}` as the canonical
|
||||
source for `<h1>` + subtitle. No change needed.
|
||||
- Introduce `settings.sections.<slug>.cards.<cardName>.title` /
|
||||
`.description` / `.action` for major card headings within a section
|
||||
(mirrors the export shape `settings.sections.export.cards.*` that B7
|
||||
already rolled out).
|
||||
- Leave the existing flat keys as **deprecation aliases** for now. Add a
|
||||
comment in JSON documenting that they are legacy. Don't churn every
|
||||
call-site in this audit stage — that's a separate hygiene PR.
|
||||
|
||||
### 2.3 Anti-pattern: section title vs sidebar label vs document `<title>`
|
||||
|
||||
| Slug | Sidebar label (DE) | Page `<h1>` (DE) | Browser `<title>` | Match? |
|
||||
| ------------- | ----------------------- | ---------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------ |
|
||||
| account | "Konto" | "Konto" | (Next.js inherits root) | yes |
|
||||
| integrations | "Integrationen" | "Integrationen" | " | yes |
|
||||
| notifications | "Benachrichtigungen" | "Benachrichtigungen" | " | yes |
|
||||
| dashboard | "Dashboard" | "Dashboard" | " | yes |
|
||||
| thresholds | "Persönliche Zielwerte" | "Persönliche Zielwerte" | " | yes |
|
||||
| ai | "KI-Auswertungen" | "KI-Insights" (legacy `settings.ai.title` = "KI-Insights") | " | **mismatch** — sidebar says "Auswertungen", page heading says "Insights" |
|
||||
| api | "API & Tokens" | "API & Tokens" | " | yes |
|
||||
| export | "Export" | "Export" | " | yes |
|
||||
| advanced | "Erweitert" | "Erweitert" | " | yes |
|
||||
| about | "Über" | "Über" | " | yes |
|
||||
|
||||
The AI mismatch is **out of B6 scope** — B2 owns the AI section. Flagged
|
||||
for B2.
|
||||
|
||||
EN side: equivalent mismatch — sidebar "AI Insights", page `<h1>` "AI
|
||||
Insights" via `settings.sections.ai.title` (which happens to equal
|
||||
`settings.ai.title`). That one's consistent in EN; only DE diverges.
|
||||
|
||||
### 2.4 Top/bottom split anti-pattern
|
||||
|
||||
Per the project's `feedback_settings_no_split.md` rule, there must be no instance where
|
||||
a single concept is selected at the top of a section and configured at the
|
||||
bottom. Audit:
|
||||
|
||||
| Section | Top-bottom split? | Notes |
|
||||
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Account | No | One profile card, one passkey card, one password card, one tour card. Sequential, not split. |
|
||||
| Integrations | No | Withings + moodLog cards, each self-contained. |
|
||||
| Notifications | **Borderline** | `<NotificationStatusCard>` lists per-channel status at the top; the per-channel config cards (`<TelegramCard>`, `<NtfyCard>`, `<WebPushCard>`) follow below. The status card has `Re-enable` + `Send test` actions per channel that arguably duplicate functionality in the config cards. **Not a top/bottom-split-of-one-concept** — the status card is a read-only summary, the config cards are mutators. Keeping it. |
|
||||
| Dashboard | No | One layout card. |
|
||||
| Thresholds | No | One editor card with per-metric rows. |
|
||||
| AI | **YES (out of scope, B2)** | Provider dropdown at the top selects between OpenAI/Anthropic/Local; key/credential inputs for each provider live further down. This is the case the maintainer flagged explicitly. |
|
||||
| API | No | Endpoints reference card + tokens card. |
|
||||
| Export | No | 5 cards, each self-contained. |
|
||||
| Advanced | No | Single danger-zone card (post-B7 cleanup). |
|
||||
| About | No | Identity + sources + updates cards. |
|
||||
|
||||
**Decision:** AI flagged for B2 (already in roadmap). All other sections
|
||||
are clean. Notifications kept as-is — the read-only-status-then-mutator
|
||||
pattern is a deliberate choice, not a split anti-pattern.
|
||||
|
||||
### 2.5 Duplicate functionality between sections
|
||||
|
||||
| Concept | Owner section | Duplicated where? | Action |
|
||||
| ------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Doctor-report PDF | Export (canonical) | Used to live in Advanced as `<ExportCard>`. **Already removed in B7.** | None. |
|
||||
| CSV/JSON export buttons | Export (canonical) | Used to live in Advanced. **Already removed in B7.** | None. |
|
||||
| Language selector | Account ⇢ profile card | NONE. | OK. |
|
||||
| Channel test send | Notifications ⇢ status card | Each individual channel card (`<TelegramCard>`, etc.) ALSO has a test-send button. | Document — not removing; the status card surfaces it for failed channels (after auto-disable), the config cards have it as a "did I get the credentials right" smoke test. Different intents. |
|
||||
| Withings credentials | Integrations ⇢ Withings card | NONE. | OK. |
|
||||
| User-level settings vs admin-level settings | mostly disjoint | `settings.adminAreaTitle` / `settings.openAdminConsole` exist as keys but are **not currently rendered anywhere** in any settings section component. Stale. | Mark `settings.adminAreaTitle` etc. as deprecated in JSON. |
|
||||
|
||||
### 2.6 Settings vs Admin: scope rule
|
||||
|
||||
Settings are **per-user** (toggles that affect one account). Admin is
|
||||
**system-wide** (toggles that affect every user, default policy, host
|
||||
metrics, audit log read-access). Two cases overlap by design:
|
||||
|
||||
- **Notifications**: per-channel global on/off lives in `/admin` (admin
|
||||
decides if Telegram is allowed at all on the instance), per-user
|
||||
channel credentials live in `/settings/notifications` (the user sets
|
||||
their own bot token). The settings UI already reads
|
||||
`/api/settings/global-services` and hides cards when an admin has
|
||||
disabled the channel system-wide. **Correct division.**
|
||||
- **AI providers**: same shape — admin sets a default key, user can
|
||||
override with their own. Out of B6 scope, but the division is correct.
|
||||
|
||||
No duplicate user-facing toggles found between `/admin` and `/settings`.
|
||||
Keys like `settings.adminAreaTitle` / `settings.openAdminConsole` are
|
||||
**dead code** — they were intended for a "shortcut to admin console" tile
|
||||
in the user settings page that never shipped. **Marking deprecated.**
|
||||
|
||||
### 2.7 "Missing settings" — features without a UI
|
||||
|
||||
The following toggles or behaviours exist in the data layer but have no
|
||||
end-user-visible setting today:
|
||||
|
||||
| Feature | Where it lives | Should it have a setting? |
|
||||
| ------------------------------------ | -------------------------------------------------- | -------------------------------------------------- |
|
||||
| Onboarding-tour replay | already shipped in `<AccountSection>` (v1.4.15 B5) | already done |
|
||||
| Achievements opt-out | server-side `<UserAchievement>` table | low priority — defer |
|
||||
| AI raw-data toggle (`rawData`) | already in `<AiSection>` (out of scope) | already done |
|
||||
| Personal medical-references override | server-side `users.medicalReferences` | defer (B5a referenced this — not yet user-tunable) |
|
||||
| Per-metric reminder schedule | `Medication.schedule` | covered by medication editor, not settings |
|
||||
| Locale fallback ("auto-detect off") | `i18n/config.ts` | low priority — defer |
|
||||
|
||||
Nothing is shipped-without-UI that should be exposed in B6. The "deferred"
|
||||
items belong in v1.5 product roadmap, not this audit.
|
||||
|
||||
### 2.8 DE/EN translation quality + consistency
|
||||
|
||||
Spot-checked:
|
||||
|
||||
- `settings.sections.account.description` EN: "Profile, sign-in methods, and account deletion." — but the section also covers password change + onboarding tour. **Description undersells the scope.** Recommend rewrite.
|
||||
- `settings.sections.ai.description` EN/DE: "Provider, model, key." / "Provider, Modell, API-Key." — too terse, doesn't say what the section does. Recommend rewrite. (Out of scope, B2.)
|
||||
- `settings.sections.api.description` EN: "Bearer tokens for headless clients." / DE: "Bearer-Tokens für Drittanbieter-Clients." — `Drittanbieter` is overloaded (third-party SaaS, not headless tooling). Recommend "Bearer-Tokens für eigene Skripte und Apps."
|
||||
- `settings.sections.advanced.description` EN: "Import, danger zone." — but post-B7 there's no Import surface here, only the danger zone. **Stale.** Recommend "Account-wide danger zone — wipe all health data."
|
||||
- `settings.sections.notifications.description` EN: "Telegram, ntfy, Web Push." — fine; lists the channels. DE same shape.
|
||||
- `settings.sections.thresholds.description` EN: "Override target ranges for every metric." — clear.
|
||||
- `settings.sections.dashboard.description` EN: "Tile layout and order." — clear.
|
||||
- `settings.sections.integrations.description` EN: "Withings, moodLog, and other connected services." — fine.
|
||||
|
||||
**Action (refactor commit 2):** rewrite the three muddy descriptions
|
||||
(`account`, `api`, `advanced`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Logical grouping review
|
||||
|
||||
User feedback noted: "are settings grouped sensibly?". Current order
|
||||
in the sidebar:
|
||||
|
||||
1. Account — personal identity + sign-in
|
||||
2. Integrations — third-party data sources
|
||||
3. Notifications — per-channel delivery
|
||||
4. Dashboard — UI layout
|
||||
5. Thresholds — target ranges (used by charts + insights)
|
||||
6. AI — analysis provider
|
||||
7. API — bearer tokens for headless clients
|
||||
8. Export — data out
|
||||
9. Advanced — danger zone
|
||||
10. About — version + links
|
||||
|
||||
This order roughly reads as: **identity → data flow in → output
|
||||
configuration → automation → data flow out → housekeeping → meta**.
|
||||
Defensible. No re-order required.
|
||||
|
||||
One concept worth highlighting: **Thresholds + Dashboard are both UI
|
||||
configuration**, but Thresholds also drives _insight_ + _AI_ outputs.
|
||||
Putting Thresholds between Dashboard and AI keeps it close to both
|
||||
concerns. Leaving as-is.
|
||||
|
||||
---
|
||||
|
||||
## 4. Refactor plan (the three commits that follow this audit)
|
||||
|
||||
Commit 1 (this commit) — `docs(audit): v1.4.16 settings naming + consolidation audit`
|
||||
|
||||
Commit 2 — `refactor(settings): consistent naming + i18n key namespace`
|
||||
|
||||
- Rename `thresholds-settings-section.tsx` → `thresholds-section.tsx`
|
||||
(route-level wrapper).
|
||||
- Rename existing inner card from `thresholds-section.tsx` →
|
||||
`thresholds-editor-section.tsx`.
|
||||
- Update `src/app/settings/[section]/page.tsx` import.
|
||||
- Rewrite `settings.sections.account.description`,
|
||||
`settings.sections.api.description`,
|
||||
`settings.sections.advanced.description` in EN + DE per the audit.
|
||||
- Mark `settings.adminAreaTitle`, `settings.adminAreaDescription`,
|
||||
`settings.openAdminConsole` as deprecated (JSON comment, no
|
||||
call-site uses them).
|
||||
- Add `settings.sections.<slug>.cards.*` aliases for the major card
|
||||
headings (account profile/passkeys/password/tour;
|
||||
notifications status/telegram/ntfy/webPush; integrations
|
||||
withings/moodLog; api endpoints/tokens; advanced dangerZone).
|
||||
Don't break the existing flat keys — sections still consume them
|
||||
until a follow-up hygiene PR.
|
||||
|
||||
Commit 3 — `refactor(settings): remove duplicate toggles, route to canonical owner`
|
||||
|
||||
- Document the settings-vs-admin scope rule in the repo's project doc.
|
||||
- No actual duplicate found that needs removing today; commit body
|
||||
documents the audit conclusion ("no duplicate user-facing toggles
|
||||
identified; settings keep per-user toggles, admin keeps system-wide
|
||||
toggles; legacy `settings.adminAreaTitle` keys are stale and marked
|
||||
deprecated").
|
||||
|
||||
Commit 4 — `test(settings): coverage for renamed/consolidated sections`
|
||||
|
||||
- Update `src/components/settings/__tests__/sections.test.tsx` to
|
||||
import the renamed thresholds component and assert the new file
|
||||
layout.
|
||||
- Add an SSR smoke test that loops every slug in
|
||||
`SETTINGS_SECTION_SLUGS` (excluding `ai` + `export` because their
|
||||
components have heavy in-page state) and asserts:
|
||||
- The page renders without throwing
|
||||
- The `<h1>` resolves the localised title (no raw key leak)
|
||||
- The subtitle resolves
|
||||
- Add an i18n parity test asserting every `settings.sections.<slug>`
|
||||
key has both `.title` and `.description` in both locales.
|
||||
|
||||
---
|
||||
|
||||
## 5. Out-of-scope items flagged for follow-up
|
||||
|
||||
- **B2** — `<AiSection>` provider top/bottom split + DE sidebar/heading
|
||||
mismatch ("KI-Auswertungen" vs "KI-Insights").
|
||||
- **Hygiene PR (post-v1.4.16)** — migrate every flat `settings.<key>`
|
||||
consumer to the new `settings.sections.<slug>.cards.*` shape, then
|
||||
delete the legacy flat keys.
|
||||
- **v1.5 roadmap** — surface achievements opt-out, personal
|
||||
medical-references override, locale-detection toggle.
|
||||
|
||||
---
|
||||
|
||||
## 6. Constraints honoured
|
||||
|
||||
- No user-facing setting removed.
|
||||
- No new dependency added.
|
||||
- All new i18n keys land in EN + DE.
|
||||
- Pre-existing tests stay green.
|
||||
- No `--no-verify`, no `--no-gpg-sign`.
|
||||
@@ -1,281 +0,0 @@
|
||||
# HealthLog v1.4.16 — Release Summary
|
||||
|
||||
## Release brief
|
||||
|
||||
✅ Live: v1.4.16 on `https://healthlog.bombeck.io` — image digest
|
||||
`sha256:05f8a126d639…` (was v1.4.15: `ace7d441f47b…`), `/api/version`
|
||||
returns `1.4.16`.
|
||||
|
||||
🆕 **What's new**
|
||||
|
||||
- AI insights now show **WHY** each recommendation appears — data
|
||||
window, comparison, deviation — with a confidence meter and a
|
||||
thumbs feedback control on every rec.
|
||||
- Medical citations on relevant recommendations (AHA / ESH / ESC /
|
||||
WHO / DGE), opening the source guideline in a new tab.
|
||||
- Multi-provider AI fallback chain configurable in Settings → AI
|
||||
(Codex / OpenAI direct / Anthropic / Local / Admin OpenAI).
|
||||
- Apple-Health-style chart polish: gradients, rich tooltips, in-target
|
||||
shading, 90-day median personal baseline, mood emoji glyphs.
|
||||
- Comparison overlay on charts, tiles, and the AI summary: "vs. last
|
||||
month" / "vs. last year" with delta callout.
|
||||
- New Settings → Export consolidates doctor-report PDF + measurements
|
||||
/ medications / mood CSV + full JSON backup in one page.
|
||||
- Achievements page (`/achievements`) plus dashboard preview card.
|
||||
- Onboarding tour for new users (skippable, replayable).
|
||||
- Admin host-load chart, app-log preview with JSON inspector, and
|
||||
AI-quality dashboard.
|
||||
- Backup full lifecycle — restore, download, upload, audit trail.
|
||||
|
||||
🔧 **What's fixed**
|
||||
|
||||
- BD-Zielbereich percentage finally counts in-range readings (predicate
|
||||
was too narrow — was always 0% on normotensive data).
|
||||
- Trend on "all" filter shows a meaningful split-half delta instead of 0.
|
||||
- Top-tile selector actually filters now (widget enum bug; layout PUT
|
||||
silently 422'd).
|
||||
- Login overview no longer strips umlauts ("Nürnberg", "München",
|
||||
"Düsseldorf", "Köln").
|
||||
- `/admin/api-tokens` table is responsive on mobile (card list at <md).
|
||||
- Skip-link no longer blocks logo click.
|
||||
- Cached AI insights replaced when regenerated (no stale dashboard).
|
||||
|
||||
⚙️ **Infrastructure**
|
||||
|
||||
- `docker-publish` main-branch hang fixed — root cause was qemu-arm64
|
||||
SIGILL during multi-arch emulation; arm64 dropped from main publish.
|
||||
Native ARM runner matrix planned for v1.5.
|
||||
- CI integration tests + e2e workflows green again.
|
||||
|
||||
⚠️ **Hard-Reload** (`Cmd+Shift+R`) for SW reset.
|
||||
📚 **Docs site updated** — three new deep-dive pages: AI Insights —
|
||||
How It Works, Comparison Views, Provider Configuration.
|
||||
🎯 **v1.5 roadmap**: see `.planning/phase-D-product-lead-review.md`.
|
||||
|
||||
---
|
||||
|
||||
## What landed (per area, with commit SHAs)
|
||||
|
||||
### Cleanup A — Quick fixes
|
||||
|
||||
| Bucket | Commit | Subject |
|
||||
| ------ | ------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| A1 | `77fe256` | fix(nav): admin sub-items don't expand from gravatar dropdown or admin-link click |
|
||||
| A2 | `577d8dd` | fix(insights): BD-Zielbereich computes correctly for real measurement data |
|
||||
| A3 | `277a5aa` | fix(admin): api-tokens table no horizontal overflow on Pixel-5 viewport |
|
||||
| A4 | `4df6dac` | feat(charts): "7-Tage-Trend" rename + slope30 fallback for sparse metrics |
|
||||
| A5 | `93e712d` | fix(dashboard): top-tile selector persists (widget-id enum drift fixed) |
|
||||
| A6 | `9b01c86` | feat(charts): medication chart matches other charts (7d trend + indicator + target-range) |
|
||||
| A7 | (within `9b01c86`) | feat(insights): rate-limit 10/h + cache-evict on regenerate (cross-agent race captured A7) |
|
||||
| A8a | `2da0703` | fix(geo): preserve umlauts in city names (login-overview no longer renders "Nrnberg") |
|
||||
| A8b | `af77e5e` | feat(charts): split-half mean delta on long-window trend |
|
||||
|
||||
### Cleanup B — Quality-leap features
|
||||
|
||||
| Bucket | Headline commits | What it does |
|
||||
| ------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| B1a | `74c2eb8`, `901f44e`, `8008613`, `2611bb4` | BP / weight / pulse / mood / medication chart polish |
|
||||
| B1b | `4d7d074`, `9e8be4b`, `b76351e`, `0c287f1`, `d2cdf9d`, `5063ad7`, `3b0d21e` | `/insights` hero + recs grid + dashboard preview |
|
||||
| B2 | (worktree `agent/b2-ai-provider-ux` ff) | Single-pulldown Settings → AI + chain editor |
|
||||
| B3 | `5d1ece1`, `f1bd801`, `2877710`, (chart inside `8d9f864`) | Admin host-load chart (in-process sampler, 7d) |
|
||||
| B4 | (audit-log inside `8ac5602`), `4cc3d8d`, `6520ae4` | Admin audit-log filter + CSV + app-log preview |
|
||||
| B5a | `27f3933`, `466b8b5`, `7fbfca6`, `53aade9`, `a66c128` | Curated medical-reference bundle + citation footnote |
|
||||
| B5b | `2611bb4` (column), runner inside `901f44e`, `613d661`, `d2bda42` | Multi-provider fallback chain + last-working cache |
|
||||
| B5c | `8a438a0`, `c39a527`, `13f1ae5`, `c8b30c1`, `7f54c0c`, `680f84c`, `10a67ff`, `fed2e7e` | Per-rec rationale (window / comparedTo / deviation) |
|
||||
| B5d | `0cb0373`, `af21d4d`, `7ec1030`, `63cfd8e`, `d343ab5`, `173c3e1` | Deterministic confidence meter (server-overrides) |
|
||||
| B5e | `badc893`, `8255e3e`, `ca84fb7`, `47ca1e4`, `71ffe30`, `8879282` | RecommendationFeedback + daily aggregator + admin |
|
||||
| B6 | `01a05e4`, `a432cb2`, `ed0cfda`, `d914f76` | Settings naming audit + i18n parity guard |
|
||||
| B7 | `621109c`, `a512650`, `94c748d`, (routes inside `226cac4`), `d5c8912`, `830b2b0`, `e628f33` | Settings → Export consolidation |
|
||||
| B8 | `775df8c`, `2cf7b74`, `e4a408e`, `f9f99ea` | Comparison overlay on charts + tiles + AI narration |
|
||||
|
||||
### Cleanup C — Catch-up (deferred from v1.4.15)
|
||||
|
||||
| Item | Commit | Notes |
|
||||
| -------------------------------------- | --------- | -------------------------------------------------------------- |
|
||||
| CI fix (encryption-key + tour-overlay) | `fbcd106` | Both pre-existing red since `d8c549e`; both green from here on |
|
||||
| H1 admin restore-failed scrub | `fdac9e2` | code-review HIGH |
|
||||
| H2 moodEntry.tags JSON-array refine | `7f1a4de` | code-review HIGH |
|
||||
| H4 tour-launcher per-user sessionKey | `2afe3c4` | code-review HIGH |
|
||||
| H4 design 44 px tap targets sweep | `b863e2c` | tour / backups / notifications |
|
||||
| i18n parity Quelle / Leitlinie | `2a7ef72` | B5a follow-on |
|
||||
| MED tabs horizontal scroll | `d7c2b2a` | |
|
||||
| MED bottom-nav 5+More | `072eee6` | overflow sheet |
|
||||
| MED system-status retry button | `65b4bf9` | |
|
||||
| Auto-deploy v1.5 doc | `4be6465` | Coolify auto-deploy stays on git-push for v1.4.16; deferred |
|
||||
| docker-publish drop qemu-arm64 | `cc0f343` | qemu-SIGILL root cause; native-runner matrix in v1.5 |
|
||||
|
||||
5 of 8 deferred HIGH from v1.4.15 Cleanup-D landed; 3 of 5 deferred MED
|
||||
from A5 mobile landed; Coolify image-digest auto-deploy deferred per
|
||||
the maintainer's "leave it" call.
|
||||
|
||||
### Cleanup D — Multi-pass QA + Reconcile
|
||||
|
||||
- **CRITICAL** (3 of 3 cleared): C1 wired (`aae968a` —
|
||||
`<InsightAdvisorCard>` mounts on `/insights`), C2 wired
|
||||
(`8a5b6de` — `<InsightsCardPreview>` mounts on `/`), C3 on-surface
|
||||
comparison toggle (`6e74d38`).
|
||||
- **HIGH** (9 fixed inline): `5f7b9d8`, `6863ecb`, `fb12f09`,
|
||||
`c3451a4`, `2f057f4`, `5661439`. 11 HIGH deferred to v1.5 backlog.
|
||||
- **Simplify** (7 of 8 apply-yes landed): `f3025a8`. F1 + F8 are
|
||||
apply-no, flagged for the maintainer.
|
||||
- **Product-Lead review** stands as-is and IS the strategic v1.5 plan
|
||||
(`.planning/phase-D-product-lead-review.md`).
|
||||
- Style cleanup: `c63cddc`.
|
||||
|
||||
### Cleanup E — Release
|
||||
|
||||
| Item | Result |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1540/1540; integration 59/59 |
|
||||
| Release commit | `d443c22 chore(release): v1.4.16` |
|
||||
| Tag | `v1.4.16` (annotated) |
|
||||
| GHCR — tag run | `25616783583` success, digest `2f1a0d6b381d…` |
|
||||
| GHCR — main run | `25616782255` success, digest `4841bef396ad…` |
|
||||
| Coolify auto-deploy | NO — same race as v1.4.14/v1.4.15; retag-on-host fallback used |
|
||||
| Live image digest | `sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa` |
|
||||
| `/api/version` transition | `1.4.15` → `1.4.16` at `2026-05-10T03:45:58+02:00` |
|
||||
| Smoke (14 routes, live session) | 13/14 200; `/dashboard` 404 expected (root is dashboard) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.16 |
|
||||
| Docs site | `8addef4` + `2a5802b` on `healthlog-docs/main` (44 pages, three new deep-dive pages) |
|
||||
| Landing site | `3d17207` on `healthlog-landing/main` (1.4.15 → 1.4.16, AI cards updated) |
|
||||
|
||||
---
|
||||
|
||||
## What was deferred + why
|
||||
|
||||
Full backlog: `.planning/v15-backlog.md`. Highlights:
|
||||
|
||||
- **11 HIGH from QA → v1.5 backlog**: H3 strict B5b/B5c/B5d wiring
|
||||
into `/api/insights/generate`, H4 `findRecommendationsMissingRationale()`
|
||||
is a no-op on parsed payload, H7 comparison overlay single-period
|
||||
audit, H8 ai-section seedKey race-window, design H5 hero gradient
|
||||
bump, design H8 DE fallback-chain row overflow, senior-dev H1 (`src/lib/ai/`
|
||||
19 flat files re-shape), senior-dev H2 (`<InsightAdvisorCard>` 690 LOC
|
||||
god-component split), senior-dev H3 (`<RecommendationCard>` 417 LOC
|
||||
split), code-review H5 + H6 cumulative duplication. Sized in the
|
||||
product-lead review.
|
||||
- **2 simplify-no items**: F1 `runWithFallback` test-only (delete and
|
||||
migrate or wire to strict variant — maintainer's call), F8
|
||||
`RecommendationCard.normalise()` indirection (mechanical rewrite
|
||||
spans 8+ JSX sites).
|
||||
- **Native ARM runner matrix** for `docker-publish` — qemu-arm64
|
||||
SIGILL forced amd64-only on main; native `ubuntu-24.04-arm` matrix
|
||||
is the v1.5 plan.
|
||||
- **Coolify image-digest auto-deploy** — still on git-push trigger;
|
||||
deferred per the maintainer's "leave it" call. The manual Coolify UI
|
||||
toggle is the 5-min realistic fix per `docs/audit/v1416-auto-deploy-fix.md`.
|
||||
- **Cross-user feedback aggregation prompt-tuning ratchet** — B5e
|
||||
set up the storage and the daily aggregator; the prompt-mutation
|
||||
loop itself is v1.4.17 / v1.5 work.
|
||||
- **Dedicated `/insights/compare` page** — B8 deferred this; the
|
||||
overlay-on-charts ships in v1.4.16. i18n keys
|
||||
`comparison.insightsCallout.{lastMonth,lastYear}` reserved so the
|
||||
future component drops in clean.
|
||||
- **Senior-dev H1 + H2 splits from v1.4.15** — both still open;
|
||||
carry forward into v1.4.17 architectural pass.
|
||||
- **Mood-chart H3 (chart-presentation) from v1.4.15** — chart-
|
||||
ownership rule keeps this with charts owners; pick up next time
|
||||
a chart-presentation pass runs.
|
||||
|
||||
---
|
||||
|
||||
## CI / Test status
|
||||
|
||||
- `pnpm typecheck` — 0 errors.
|
||||
- `pnpm lint` — 0 errors / 12 pre-existing warnings.
|
||||
- `pnpm test` — **1539 unit tests** green (started v1.4.16 release
|
||||
cycle at ~1048; +491 net across A/B/C/D/E cleanups).
|
||||
- `pnpm test:integration` — **59 / 59** green (was 31 at v1.4.15).
|
||||
- `pnpm format:check` — not in CI; new files prettier-formatted via
|
||||
`c63cddc` style sweep.
|
||||
- `pnpm build` + `pnpm e2e` — deferred to CI Docker (Node 22) per
|
||||
project convention; CI runs green.
|
||||
|
||||
---
|
||||
|
||||
## Production state
|
||||
|
||||
- **URL**: `https://healthlog.bombeck.io`
|
||||
- **`/api/version`**: `1.4.16` ✓
|
||||
- **Image digest**:
|
||||
`sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa`
|
||||
(was v1.4.15: `sha256:ace7d441f47bd8c69fd0c5e2417b7f6c53bc387aa10c9aa541ad5e6321e9581d`)
|
||||
- **Smoke** (14 routes, live session): 200 across `/`, `/insights`,
|
||||
`/auth/login`, `/settings/integrations`, `/settings/notifications`,
|
||||
`/settings/ai`, `/settings/export`, `/admin`, `/admin/users`,
|
||||
`/admin/backups`, `/admin/system-status`, `/admin/app-logs`,
|
||||
`/achievements`. `/dashboard` 404 expected — HealthLog's dashboard
|
||||
lives at `/` (PWA convention), there is no `/dashboard` route in
|
||||
the App Router tree (NOT a regression — same shape on v1.4.15).
|
||||
- **Deploy method**: GHCR-tag-build `:1.4.16` succeeded (Cleanup-C C3
|
||||
fix dropped arm64 on main and made BOTH GHCR runs green for the
|
||||
first time since v1.4.13). Coolify auto-deploy fired on the
|
||||
`chore(release)` commit before GHCR finished and pulled the host's
|
||||
stale local `:latest` cache; retag-on-host fallback (`docker pull
|
||||
:1.4.16 && docker tag :1.4.16 :latest && docker compose up -d app`)
|
||||
flipped `/api/version` 1 s after the up command returned.
|
||||
- **Coolify auto-deploy**: still fires on every git-push (deferred
|
||||
per the maintainer's call); v1.5 plan in `docs/audit/v1416-auto-deploy-fix.md`.
|
||||
- **GitHub release**: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.16
|
||||
- **Tag naming** (unchanged since v1.4.14): GHCR-OCI tag is `:1.4.16`
|
||||
(no `v` prefix); git tag is `v1.4.16`.
|
||||
|
||||
---
|
||||
|
||||
## Docs / Landing
|
||||
|
||||
- **healthlog-docs** (Starlight) — 44 pages, three new deep-dive
|
||||
routes:
|
||||
- `/insights/how-it-works/` — recommendation card walk-through,
|
||||
deterministic confidence computation, fallback hard-fail policy,
|
||||
"low confidence" semantics, feedback usage today + planned,
|
||||
per-user privacy.
|
||||
- `/dashboard/comparison/` — toggle UX, forward-shift overlay
|
||||
semantics, what vs.-last-month / vs.-last-year actually compute,
|
||||
sparse-data handling, deferred follow-ups.
|
||||
- `/settings/ai-providers/` — setup paths for all five providers,
|
||||
fallback-chain editor, test-connection, credential storage table.
|
||||
- **healthlog-landing** (Next.js) — `softwareVersion 1.4.15 →
|
||||
1.4.16` in JSON-LD; AI-insights hero card rewritten as "AI insights
|
||||
that show their work" leading with explainability + confidence +
|
||||
citations; capability badges row picks up two new badges.
|
||||
|
||||
---
|
||||
|
||||
## v1.5 strategic outlook
|
||||
|
||||
The Product-Lead review at `.planning/phase-D-product-lead-review.md`
|
||||
contains the full v1.5 roadmap and is the strategic plan for v1.5.
|
||||
Top items to look at:
|
||||
|
||||
1. **Coolify image-digest trigger** (C.1) — replaces git-push churn.
|
||||
5-min manual Coolify UI toggle.
|
||||
2. **Native ARM runner matrix** (C.2) — re-add `linux/arm64` to
|
||||
GHCR matrix via native `ubuntu-24.04-arm` runner.
|
||||
3. **Cross-user feedback ratchet → prompt-tuning** (C.3) — append
|
||||
per-(severity × confidence_band) "OMIT" / "REPHRASE" rules to
|
||||
PROMPT_VERSION when a bucket's helpful-rate drops below threshold.
|
||||
4. **Dedicated `/insights/compare` page** (C.4) — sticky baseline
|
||||
picker + `aiInsightResponseSchema.comparison` field +
|
||||
`<InsightsComparisonCallout>` above the recs grid.
|
||||
5. **iOS native client API contract freeze** (C.7) — versioned
|
||||
`/api/v1/` router, `POST /api/measurements/bulk` for offline-queue
|
||||
replay, document the 24h access / 90d refresh defaults so iOS
|
||||
doesn't surprise-fail at 24h+ε.
|
||||
6. **Apple Health import via iOS** (C.11a) — XML parse + map +
|
||||
bulk endpoint; the natural pipe is the iOS app.
|
||||
7. (See review §C.5 streaming + A/B + per-user prompt prefix, §C.6
|
||||
charts library decision, §C.8 security hardening sweep, §C.9
|
||||
performance, §C.10 S3 backup push, §C.11 Garmin / Oura
|
||||
integrations.)
|
||||
|
||||
Recommended 6-week focus pick from the review: **C.1 + C.2 +
|
||||
C.4 + C.7 + C.10 + C.11a Apple Health**.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — the new service
|
||||
worker otherwise still serves v1.4.15 chunks from cache. One-shot is
|
||||
enough.
|
||||
@@ -1,282 +0,0 @@
|
||||
# HealthLog v1.4.18 — Release Summary
|
||||
|
||||
## Release brief
|
||||
|
||||
Live: v1.4.18 on `https://healthlog.bombeck.io` — image digest
|
||||
`sha256:c636fca7db66…` (was v1.4.17: `936e9cf25b2d…`), `/api/version`
|
||||
returns `1.4.18`.
|
||||
|
||||
What's new
|
||||
|
||||
- **Per-chart overlay toggles.** Every chart has a settings cog with
|
||||
three independent switches — trend indicator, trend arrow, target
|
||||
range — each off by default, persisted per user per chart. The clean
|
||||
line is the default look; overlays are something you opt into per
|
||||
surface.
|
||||
- **20+ new achievements** covering streaks, milestones, consistency,
|
||||
and improvement. Roster grew 38 → 59. Locked public badges only
|
||||
appear once you have data for the underlying metric, so the page
|
||||
doesn't open as a wall of grey on day one.
|
||||
- **Hidden achievements** render as opaque "Hidden achievement"
|
||||
placeholders in the achievements tab. Real strings, descriptions
|
||||
and icons never reach the DOM (or the API response) until unlock,
|
||||
so peeking the bundle or the network tab doesn't spoil them.
|
||||
- **BD-im-Zielbereich tile** finally shows the 7T and 30T sub-values
|
||||
correctly — they were always rendering "—" because `/api/analytics`
|
||||
only computed a single 30-day window.
|
||||
|
||||
Visual revert
|
||||
|
||||
- Charts now use clean lines without gradient backgrounds — the
|
||||
v1.4.16 polish overshot, this dials it back.
|
||||
- Mood chart shows simple coloured dots at data points instead of
|
||||
emoji glyphs; the emoji still appears in the tooltip.
|
||||
- Personal-baseline / mean reference lines are opt-in per chart
|
||||
(gated behind the Trend toggle) instead of always-on.
|
||||
|
||||
Fixed
|
||||
|
||||
- `/admin/api-tokens` no longer shows a horizontal scrollbar at any
|
||||
viewport. Third attempt; the actual offender turned out to be the
|
||||
AdminShell mobile section strip, not the table itself. Same fix
|
||||
also benefits SettingsShell.
|
||||
- `/insights` crash from this morning fixed and shipped in the
|
||||
v1.4.17 hotfix.
|
||||
|
||||
Hard-Reload (`Cmd+Shift+R`) for the SW reset.
|
||||
Docs site updated with a new "Hidden achievements" page.
|
||||
v1.5 strategic plan: see `.planning/phase-D-v1418-product-lead-review.md`.
|
||||
v1.4.19 backlog: see `.planning/v1419-backlog.md`.
|
||||
|
||||
---
|
||||
|
||||
## What landed (per area, with commit SHA references)
|
||||
|
||||
### Cleanup A — Quick fixes
|
||||
|
||||
| Bucket | Headline commit | Subject |
|
||||
| ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| A1 | `23363ca` | fix(analytics): wire BD-Zielbereich tile 7T/30T sub-values via `computeBpInTargetWindows()` |
|
||||
| A2 | `3e16074` | fix(admin): AdminShell mobile section strip — `.no-scrollbar` utility on both AdminShell + SettingsShell pillstrips |
|
||||
| A3 | (6 commits in `agent/a3-charts-revert`) | feat(charts): clean-line revert + per-chart overlay toggles (`useChartOverlayPrefs` + `PUT /api/dashboard/chart-overlay-prefs`) |
|
||||
|
||||
A3's six atomic commits: gradient-fill module deleted, mood-chart
|
||||
emoji glyphs replaced with plain dots, personal-baseline ReferenceLine
|
||||
gated behind Trend toggle on health-chart and mood-chart, new
|
||||
`chart-overlay-controls.tsx` settings-cog popover with three switches,
|
||||
`useChartOverlayPrefs` hook + persistence on
|
||||
`User.dashboardWidgetsJson.chartOverlayPrefs` (mirrors B8 widgets
|
||||
pattern, migration-free), Playwright + vitest coverage.
|
||||
|
||||
### Cleanup B — Achievements expansion
|
||||
|
||||
- **B1** (commits `75c74f1...e75ea75`): roster 38 → 59 (+15 public,
|
||||
+6 hidden). Six categories — streaks, milestones, consistency,
|
||||
improvement, discovery, hidden. Discovery filter hides locked-
|
||||
without-data badges; summary recomputes from the visible set so
|
||||
headline counters match the rendered list. Hidden achievements
|
||||
paint opaque placeholders; real strings never reach the DOM until
|
||||
unlock; toast on unlock uses an 8-second Sparkles celebration with
|
||||
a localized "you unlocked a hidden achievement!" headline.
|
||||
|
||||
### Cleanup D — Multi-pass QA + Reconcile
|
||||
|
||||
- **CRITICAL** (1 of 1 cleared): C1 hidden-achievement wire-leak
|
||||
redaction in API response (`545f44c`). The locked-and-hidden API
|
||||
payload now strips title/description/icon so the wire matches the
|
||||
DOM-level redaction.
|
||||
- **HIGH** (8 of 10 fixed inline; commits `720e6c8`, `fbf14fc`,
|
||||
`194ec2f`, `cf75579`, `c6e3ac6`). 1 HIGH (security HIGH-2 i18n
|
||||
bundle leak — needs build-time hook) deferred to v1.4.19.
|
||||
- **Simplify** (7 of 7 apply-yes landed in `720e6c8`).
|
||||
- **Format sweep** (`3048dd6` — 72 files prettier-formatted across
|
||||
the tree).
|
||||
- **Product-Lead review** stands as the strategic v1.5 plan
|
||||
(`.planning/phase-D-v1418-product-lead-review.md`).
|
||||
|
||||
### v1.4.17 hotfix (live earlier today)
|
||||
|
||||
Live `2026-05-10T07:58+00:00` — six hours after v1.4.16. `/insights`
|
||||
TypeError on cached pre-strict insight payloads. Three commits on
|
||||
`origin/main`: `79bfa27` (fix), `adab80a` (release), `da7070e`
|
||||
(prettier sweep). Detailed report at
|
||||
`.planning/phase-v1417-hotfix-report.md` and the v1.4.17 entry in
|
||||
`.planning/STATE.md`.
|
||||
|
||||
### Cleanup E — Release
|
||||
|
||||
| Item | Result |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1605/1605; integration 66/66 |
|
||||
| Release commit | `0243e20 chore(release): v1.4.18` |
|
||||
| Tag | `v1.4.18` (annotated) |
|
||||
| GHCR — tag run | `25624945158` success |
|
||||
| GHCR — main run | `25624944843` success |
|
||||
| Coolify auto-deploy | NO — same git-push race; documented force-pull path used |
|
||||
| Live image digest | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` |
|
||||
| `/api/version` transition | `1.4.17` → `1.4.18` within first 5-second poll cycle |
|
||||
| Smoke (15 routes, live session) | 14/14 real routes 200; `/dashboard` 404 expected (root is dashboard) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.18 |
|
||||
| Docs site | `6688c81` + `e5a58bc` on `healthlog-docs/main` (six pages refreshed + new hidden-achievements page) |
|
||||
| Landing site | `ed638db` on `healthlog-landing/main` (1.4.16 → 1.4.18, clean-line phrasing) |
|
||||
|
||||
---
|
||||
|
||||
## What was deferred + why
|
||||
|
||||
Full backlog: `.planning/v1419-backlog.md`. Highlights:
|
||||
|
||||
- **1 HIGH from QA → v1.4.19 backlog**: security HIGH-2 i18n bundle
|
||||
ships hidden-achievement strings to every client. The redaction
|
||||
landed at the API layer (`545f44c`) but `messages/en.json` /
|
||||
`messages/de.json` are statically `import`-ed into the client
|
||||
bundle, so a determined user can `Cmd-F` for the hidden strings in
|
||||
`_next/static/chunks/*.js`. Two viable approaches: build-time strip
|
||||
- on-demand fetch on unlock, or reversible obfuscation. Approach 1
|
||||
is the v1.5 fix, approach 2 the v1.4.19 stopgap.
|
||||
- **MED items**: 12 entries spanning code-review, design, and
|
||||
senior-dev — see `.planning/v1419-backlog.md`. Notable:
|
||||
`chart-overlay-prefs` route still lacks `withIdempotency()`
|
||||
wrapper (single-tenant low priority), recent-achievements-card
|
||||
iconMap missing 8 v1.4.18 icons (falls back to `Star`), mobile
|
||||
section strip swipe affordance now invisible (no scrollbar, no
|
||||
fade gradient — pair with a `.no-scrollbar-with-fade` variant).
|
||||
- **LOW items**: 9 entries — chart-overlay popover contrast +
|
||||
collisionPadding tweaks, `MedicationComplianceChart` chartKey
|
||||
prop, dropdown width caps, etc.
|
||||
- **Strategic v1.5 items** stay in
|
||||
`.planning/phase-D-v1418-product-lead-review.md` and
|
||||
`.planning/v15-backlog.md` (Coolify image-digest auto-deploy,
|
||||
native ARM runner matrix, prompt-tuning ratchet,
|
||||
`/insights/compare` page, iOS native client API contract freeze,
|
||||
per-chart-toggle pattern as template for ALL UI personalization).
|
||||
|
||||
---
|
||||
|
||||
## CI / Test status
|
||||
|
||||
- `pnpm typecheck` — 0 errors.
|
||||
- `pnpm lint` — 0 errors / 12 pre-existing baseline warnings.
|
||||
- `pnpm test` — **1605 unit tests** green (was 1539 at v1.4.16,
|
||||
1547 at v1.4.17; +66 net across A/B/D cleanups).
|
||||
- `pnpm test:integration` — **66 / 66** green (was 59 at v1.4.16).
|
||||
- `pnpm format:check` — clean (sweep at `3048dd6`).
|
||||
- `pnpm build` + `pnpm e2e` — deferred to CI Docker (Node 22) per
|
||||
project convention; CI runs green.
|
||||
|
||||
---
|
||||
|
||||
## Production state
|
||||
|
||||
- **URL**: `https://healthlog.bombeck.io`
|
||||
- **`/api/version`**: `1.4.18`
|
||||
- **Image digest**:
|
||||
`sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca`
|
||||
(was v1.4.17: `sha256:936e9cf25b2d8e75d70a7912a42c8b0647e374ece036eb451676d0be9cd120ce`)
|
||||
- **Smoke** (15 routes, live session): 200 across `/`, `/insights`,
|
||||
`/auth/login`, `/settings/integrations`, `/settings/notifications`,
|
||||
`/settings/ai`, `/settings/export`, `/admin`, `/admin/users`,
|
||||
`/admin/api-tokens`, `/admin/backups`, `/admin/system-status`,
|
||||
`/admin/app-logs`, `/achievements`. `/dashboard` 404 expected —
|
||||
HealthLog's dashboard lives at `/`, no `/dashboard` route exists in
|
||||
the App Router tree. Same shape on v1.4.16 / v1.4.17; not a
|
||||
regression.
|
||||
- **Deploy method**: GHCR-tag-build `:1.4.18` succeeded (both tag and
|
||||
main runs green for the second consecutive release post Cleanup-C
|
||||
v1.4.16 qemu-arm64 fix). Coolify auto-deploy fired on the
|
||||
`chore(release)` commit before GHCR finished; force-pull path used
|
||||
on `apps-01`:
|
||||
|
||||
```
|
||||
ssh apps-01 'cd /data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss && \
|
||||
docker compose pull app && docker compose up -d app'
|
||||
```
|
||||
|
||||
`:latest` had already been refreshed by the main GHCR run, so the
|
||||
retag-on-host fallback was NOT needed this release (was needed at
|
||||
v1.4.16). `/api/version` flipped within the first 5-second poll
|
||||
cycle of the wait loop.
|
||||
|
||||
- **Coolify auto-deploy**: still on git-push trigger; deferred again
|
||||
per the maintainer's "leave it" call. The manual Coolify UI toggle
|
||||
remains the 5-min realistic fix per
|
||||
`docs/audit/v1416-auto-deploy-fix.md`.
|
||||
- **GitHub release**: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.18
|
||||
- **Tag naming** (unchanged since v1.4.14): GHCR-OCI tag is `:1.4.18`
|
||||
(no `v` prefix); git tag is `v1.4.18`.
|
||||
|
||||
---
|
||||
|
||||
## Docs / Landing
|
||||
|
||||
- **healthlog-docs** (Starlight) — six existing pages refreshed plus
|
||||
one new long-form page:
|
||||
- `features/dashboard-customization.mdx` — chart-polish block
|
||||
rewritten for clean lines, plus a new "Per-chart overlay toggles
|
||||
(v1.4.18+)" subsection.
|
||||
- `features/gamification.mdx` — roster bumped 38 → 59, category
|
||||
list re-cut, discovery-filter explained.
|
||||
- `features/achievements-hidden.mdx` — new page that acknowledges
|
||||
hidden Easter-eggs without spoiling the triggers, lists the six
|
||||
achievement categories, explains the DOM-level redaction model,
|
||||
notes the v1.4.19 follow-up for the bundle leak.
|
||||
- `dashboard/comparison.mdx` — gradient-fill phrasing dropped.
|
||||
- `configuration/admin-settings.mdx` — API-tokens section
|
||||
annotated "(mobile layout fixed in v1.4.18)".
|
||||
- `self-hosting/scaling.mdx` + `self-hosting/updates.mdx` — image
|
||||
tag examples bumped 1.4.16 → 1.4.18, rollback example
|
||||
1.4.15 → 1.4.17.
|
||||
- **healthlog-landing** (Next.js) — `softwareVersion 1.4.16 →
|
||||
1.4.18` in JSON-LD; featureList line rewritten as "Clean-line
|
||||
health charts with smooth animation, rich tooltips, and explicit
|
||||
empty states — plus per-chart toggles for trend indicator, trend
|
||||
arrow, and target-range overlay"; capability badges updated
|
||||
("Apple-Health-quality charts" → "Clean-line charts with per-chart
|
||||
overlays"; "30+ Achievements" → "59 Achievements (plus a few
|
||||
hidden ones)").
|
||||
|
||||
---
|
||||
|
||||
## v1.5 strategic outlook
|
||||
|
||||
The Product-Lead review at
|
||||
`.planning/phase-D-v1418-product-lead-review.md` contains the updated
|
||||
v1.5 roadmap. Key items:
|
||||
|
||||
1. **Coolify image-digest auto-deploy** (C.1) — still on git-push
|
||||
trigger, 5-min manual Coolify UI toggle.
|
||||
2. **Native ARM runner matrix** (C.2) — re-add `linux/arm64` via
|
||||
`ubuntu-24.04-arm` runner.
|
||||
3. **Cross-user feedback aggregation prompt-tuning** (C.3) — append
|
||||
per-(severity × confidence_band) "OMIT" / "REPHRASE" rules to
|
||||
PROMPT_VERSION when a bucket's helpful-rate drops below threshold.
|
||||
4. **Dedicated `/insights/compare` page** (C.4) — sticky baseline
|
||||
picker; i18n keys still reserved.
|
||||
5. **iOS native client API stabilization** (C.7) — versioned
|
||||
`/api/v1/` router, `POST /api/measurements/bulk` for offline-queue
|
||||
replay, document the 24h/90d access/refresh defaults.
|
||||
6. **Per-chart toggle pattern as template for ALL UI personalization**
|
||||
(C.12, NEW after v1.4.18) — extract `User.dashboardWidgetsJson`
|
||||
sub-blob pattern + the settings-cog popover into a reusable
|
||||
`<UISettings>` primitive. Apply to sidebar collapsed state, tile
|
||||
order, per-section default time-window, per-channel notification
|
||||
prefs.
|
||||
7. **Achievement engine as a "next-best-action" predicate engine**
|
||||
(C.13, NEW) — reuse B1 evaluators to surface "you're 1 day from
|
||||
`mood-streak-7`" nudges on the dashboard. Local, instant, no LLM
|
||||
call.
|
||||
8. **Hidden-achievement pattern as "discover features" tutorial**
|
||||
(C.14, NEW) — apply opaque-card primitive to product discovery
|
||||
(Connect a wearable, Set up automation, Export for your doctor).
|
||||
|
||||
The recommended 6-week focus pick from v1.4.16 still stands
|
||||
(C.1 + C.2 + C.4 + C.7 + C.10 + C.11a Apple Health), with C.12
|
||||
added as a parallelisable side-quest.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — the new service
|
||||
worker otherwise still serves v1.4.17 chunks from cache. One-shot is
|
||||
enough.
|
||||
@@ -1,151 +0,0 @@
|
||||
# HealthLog v1.4.19 — Release Summary
|
||||
|
||||
## Release brief
|
||||
|
||||
Live: v1.4.19 (image digest `sha256:b48f93874cdb…`, `/api/version=1.4.19`)
|
||||
|
||||
Fixed
|
||||
|
||||
- BD im Zielbereich now shows independent values for 7T / 30T / total
|
||||
(was identical because the headline aliased the 30-day percentage)
|
||||
- `/admin/api-tokens` table — no horizontal scrollbar at any viewport
|
||||
(4th attempt — truncate-with-tooltip)
|
||||
- Charts on mobile no longer break the layout when the period chip +
|
||||
range tabs combine
|
||||
- `/insights` raw `metric: blood_pressure_sweet` template leak fixed
|
||||
|
||||
Polish
|
||||
|
||||
- Settings/Integrations: single status pill top-right (Withings + Mood
|
||||
Log) — no more redundant containers
|
||||
- `/insights` single page-level refresh button
|
||||
- Comparison toggle removed from dashboard, kept only on `/insights`
|
||||
- AI prompt: no default "data foundation is strong" opener
|
||||
- Settings input heights, spacing, action-button positions consistent
|
||||
across all sub-routes mobile + desktop
|
||||
- Zielwerte status labels in German
|
||||
- 6 CRITICAL + 21 HIGH copy / consistency / a11y fixes from a
|
||||
comprehensive audit
|
||||
|
||||
Hard-Reload (`Cmd+Shift+R`) for SW reset.
|
||||
v1.4.20 next — full `/insights` redesign with AI Coach (plan in
|
||||
`.planning/phase-D-v1419-product-lead-review.md`).
|
||||
v1.5 reserved for iOS app + Apple Health.
|
||||
|
||||
---
|
||||
|
||||
## What landed (commit highlights per cleanup)
|
||||
|
||||
### Cleanup A — bugs + polish
|
||||
|
||||
| Bucket | Headline commit | Subject |
|
||||
| ------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| A1 | `a856272` | fix(analytics): BD-Zielbereich headline routes through new `allTime` window in `computeBpInTargetWindows()` |
|
||||
| A2 | `77a3ad3` + `a739085` | fix(charts): mobile-first header stack + universal x-axis tick density helper |
|
||||
| A3 | `60a91af` / `98a3d10` / `335f288` / `fa91a73` | feat(insights): comparison toggle relocated to hero, single page refresh, BP/Weight tile strip removed, lowercase template-token leak fixed |
|
||||
| A4 | `b5e9a95` | fix(ai): GROUND RULE 7 — no default-positivity opener; `PROMPT_VERSION` 4.16.1 → 4.19.0 |
|
||||
| A5 | `ba0d6b8` / `0dcc91a` / `47a8fc7` | feat(settings): `<IntegrationStatusPill>` consolidates Withings + Mood Log status UI |
|
||||
| A6 | `957f8e9` / `9fda634` / `1075784` / `737b533` / `78f1f3f` | fix(settings): mobile consistency sweep — input heights, action button placement, language select row |
|
||||
| A7 | `088832a` / `dd8212e` / `7a70db6` / `6507646` / `90a109d` | fix(admin): feedback tab strip, api-tokens scrollbar 4th attempt, Einklappen removal, Zielwerte spacing + DE labels |
|
||||
|
||||
### Cleanup B — A8 quality findings applied
|
||||
|
||||
16 atomic commits — 6/6 CRITICAL + 21/25 HIGH — covering time-window
|
||||
locale, login-overview auth filter, achievement title humanisation,
|
||||
date-input `lang`, sidebar copy, raw enum badges, audit-action labels,
|
||||
mobile Sys/Dia badge enum, telegram badge collapse, token name ISO
|
||||
suffix, recent-activity row links.
|
||||
|
||||
### Cleanup D — multi-pass QA + reconcile
|
||||
|
||||
Six atomic commits cleared the post-Cleanup-B CRITICAL (mobile Sys/Dia
|
||||
badge enum mismatch, `ef74241`), 5 simplify-yes consolidations
|
||||
(`6b35cad`), and four HIGH fixes (`1258b24`, `5a8ad3d`, `977f124`,
|
||||
`1f0d9ad`).
|
||||
|
||||
---
|
||||
|
||||
## What was deferred
|
||||
|
||||
Full backlog: `.planning/v1420-backlog.md`.
|
||||
|
||||
- **3 HIGH from QA** rolled into v1.4.20: D-CR-H-05
|
||||
(`/insights` `data?.` narrowing — large refactor), D-DSGN-H-01
|
||||
(api-tokens touch tooltip — needs Popover swap), D-DSGN-H-02
|
||||
(insights hero density — folded into the v1.4.20 redesign).
|
||||
- **1 HIGH** to v1.5: D-SR-H-3 Withings / Mood Log card-chrome dedup —
|
||||
parked as Apple-Health-card-prep work in `.planning/v15-backlog.md`.
|
||||
- **31 MED + 16 LOW** from the quality-of-life audit — short-list at
|
||||
`.planning/v1420-backlog.md`.
|
||||
- **`/insights` redesign with AI Coach** — separate v1.4.20 roadmap;
|
||||
design handoff at `~/Downloads/design_handoff_insights_redesign`.
|
||||
- **iOS app + Apple Health** — reserved for v1.5
|
||||
(`.planning/v15-backlog.md`).
|
||||
|
||||
---
|
||||
|
||||
## CI / Test status
|
||||
|
||||
- `pnpm typecheck` — 0 errors.
|
||||
- `pnpm lint` — 0 errors / 12 pre-existing baseline warnings (no new).
|
||||
- `pnpm test` — **1672 / 1672** unit tests green (was 1605 at v1.4.18;
|
||||
+67 net across A1, A2, A3, A4, A5, A6, A7, B, and Cleanup D reconcile
|
||||
TDD additions).
|
||||
- `pnpm test:integration` — **67 / 67** green (was 66 at v1.4.18; +1
|
||||
for A1's BD-Zielbereich `allTime` window guard).
|
||||
- `pnpm format:check` — only pre-existing `.planning/*` +
|
||||
`docs/audit/*` baseline noise dirty (same call as v1.4.18 / v1.4.16;
|
||||
no source-tree drift).
|
||||
- `pnpm build` + `pnpm e2e` — deferred to CI Docker (Node 22) per
|
||||
project convention; CI runs green on tag and main.
|
||||
|
||||
---
|
||||
|
||||
## Production deploy
|
||||
|
||||
| Field | Value |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.19` (flipped at 2026-05-10T12:39:59Z) |
|
||||
| Image digest BEFORE | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` (v1.4.18) |
|
||||
| Image digest AFTER | `sha256:b48f93874cdbcd6c2d729f1b8eeb63a6d1bbb90d56f629846ef6eab6cf272aa9` (v1.4.19) |
|
||||
| Release commit | `89f00cf chore(release): v1.4.19` |
|
||||
| Tag | `v1.4.19` (annotated) |
|
||||
| GHCR — tag run | `25628853202` success |
|
||||
| Coolify auto-deploy | NO — main-branch deploy hung and was canceled; host-side retag-on-host fallback used |
|
||||
| Smoke (curl, live session) | `/` 200 · `/insights` 200 · `/admin/api-tokens` 200 · `/settings/integrations` 200 · `/achievements` 200 · `/dashboard` 404 (expected — root is dashboard, no `/dashboard` route in App Router) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.19 |
|
||||
| Docs site | `6e8840e` on `healthlog-docs/main` (six pages refreshed) |
|
||||
| Landing site | `dd5892f` on `healthlog-landing/main` (`softwareVersion` JSON-LD bumped) |
|
||||
|
||||
The tag-build pipeline succeeded; the main-branch deploy stalled and
|
||||
was canceled, so v1.4.19 was promoted via the documented host-side
|
||||
fallback path:
|
||||
|
||||
```
|
||||
docker pull ghcr.io/mbombeck/healthlog:1.4.19
|
||||
docker tag ghcr.io/mbombeck/healthlog:1.4.19 ghcr.io/mbombeck/healthlog:latest
|
||||
docker compose up -d --force-recreate app
|
||||
```
|
||||
|
||||
`/api/version` flipped to `1.4.19` on the first poll cycle after
|
||||
recreate, well inside the 5-minute cap.
|
||||
|
||||
---
|
||||
|
||||
## v1.4.20 strategic preview
|
||||
|
||||
v1.4.20 is the full `/insights` redesign with an AI Coach surface —
|
||||
strategic plan at `.planning/phase-D-v1419-product-lead-review.md`
|
||||
(redesign brief, layout principles, 3 deferred HIGH items folded into
|
||||
the new layout). v1.5 stays reserved for the iOS app + Apple Health
|
||||
integration; design handoff at
|
||||
`~/Downloads/design_handoff_insights_redesign`.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — the new service
|
||||
worker otherwise still serves v1.4.18 chunks from cache. One-shot is
|
||||
enough.
|
||||
@@ -1,283 +0,0 @@
|
||||
# HealthLog v1.4.20 — Release Summary
|
||||
|
||||
## Release brief
|
||||
|
||||
Live: v1.4.20 (image digest
|
||||
`sha256:b112a31947b9…`, `/api/version=1.4.20`).
|
||||
|
||||
v1.4.20 is the deliberately-loud release that cashes in the runway
|
||||
v1.4.19 set up. The redesigned `/insights` page lands a hero strip
|
||||
with greeting + suggested-prompt chips, a Daily Briefing card, an
|
||||
AI Coach drawer with SSE-streaming chat and encrypted conversation
|
||||
history, three pre-defined correlation cards (BP × medication
|
||||
compliance, mood × pulse, weight × weekday), a Trends row with AI
|
||||
annotations, a printable Weekly Report at `/insights/report/[week]`,
|
||||
and a Personal Health Score panel (composite 0-100 with three
|
||||
bands). The polish-vs-feature ratio inverted to roughly 30/70 — the
|
||||
right ratio for a release that means to ship new product.
|
||||
|
||||
`PROMPT_VERSION` ratcheted 4.19.0 → 4.20.2 across three controlled
|
||||
bumps; `aiInsightResponseSchema` gained five optional blocks
|
||||
(`dailyBriefing`, `trendAnnotations`, `weeklyReport`,
|
||||
`storyboardAnnotations`, `healthScore`). Every new field is
|
||||
nullable + optional, so insights cached before v1.4.20 still parse
|
||||
without a re-run — the v1.4.17 lesson is wired in by construction
|
||||
now, not by audit.
|
||||
|
||||
Hard-Reload (`Cmd+Shift+R`) for SW reset.
|
||||
|
||||
v1.4.20 is also the first release shipped through the new
|
||||
`develop` → `main` branch model introduced in this milestone's
|
||||
foundation phase: feature work lands on `develop` and stays out of
|
||||
the GHCR build path; only the release-merge to `main` plus the
|
||||
annotated tag trigger image publication. The model is documented in
|
||||
`CONTRIBUTING.md` and on the public docs site under
|
||||
`/contributing/branch-model/`.
|
||||
|
||||
---
|
||||
|
||||
## Production deploy
|
||||
|
||||
| Field | Value |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.20` (flipped at 2026-05-10T16:49:25Z) |
|
||||
| Image digest BEFORE | `sha256:b48f93874cdbcd6c2d729f1b8eeb63a6d1bbb90d56f629846ef6eab6cf272aa9` (v1.4.19) |
|
||||
| Image digest AFTER | `sha256:b112a31947b91f2fdb4bb01b60c6b17a0b8ec128ae116d37e1094da5c3decaa6` (v1.4.20) |
|
||||
| Release-prep commit (develop) | `aac8b5e chore(release): v1.4.20` |
|
||||
| Develop ↔ main sync commit | `dd2ef57 Merge main into develop — sync Dependabot dep bumps before v1.4.20 tag` |
|
||||
| Release-merge commit (main) | `666f6ee Release v1.4.20` |
|
||||
| Tag | `v1.4.20` (annotated) at `666f6ee` |
|
||||
| GHCR — tag run | `25633807964` success (4m02s) |
|
||||
| Coolify auto-deploy | NO — webhook secret still missing on the deploy server (same fault mode as v1.4.19); host-side retag fallback used |
|
||||
| Smoke (curl, no session) | `/api/version` 200 · `/auth/login` 200 · all gated routes return 307 → `/auth/login` (expected proxy.ts behaviour without a session cookie) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.20 |
|
||||
| Docs site | `0b83430` on `healthlog-docs/main` (updates.mdx + scaling.mdx version pins; `ai-insights.mdx` v1.4.20 callout) |
|
||||
| Landing site | `43ce4c7` on `healthlog-landing/main` (`softwareVersion` JSON-LD bumped, three new featureList entries) |
|
||||
|
||||
The tag-build pipeline succeeded; the deploy webhook still isn't
|
||||
wired on apps-01, so v1.4.20 was promoted via the documented
|
||||
host-side fallback path:
|
||||
|
||||
```
|
||||
docker pull ghcr.io/mbombeck/healthlog:1.4.20
|
||||
docker tag ghcr.io/mbombeck/healthlog:1.4.20 ghcr.io/mbombeck/healthlog:latest
|
||||
cd /data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss
|
||||
docker compose up -d --force-recreate app
|
||||
```
|
||||
|
||||
`/api/version` flipped to `1.4.20` on the first poll cycle after
|
||||
recreate — well inside the 5-minute cap. The Coolify-managed
|
||||
restart-and-redeploy path was tried twice first (`d10hrd3he27w…`
|
||||
and `tgkn9ewch46c…`); both ran cleanly to "finished" but neither
|
||||
pulled a fresh manifest, so the running container kept serving
|
||||
v1.4.19. Wiring an image-digest auto-deploy hook on apps-01 is
|
||||
captured in `.planning/v1421-backlog.md`.
|
||||
|
||||
---
|
||||
|
||||
## Smoke results
|
||||
|
||||
The previous releases' brief showed 200s because they ran with a
|
||||
live session cookie that's not persisted on this run. Without the
|
||||
cookie, every authenticated route correctly returns 307 →
|
||||
`/auth/login`, so the smoke verifies the proxy gate is healthy
|
||||
end-to-end:
|
||||
|
||||
| Path | Status (no session) | Notes |
|
||||
| --------------------------- | ------------------- | ------------------------------------------------------ |
|
||||
| `/api/version` | 200 | Returns `version: "1.4.20"` |
|
||||
| `/auth/login` | 200 | Public surface, renders normally |
|
||||
| `/` | 307 → `/auth/login` | Gated dashboard root |
|
||||
| `/insights` | 307 → `/auth/login` | Redesigned Insights shell |
|
||||
| `/insights/report/2026-W19` | 307 → `/auth/login` | New v1.4.20 weekly-report route |
|
||||
| `/admin/api-tokens` | 307 → `/auth/login` | Admin section |
|
||||
| `/settings/integrations` | 307 → `/auth/login` | Settings section |
|
||||
| `/achievements` | 307 → `/auth/login` | Achievements page |
|
||||
| `/dashboard` | 307 → `/auth/login` | Expected: root is the dashboard, no `/dashboard` route |
|
||||
|
||||
The Coach drawer and Health Score panel can't be smoked without a
|
||||
live session — neither carries a public surface. Confirmed live via
|
||||
the `/api/version` check; Wave-D reviewers exercised both paths
|
||||
under integration coverage before tag.
|
||||
|
||||
---
|
||||
|
||||
## What shipped
|
||||
|
||||
### Hero strip + Daily Briefing + Suggested Prompts
|
||||
|
||||
`/insights` opens with a redesigned hero strip that pairs a
|
||||
time-of-day greeting and primary action row with a horizontal strip
|
||||
of suggested-prompt chips. A Daily Briefing card mounts full-width
|
||||
below the hero, surfacing an AI-generated paragraph plus three
|
||||
keyFindings drawn from the last 24-hour window. Suggested-prompt
|
||||
chips drive the Coach drawer with the prompt prefilled, so the
|
||||
shortest path from "I want to know X" to a streaming answer is one
|
||||
tap. The four-vitals tile row from the design artboard was
|
||||
intentionally deferred — the per-section status cards already
|
||||
render the same numbers, and the briefing fills the at-a-glance
|
||||
role.
|
||||
|
||||
### AI Coach drawer with streaming chat + encrypted persistence
|
||||
|
||||
The biggest piece of the release. `POST /api/insights/chat` is an
|
||||
SSE endpoint that walks the existing AI provider chain, persists
|
||||
user + assistant turns under AES-256-GCM, and emits `token` →
|
||||
`provenance` → `done` frames. A per-user daily token budget
|
||||
(25 000 tokens, ≈13 turns for a heavy user) caps shared-key spend;
|
||||
a refusal pattern blocks prompt-injection attempts and obvious
|
||||
off-topic asks before the provider call. The drawer mounts from the
|
||||
hero strip's "Ask the coach" button and from any suggested-prompt
|
||||
chip; layout is three columns on `lg+` (history rail · message
|
||||
thread · sources rail) and full-screen single-column on mobile with
|
||||
chevron-button trays for the rails. Source-chip provenance attaches
|
||||
to every assistant turn (metric, window, n-count — labels only,
|
||||
never raw values). `parseSseChunk()` is exported as a pure helper
|
||||
so the streaming parser is unit-testable without a live network.
|
||||
|
||||
### Correlation cards + Trends row with AI annotations
|
||||
|
||||
Three pre-defined hypothesis cards mount between the Daily Briefing
|
||||
and the Advisor card: BP × medication compliance, mood × pulse, and
|
||||
weight × weekday. Pearson r is rendered with a Fisher-z confidence
|
||||
interval; surfacing gate is n ≥ 14 paired observations and p < 0.05.
|
||||
The Trends row alongside it carries short AI-generated annotations
|
||||
tied to specific dates in the analyzed window; the BP timeline
|
||||
gains storyboard markers at the same positions via an additive
|
||||
`annotations[]` prop on `<HealthChart>`.
|
||||
|
||||
### Weekly Report at `/insights/report/[week]`
|
||||
|
||||
A newsletter-style printable surface with five sections: Summary,
|
||||
Going-well, Worth-watching, Tips, Data-quality. `window.print()`
|
||||
export via Tailwind `print:` variants gives a clean A4 page without
|
||||
a server round-trip. The hero strip surfaces fresh weekly reports
|
||||
with Read · Share · Export PDF actions; Share uses Web Share API
|
||||
with a clipboard fallback, Export deep-links to `?print=1`.
|
||||
|
||||
### Personal Health Score (composite 0–100)
|
||||
|
||||
A deterministic composite score with three bands (green ≥75, yellow
|
||||
50–74, red <50). Weights: 30% BP-target rate + 20% weight-trend
|
||||
alignment + 20% mood stability + 30% medication compliance. When
|
||||
a sub-component is null, remaining weights scale to 100 so the
|
||||
score stays interpretable. The `<HealthScoreCard>` renders alongside
|
||||
the hero strip on `lg+` (220 px desktop, full-width mobile) with a
|
||||
band-tinted progress bar, four sub-bars, "vs last week" delta line,
|
||||
and a score-aware "Ask the Coach" CTA that opens the drawer with
|
||||
"Why is my health score X out of 100?" prefilled.
|
||||
|
||||
---
|
||||
|
||||
## Quality bar
|
||||
|
||||
- **Test counts moved hard.** 1672 → 2026 unit tests (+354 net),
|
||||
67 → 81 integration (+14). Five new test files for the Coach
|
||||
alone (use-coach, source-chips, message-thread, coach-input,
|
||||
sources-rail, history-rail). Test files 217 → 237.
|
||||
- **typecheck + lint clean.** 0 type errors, 13 baseline warnings
|
||||
unchanged (every warning is the `_request` / `_params` /
|
||||
intentional-unused pattern).
|
||||
- **Multi-reviewer Wave-D pass.** Six reviewers (code, security,
|
||||
design, senior-dev, simplify, product-lead) filed 0 CRIT · 12
|
||||
HIGH · 28 MED · 22 LOW + 5 simplify-apply-yes + 4 simplify-apply-
|
||||
maybe + 3 simplify-apply-no. All 12 HIGH items + the design
|
||||
pushback on the disabled weekly-report button + 6 MED items +
|
||||
every simplify-apply-yes item landed inline across 11 atomic
|
||||
commits on `develop` (`e632e26` → `31fbf98`). 22 MED + 16 LOW + 4
|
||||
simplify-apply-maybe deferred to `.planning/v1421-backlog.md`
|
||||
under "Phase D — v1.4.20 reconcile carry-over".
|
||||
- **One known integration-test flake.** `coach-chat.test.ts` /
|
||||
"round-trips" was observed to flake on a single run during
|
||||
Phase D and again during pre-flight; consistent re-runs pass.
|
||||
Tracked in the v1.4.21 backlog.
|
||||
- **format:check clean on the source tree.** A small style commit
|
||||
(`73e51d7 style: prettier on touched insights/coach surfaces`)
|
||||
ran prettier across the new B-phase additions before tag, so the
|
||||
only remaining `format:check` warnings are the `.planning/*`
|
||||
baseline noise carried over since v1.4.16.
|
||||
|
||||
---
|
||||
|
||||
## Branch model
|
||||
|
||||
v1.4.20 is the first release shipped through the new
|
||||
`develop` → `main` model:
|
||||
|
||||
- All B1 → B5 phase work, every Wave-D reconcile commit, the
|
||||
release-prep commit, the prettier sweep, and the merge-back of
|
||||
Dependabot's day-of dep bumps landed on `develop`.
|
||||
- A single `Release v1.4.20` no-ff merge commit on `main` plus the
|
||||
annotated `v1.4.20` tag triggered the GHCR tag-build job
|
||||
(`25633807964`).
|
||||
- The GHCR workflow is gated to `branches: [main]` + `tags: ['v*']`
|
||||
— `develop` pushes neither build nor publish images, which
|
||||
removes the v1.4.13-era image-churn problem on GHCR.
|
||||
- Two Dependabot dep-bump PRs (`#161`, `#165`) auto-merged into
|
||||
`main` after the tag build started but before the host-side
|
||||
fallback ran. They were absorbed into `develop` ahead of the
|
||||
release-merge via a `Merge main into develop` commit, so the
|
||||
v1.4.20 tag includes them. Three more dep-bump PRs (#153, #154,
|
||||
#157, #158, #159, #160) had auto-merged earlier the same day and
|
||||
were absorbed the same way.
|
||||
|
||||
---
|
||||
|
||||
## Carry-over
|
||||
|
||||
Full backlog: `.planning/v1421-backlog.md`.
|
||||
|
||||
- **22 MED + 16 LOW + 4 simplify-apply-maybe** items from the
|
||||
multi-reviewer Phase-D pass. Headline items: senior-dev call to
|
||||
consolidate the duplicated Pearson / linear-regression maths
|
||||
layer (Sr-HIGH-2 — too large for inline reconcile); refactor
|
||||
`<CoachDrawer key={prefill}>` into a controlled prefill prop
|
||||
(Sr-HIGH-4); transactional `recordSpend()` (Sec-M-4); refusal
|
||||
accounting and lexicon expansion (Sec-M-2 / Sec-M-3).
|
||||
- **Schema drift on `medication_schedules.days_of_week`.** Worked
|
||||
around twice in v1.4.20 (B5 analytics, B2a coach snapshot); the
|
||||
migration needs to either deploy the column or drop it from
|
||||
`schema.prisma`.
|
||||
- **`src/app/insights/page.tsx` now composes nine distinct
|
||||
surfaces.** Splitting the component is on the v1.4.21 cleanup
|
||||
short-list; pre-emptive splitting before the multi-reviewer pass
|
||||
ran was deliberately avoided.
|
||||
- **Coolify image-digest auto-deploy hook on apps-01.** Tag-build
|
||||
pipeline is fine; the deploy step needs the webhook wired so
|
||||
`/api/version` flips on its own without the manual host-side
|
||||
retag.
|
||||
- **Source-comment sweep** (191 maintainer-name references in
|
||||
`src/`), **DE+EN bilingual CHANGELOG entries** (v1.4.15 era),
|
||||
and **`CLAUDE.md` / `AGENTS.md` filename hygiene** are tracked
|
||||
for a hygiene PR.
|
||||
|
||||
---
|
||||
|
||||
## Strategic next
|
||||
|
||||
v1.5 stays reserved for the iOS native app + Apple Health
|
||||
integration + per-metric APNs alerts. The redesigned `/insights`
|
||||
shell, AI Coach drawer, and Health Score panel were all built with
|
||||
that next milestone in mind: the Coach SSE endpoint accepts the
|
||||
same Bearer-token auth that ships for the existing native API
|
||||
clients (`hlk_<64hex>`); the Health Score formula is deterministic
|
||||
and pure, so an iOS port can compute the same number locally if it
|
||||
wants to; the source-chip provenance contract is JSON-only and
|
||||
labels-only, ready for an APNs payload. Strategic plan and the
|
||||
artboard / API-shape sketches at
|
||||
`.planning/phase-D-v1420-product-lead-review.md`.
|
||||
|
||||
---
|
||||
|
||||
## GH release URL
|
||||
|
||||
https://github.com/MBombeck/HealthLog/releases/tag/v1.4.20
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) — the new service
|
||||
worker otherwise still serves v1.4.19 chunks from cache. One-shot
|
||||
is enough.
|
||||
@@ -1,55 +0,0 @@
|
||||
# HealthLog v1.4.21 — release summary
|
||||
|
||||
## Release brief
|
||||
|
||||
v1.4.21 ships as a same-day patch on top of v1.4.20 to fix a regression in the Insights redesign and tighten the AI Coach so it actually answers the questions it now invites. Five issues hit production within hours of the v1.4.20 tag, four of them user-reported, one surfaced in the post-deploy check. Daily Briefing regenerate was the headline: clicking "Re-run analysis" produced no briefing because `/api/insights/generate` was still calling the legacy system prompt, which never asks the model for a `dailyBriefing` block. The Coach felt generic because the snapshot it shipped to the model was aggregates only — the model honestly told users it could not isolate weekday-specific questions because it had no day-level data. Both are fixed. The Coach now carries day-level readings tagged with weekday, the regenerate path uses the strict-schema prompt, the duplicate streaming bubble is gone, the settings cog moved out from under the drawer's close-X, and the sources rail finally exposes the per-source toggles + window selector that the artboard always promised. Verdict: v1.4.21 makes v1.4.20 land as intended.
|
||||
|
||||
## Live state
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------------- |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.21` |
|
||||
| Image digest | `sha256:4e818d44702c3581a14d6480a953fd20d16cbbaf21c41e0c778c07340d3c4b1c` |
|
||||
| Version transition | 2026-05-10T17:46:xx (host-side retag fallback) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.21 |
|
||||
| Branch model | first patch through `develop` → `main` release-merge model (introduced in v1.4.20 F1) |
|
||||
|
||||
## Smoke (no session)
|
||||
|
||||
`/api/version` → 200 (returns `1.4.21`). Every gated route (`/`, `/insights`, `/insights/report/2026-W19`, `/admin/api-tokens`, `/settings/integrations`, `/achievements`) returns 307 → `/auth/login`, confirming the proxy gate is alive on the new image.
|
||||
|
||||
## What shipped
|
||||
|
||||
**Daily Briefing regenerate fixed.** The `/api/insights/generate` route called `getInsightsSystemPrompt` (legacy v1.4.5 schema, no briefing block) so manual regeneration always wrote a payload without a `dailyBriefing`. Switched to `getStrictInsightsSystemPrompt(locale)` so the v1.4.20 GROUND RULE 8 applies on every call. Cached legacy blobs still parse cleanly — the schema fields are optional, the SSE stream simply renders the empty state until the next regeneration produces one.
|
||||
|
||||
**Coach context now carries day-level readings.** `buildCoachSnapshot` ships a `timeline.recent` block (one row per UTC day for the last 14 days, weekday-tagged) and a `timeline.weekly` block (ISO-week buckets covering the rest of the analysis window). Per-day medication adherence rows come straight from `MedicationIntakeEvent`. The EN + DE Coach system prompts grew a DAY-LEVEL READINGS section telling the model to answer weekday questions out of `timeline.recent` with explicit date + weekday citations, fall back to `timeline.weekly` for older windows, and acknowledge missing days plainly instead of apologising about aggregate-only context. Token cost per Coach turn rises from ≈190 tokens (aggregates only) to ≈3000 tokens for a full-scope snapshot. The scope picker is the relief valve.
|
||||
|
||||
**Per-source + per-window scope picker on the Coach sources rail.** The rail grew real checkboxes (BP / Weight / Pulse / Mood / Compliance) at a 36px touch target with 60% opacity when excluded, and a window selector (7d / 30d / 90d / all-time). The drawer owns the scope state and forwards it through `useSendCoachMessage` to the chat request body. Toggles reset to "all sources, last 30d" on each drawer open. A single-source last-7-days narrowed turn lands around ≈600–700 tokens.
|
||||
|
||||
**Duplicate streaming bubble fixed.** After `done`, the streaming hook keeps `streaming.content` populated for the in-flight render path, then fires the TanStack invalidate that pulls the persisted assistant message into the conversation cache. The thread rendered the reply twice until the next `send` reset cleared it. The render path now suppresses the in-flight bubble as soon as the persisted twin lands, keyed on `streaming.messageId`.
|
||||
|
||||
**Settings cog vs Sheet close-X collision fixed.** Radix Sheet paints its default close-X at `top-4 right-4`, which visually swallowed the cog in the right-edge button cluster. The cog moved to the left header zone next to the gradient avatar; a `pr-12 / sm:pr-14` padding rule keeps the New-chat button out of the close-X area on narrower viewports.
|
||||
|
||||
**Suggested-prompt chips meet the 36px touch target.** Chips were rendering at 28px, below the WCAG-AA target-size guideline. Padding bumped to a 36px hit area.
|
||||
|
||||
## Token-budget tradeoff
|
||||
|
||||
The day-level snapshot makes the Coach actually personal at the cost of higher per-turn token usage. For a heavy user (full-scope, last 30 days), the budget shifts from ≈13 to ≈8 turns/day at the existing 25k tokens-per-user-per-day cap. The scope picker is the explicit lever: a single-source 7-day narrow turn is ≈600 tokens, leaving plenty of room for a long conversation about a specific metric.
|
||||
|
||||
## Quality
|
||||
|
||||
- 7-commit fix wave on `develop` (`7921ffc` regenerate prompt, `ed61b17` snapshot day-level, `2143377` system-prompt instruction, `f07e35b` streaming bubble, `ddb2914` cog vs close-X, `08fd411` scope picker, `52dd9ff` planning record)
|
||||
- 1-commit release-prep on `develop` (`9955288 chore(release): v1.4.21`)
|
||||
- Release-merge `8e198e1 Release v1.4.21` on `main`
|
||||
- Tests: 2026 → 2036 unit (+10), 81 / 81 integration unchanged, typecheck clean, lint baseline (13 warnings, no new)
|
||||
|
||||
## Carry-overs
|
||||
|
||||
The Coolify "image-digest auto-deploy" bug is still present — the queued deploy completed in 17 seconds without a real `docker pull`, so `:latest` stayed pinned to the v1.4.20 digest in the local Docker cache. Worked around with the host-side retag procedure (`docker pull :1.4.21 → docker tag :latest → docker compose up -d --force-recreate`) on apps-01. Same fault that hit v1.4.19 + v1.4.20. The watchtower-via-Coolify path or the Coolify image-digest checkbox remain the durable fixes; tracked in the v1.4.21 backlog.
|
||||
|
||||
The e2e workflow on `main` reported 7 failing specs after the v1.4.20 tag. Five are stale selectors from the B1 hero rewrite (`[data-slot="insights-page-hero"]` no longer exists; the new `<HeroStrip>` exposes a different slot), the other two are layout drift on Pixel-5. A focused fix wave is in flight on `develop`; the next release will land them.
|
||||
|
||||
## Strategic next
|
||||
|
||||
v1.5 plan in `.planning/phase-D-v1420-product-lead-review.md` (iOS native client + Apple Health ingest contract, Coach extension for HRV / Sleep / Resting HR / Steps, per-metric APNs alerts, OpenAPI spec drift CI gate). v1.4.22 will pick up the e2e fix wave plus whatever else lands on the v1.4.21 backlog.
|
||||
@@ -1,60 +0,0 @@
|
||||
# HealthLog v1.4.22 — release summary
|
||||
|
||||
## Release brief
|
||||
|
||||
v1.4.22 is the polish release that closes loops the Insights redesign, the AI Coach, and the BD-Zielbereich tile have been carrying since v1.4.16. Two beats define it. The first is the Coach prose rewrite — the model used to open every reply with a number, which read clinical and database-cursor-shaped; v1.4.22 reframes the persona as warm, curious, and reserved, and moves every aggregate value out of the prose into a collapsible "Worauf bezieht sich das?" / "What I'm looking at" disclosure under each turn. The second is the long tail of small fixes that finally settle metrics, layout, and a11y where they should have landed two releases ago: the BD-Zielbereich headline re-anchored to the last-30-day window (with all-time as a sub-row), the Targets / Zielwerte page grew per-target sparklines and a Δ-vs-last-month caption, the sticky section navigation lifted above the Insights hero with proper `aria-current` + `motion-reduce` semantics, the comparison-overlay collapsed into a single global preference under Settings → Dashboard, and the admin / API-tokens horizontal scrollbar is gone on the fifth attempt. PROMPT_VERSION ratcheted 4.20.2 → 4.22.0 — the first minor-digit bump in the v1.4.x line, intentional because the persona, the sentinel-block contract, and the warmth of the prose all changed in one coherent step. v1.4.22 is the milestone that makes v1.5's iOS push start on a clean foundation instead of debt repayment.
|
||||
|
||||
## Live state
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------ |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.22` |
|
||||
| Image digest | `sha256:865154614303fdc362ee3941776f73ec0f60e1f16112ec272a75cbbe28e2cffb` |
|
||||
| Version transition | 2026-05-10T22:43:50+02:00 (host-side retag fallback) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.22 |
|
||||
| Branch model | second release through the `develop` → `main` model (introduced in v1.4.20 F1) |
|
||||
|
||||
## Smoke (no session)
|
||||
|
||||
`/api/version` → 200 (returns `1.4.22`). Every gated route (`/`, `/insights`, `/insights/report/2026-W19`, `/admin/api-tokens`, `/settings/integrations`, `/achievements`) returns 307 → `/auth/login`, confirming the proxy gate is alive on the new image.
|
||||
|
||||
## What shipped
|
||||
|
||||
**Insights surface polish.** Six changes that together move the page from "feature-rich and slightly cluttered" to "deliberate". The BD-Zielbereich tile got its fourth-attempt fix: the headline now reads `windows.last30Days?.pct` instead of the all-time aggregate, surfaces all-time as a sub-row, and gains a synthesised trend arrow plus a 7-day-trend chip. The on-surface comparison toggle retired from `/insights` because the canonical picker has lived in Settings → Dashboard since v1.4.16 (two surfaces violated the no-split-Settings rule). Three half-row layouts that left empty space got a row-fill rule. Trends-row card heights equalised. The "Muster" / "Patterns" row renamed to "Zusammenhänge" / "Relationships" because the row directly above already uses Trends. The sticky section navigation lifted above the hero with proper a11y semantics (`aria-current`, focus-visible ring, `motion-reduce`, observer-ratio sorting). And a single missing `stripChartTokens()` call upstream was producing raw `metric:WEIGHT` / `metric:BLOOD_PRESSURE_SYS` token leaks in recommendation prose; fix landed plus four DE Health-Score component labels normalised to German nouns.
|
||||
|
||||
**Coach prose rewrite + collapsible evidence.** PROMPT_VERSION 4.20.2 → 4.22.0. The Coach used to open every reply with a number; the new persona is warm, curious, reserved — a partner sitting alongside the user, not pushing data. Numbers move out of the prose into a `---KEYVALUES---` … `---END---` sentinel block that the chat route parses out server-side and renders as a `<details>` disclosure under each assistant turn, closed by default and hidden when no key-values came back. Three-way hard caps on the sentinel (1 KB payload, 8 lines max, per-line Zod) so a prompt-injection attempt can't grow the persisted envelope. The user avatar reuses the existing Gravatar URL at the same dimensions as the Coach avatar. The medical disclaimer pinned at the bottom of the message thread on every viewport (clinical-adjacent UI must not gate the disclaimer behind a chevron tray). The settings cog removed because it was a dead button in v1.4.21 — per-user prompt-tuning is deferred to v1.4.23. A graceful fallback covers the sentinel-only-output edge case so the model never surfaces a raw marker.
|
||||
|
||||
**Other surfaces + backlog cleanup.** The Targets / Zielwerte page stopped reading like a clinical cheat-sheet. Each `<TargetCard>` grew a 30-day inline SVG sparkline beneath the range bar plus a localised "Δ −2.3 kg vs. last month" caption; both null when either window has fewer than 3 readings so cold-start accounts don't see a misleading flat trace. The admin / API-tokens horizontal scrollbar fell to a live Playwright probe that traced the residual culprit to `whitespace-nowrap` on the date `<td>`s. The AuthShell post-hydration redirect on incomplete onboarding moved into `proxy.ts` so the dashboard flash is gone; the proxy reads a non-httpOnly `hl_onboarding` cookie that auth routes mirror from `onboardingCompletedAt`, with `SameSite=Strict` so cross-site requests can't see it. The `setOnboardingPendingCookie` write folded into `createSession()` so issuing a session without onboarding state is type-impossible. Twelve backlog items from `v1421-backlog.md` swept; hygiene continued (191 maintainer-name references in source comments swept, v1.4.14+v1.4.15 bilingual CHANGELOG entries normalised to English-only, the AI-vendor-specific contributor file retired in favour of `CONTRIBUTING.md`).
|
||||
|
||||
**Multi-leg QA + reconcile.** Six-leg review (code, security, design, senior-dev, simplify, product-lead) found 0 CRITICAL across all reviews. Seven HIGH findings — all applied inline. About six MEDs applied inline (judgment-pick high-leverage); the rest documented in `.planning/v1422-backlog.md` for v1.4.23. Highlights of the inline applications: BD-Zielbereich tile compareDelta now matches caption (subtracts `bpInTargetPctPriorMonth` instead of `bpInTargetPctAllTime`); `createSession()` requires `onboardingPending` as a parameter so the cookie-write fan-out is one site; `createSseStream` extracted to `src/lib/sse/create-stream.ts` ahead of v1.5 iOS streaming endpoints; `berlinDayKey()` lifted to a shared `src/lib/analytics/berlin-day.ts` helper so the targets sparkline and analytics route share the same bucketing; BP tile collapses to a single secondary row at `<sm`; sticky section nav got the full a11y polish in one commit; `<TokenStatusBadge>` extracted from the duplicated desktop + mobile api-token surfaces.
|
||||
|
||||
## Token-budget impact
|
||||
|
||||
Coach token budget unchanged from v1.4.21. The prose rewrite swaps inline numbers for a sentinel block that the route strips before persisting; the per-turn cost remains ≈3 000 tokens for a full-scope snapshot, ≈600–700 for a single-source last-7-days narrowed turn. The 25 000 tokens-per-user-per-day cap is unchanged; ≈8 turns/day for a heavy full-scope user, plenty of room for a single-metric narrowed conversation.
|
||||
|
||||
## Branch model
|
||||
|
||||
The `develop` → `main` model (introduced in v1.4.20 F1) carried its second release cleanly. Long-lived `develop` accumulated 35 commits between v1.4.21 and v1.4.22 (W1 research, W2/W3/W4 implementation, W5 reconcile, release-prep). The release-merge into `main` was a single `--no-ff` with the full list of commits in the merge body. Future GHCR builds run on `main` only.
|
||||
|
||||
## Quality
|
||||
|
||||
- W1 research + W2/W3/W4 implementation + W5 reconcile across 35 commits on `develop` (`8606767` → `b094eaa`)
|
||||
- 1-commit prettier pass on W5 touchpoints (`bbdc3fb`)
|
||||
- 1-commit release-prep on `develop` (`d71e879`)
|
||||
- Release-merge `005fed0` on `main`
|
||||
- Tests: 2026 → 2111 unit (+85), 81 → 89 integration (+8), typecheck clean, lint baseline (15 warnings, no new)
|
||||
|
||||
## Carry-overs
|
||||
|
||||
The Coolify image-digest auto-deploy bug is still present and the workflow's auto-deploy step skipped because the `COOLIFY_WEBHOOK` / `COOLIFY_TOKEN` repo secrets aren't configured. The `.planning/coolify-auto-deploy-howto.md` runbook landed in W4 C3 documents the one-time UI toggle; bumping the secrets and flipping the toggle should drop the host-side retag fallback for v1.4.23. Worked around with the host-side retag procedure (`docker pull :1.4.22 → docker tag :latest → docker compose up -d --force-recreate`) on apps-01. Same fault that hit v1.4.19, v1.4.20, and v1.4.21.
|
||||
|
||||
The full v1.4.23 carry-over list lives in `.planning/v1422-backlog.md`. Highlights: sentinel parser malformed-enum hardening (Sr-M5, ~30 LOC, highest signal-to-effort ratio of the deferred items); analytics-route unbounded `findMany` paging; targets-route 7-pass sparkline coalesce; `CoachDrawer key={prefill}` controlled-prop refactor (Sr-HIGH-4); per-user prompt-tuning surface; medication_schedules.days_of_week schema-drift cleanup.
|
||||
|
||||
## Strategic next
|
||||
|
||||
v1.5 plan in `.planning/phase-W5-v1422-product-lead-review.md`. Headline is the iOS native client + Apple Health ingest contract — six phases, P1 first launch (bearer + refresh-token end-to-end with one login page + one dashboard widget) through P6 cross-user feedback aggregator cron. New measurement types (HRV, Sleep, Resting HR, Steps, BodyFat, Glucose) extend the Coach to PROMPT_VERSION 5.0.0 with conservative phrasing rules around clinical sleep-stage conclusions and HRV claims. Per-metric APNs alerts add `src/lib/notifications/senders/apns.ts`. The Insights page split (the ~9-surface orchestrator) lands in v1.5 P5 alongside the Vitals tile row, not v1.4.23, so the split maps to the exact sub-trees the Apple Health work touches.
|
||||
|
||||
## Deploy path used
|
||||
|
||||
Host-side SSH retag fallback. The GHCR tag-build (workflow run `25639069964`) succeeded, but the auto-deploy step skipped because the `COOLIFY_WEBHOOK` / `COOLIFY_TOKEN` repo secrets aren't configured. A Coolify MCP `force=true` deploy was queued (`owrpyzoi1ijsvhge0hfv25bw`) but didn't refresh the digest within the 5-minute polling window — same `:latest` pinning bug from v1.4.21. The `ssh apps-01 'docker pull :1.4.22 → docker tag :latest → docker compose up -d --force-recreate'` procedure flipped `/api/version` from `1.4.21` to `1.4.22` at 22:43:50 +02:00.
|
||||
@@ -1,110 +0,0 @@
|
||||
# HealthLog v1.4.23 — release summary
|
||||
|
||||
## Release brief
|
||||
|
||||
v1.4.23 is the foundation release v1.4.x hasn't shipped before: the user-visible delta is small on purpose because the weight is in the contract surface underneath it. Seven new `MeasurementType` values, `APPLE_HEALTH` joining `MeasurementSource`, a sleep-stage enum, an idempotent `POST /api/measurements/batch` endpoint, APNs scaffolding wired into the existing notification cascade, an OpenAPI 3.1 generator with a warn-only drift CI gate, per-device refresh-token reuse-detection, three new device-management routes, a per-user Coach prefs surface behind the settings cog, a per-message Coach helpful/unhelpful loop, and seven hygiene items from the v1.4.22 backlog. None of those land as loud features; together they're the shape of v1.5's contract surface, frozen in code before the iOS app starts compiling against it. The cadence has matured beyond the loud/quiet two-beat into a three-beat: **loud release → polish release → foundation release**. v1.4.20 / v1.4.22 / v1.4.23 was the first time the whole cycle ran end-to-end.
|
||||
|
||||
## Live state
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------- |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.23` |
|
||||
| Image digest | `sha256:b20c25a49b1835aa66c03a4670c27a3ba0ea9414dab3ee4a45f5cd35d4f353d1` |
|
||||
| Version transition | 2026-05-11T06:40:49Z (host-side retag fallback) |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.23 |
|
||||
| Branch model | `develop` → `main` release-merge model, fourth tag through it (v1.4.20 onwards) |
|
||||
|
||||
## Smoke (no session)
|
||||
|
||||
`/api/version` → 200 (returns `1.4.23`). Every gated route (`/`, `/insights`, `/insights/report/2026-W19`, `/admin/api-tokens`, `/settings/integrations`, `/achievements`) returns 307 → `/auth/login`, confirming the proxy gate is alive on the new image.
|
||||
|
||||
## What shipped
|
||||
|
||||
### Wave 1 — Research
|
||||
|
||||
One research pass with three streams ran before any code went in: the Apple Health `HKQuantityTypeIdentifier` → `MeasurementType` mapping (with units + canonical aggregation rules), the APNs Node library decision (`@parse/node-apn` versus alternatives — chosen for type-safety + maintenance posture + lazy provider init), and the OpenAPI 3.1 tooling choice (`zod-openapi` + `yaml@^2` — picked for byte-stable output across runs once `sortMapEntries: true` is on).
|
||||
|
||||
### Wave 2 — Apple Health foundation
|
||||
|
||||
Seven additive `MeasurementType` values (`HEART_RATE_VARIABILITY`, `RESTING_HEART_RATE`, `ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, `WALKING_RUNNING_DISTANCE`, `VO2_MAX`, `BODY_TEMPERATURE`). `APPLE_HEALTH` joins `MeasurementSource`. A new `SleepStage` enum + a nullable `Measurement.sleepStage` column scoped by CHECK constraint to `SLEEP_DURATION` rows. Sleep is now persisted in minutes instead of hours. Composite unique index `(user_id, type, source, external_id)` becomes the Apple Health dedup key — the legacy `(user_id, type, measured_at, source)` index stays untouched. Migration `0036_apple_health_measurement_types` is strictly additive: no row mutations, no rename. `POST /api/measurements/batch` accepts ≤500 entries per call, wraps `withIdempotency()`, returns per-entry `inserted | duplicate | skipped` status with a typed reason, and is idempotency-replay safe. Sleep-stage aggregation in `/api/analytics` rolls multi-stage nights into one Berlin-day datapoint with a per-stage breakdown over the trailing 30 days. Apple Health source chip renders on both the desktop list and the mobile card variant.
|
||||
|
||||
### Wave 3 — APNs scaffolding
|
||||
|
||||
`@parse/node-apn@8.1.0` joins the senders cascade as channel-type 4. Cascade order is now an explicit `channelPriority()` sort (APNs → Telegram → ntfy → Web Push, unknowns last) so a Postgres scan-order change can't reorder delivery between deploys. Provider is lazily-initialised per gateway (`sandbox` vs `production`), JWT auto-rotates inside the library. Permanent failures (`Unregistered`, `BadDeviceToken`, `DeviceTokenNotForTopic`) drop the dead `Device` row mirroring the web-push 410 cleanup. `Device` model gains nullable `apnsToken` + `apnsEnvironment` columns with a paired CHECK constraint (either both set or both null) plus a partial unique index on `apns_token` for defence-in-depth. `POST /api/devices` accepts paired `apnsToken` + `apnsEnvironment` with a 422 when one comes without the other and a 409 + `apns_token_owned_by_other_user` audit reason on cross-user re-registration. Production without `APNS_KEY_ID` is a no-op rather than a boot failure; partially-set env triggers a single warning at first dispatch.
|
||||
|
||||
### Wave 4 — OpenAPI generator, Coach metric slot, native-auth hardening, Coolify runbook
|
||||
|
||||
The OpenAPI generator emits `docs/api/openapi.yaml` from Zod `.meta()` annotations. Eight iOS-touched routes are registered now: `auth/login`, passkey verify, `auth/refresh`, `measurements` GET + POST + batch, `devices` POST, and the comprehensive insights bundle. `pnpm openapi:check` diffs generated against committed; CI is warn-only for v1.4.23 (`continue-on-error: true`) so a registry oversight on a non-iOS route doesn't red-bar a PR, flips to hard-fail in v1.4.24. The legacy hand-maintained spec stays at `docs/api/openapi-v1422-legacy.yaml` during the incremental migration.
|
||||
|
||||
`PROMPT_VERSION` ratchets 4.22.0 → 4.23.0 with a new GROUND RULE 12 (EN + DE): treat Apple Health categories as silent when the snapshot doesn't carry them. The strict insight schema's `sourceMetric` and `trendAnnotations` enums extend to admit nine additive HealthKit categories (HRV, sleep, resting HR, steps, active energy, flights, distance, VO2 max, body temp). The Coach snapshot pipeline queries the new measurement types only when scope toggles them on, so web-only accounts pay zero extra SQL.
|
||||
|
||||
Refresh-token reuse-detection scopes the blast radius to the originating device — pre-1.4.23 a replay revoked every refresh token the user owned, which is the right call for security but a foot-gun for a two-device household. Legacy null-`deviceId` tokens still fall back to user-wide revoke as a safety hatch. Three new routes round it out: `GET /api/auth/me/devices` lists active devices with label, last-seen, channels, and an `isCurrent` marker keyed off the session's `deviceId` (not the forgeable `X-Device-Id` header). `DELETE /api/auth/me/devices/[id]` revokes one device transactionally: refresh tokens, access tokens, notification channels, push subscriptions, and the row. `DELETE /api/devices/[id]` is the native-friendly mirror the iOS APNs-rotation flow calls. Cross-user attempts return 404 with no enumeration leak.
|
||||
|
||||
The Coolify auto-deploy runbook is captured end-to-end: `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` repo secrets, the "Watch image registry for new digests" UI toggle as the load-bearing piece, a `::notice::` line on the workflow step that surfaces the deploy timestamp + sha for triage, and the host-side retag fallback verbatim. The webhook URL stays in repo secrets — never in a tracked file.
|
||||
|
||||
### Wave 5 — Hygiene
|
||||
|
||||
Seven backlog items from `.planning/v1422-backlog.md` shipped as atomic commits.
|
||||
|
||||
- Sentinel parser malformed-enum hardening (`SentinelParseResult.malformedEntries[]` + per-line typed reasons; the chat route splits `coach.keyvalues.parse_partial` from the full-block `coach.keyvalues.parse_failed`).
|
||||
- Analytics-route unbounded `findMany` replaced with cursor-paged 5 000-row chunks via `fetchBpSeriesChunked`; `analytics.bp_in_target.row_count` wide-event meta added for slow-query attribution; regression test seeds 6 000 rows across a chunk boundary.
|
||||
- `<CoachDrawer key={prefill}>` controlled-prop refactor: `useResettableValue` hook + pure `nextResettableValue` helper land the same UX without weaponising React keys.
|
||||
- Per-user prompt-tuning surface (Coach settings cog returns). `User.coachPrefsJson` migration `0038_coach_prefs`, `GET / PUT /api/auth/me/coach-prefs`, settings cog opens a right-edge `<Sheet>`, system prompt prepends a per-user OVERRIDE, snapshot reads prefs **before** measurement queries so `excludeMetrics` filters before the snapshot lands.
|
||||
- Schema drift on `medication_schedules.days_of_week` resolved — column is referenced by nine source files across four user-visible surfaces; migration `0039_medication_schedule_days_of_week` deploys it (NULL = daily, no backfill needed).
|
||||
- Pearson surfacing gate raised from n≥14 to n≥20 — conservative patch; the rigorous incomplete-beta replacement is queued for v1.4.24.
|
||||
- Coach helpful/unhelpful first-week observation view. Polymorphic `RecommendationFeedback.target_type` migration `0040_recommendation_feedback_target_type`; `POST /api/insights/chat/messages/:id/feedback`; `buildCoachFeedbackBuckets` aggregator slice by (`PROMPT_VERSION`, tone, verbosity); `/admin/coach-feedback` admin page with EN + DE i18n bundle and a sidebar entry.
|
||||
|
||||
### Wave 6 — Multi-agent review + reconcile
|
||||
|
||||
Six reviews ran in parallel (code, security, design/UX, senior-dev, simplify, product-lead). The reconcile pack landed 11 atomic commits across five sessions: one CRITICAL applied, all nine HIGH findings applied, four of five simplify findings applied, three MEDIUM findings applied, all LOW findings triaged into `.planning/v1423-backlog.md`. The headline reconcile work: admin coach-feedback sidebar surface restored, APNs `NotificationChannel` auto-upsert on registration, partial unique index for global `apns_token` uniqueness, Apple Health source badge on the mobile card variant, Coach prefs skeleton + save toast, device revoke wrapped in a single transaction, `isCurrent` marker keyed off the session's `deviceId` rather than the forgeable header, Coach feedback FK migrated to reference `coach_messages` directly (the plaintext `content` column on `recommendation_feedback` is gone — encryption-at-rest is back to being the only on-disk form), the Coolify runbook URL scrubbed from tracked files, and the sentinel partial-malformed annotation work landing in the W5 H1 commit. Test deltas: 2191 → 2236 unit (+45 across the W5/W6 arc), 100 integration (with two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock failures that predate v1.4.23 and are documented in the backlog).
|
||||
|
||||
### Wave 7 — Release
|
||||
|
||||
Pre-release verify (typecheck / lint / openapi:check / 2236 unit / 110 of 112 integration — the two failures are the pre-existing `coach-prefs.test.ts` carryover, not a v1.4.23 regression). `package.json` bumped 1.4.22 → 1.4.23. CHANGELOG entry covering Added / Changed / Fixed / Security / Refactor / Deferred-to-v1.4.24 / Deferred-to-v1.5. `chore(release): v1.4.23` on `develop`. `Release v1.4.23` no-fast-forward merge on `main`. Tag `v1.4.23` pushed. GHCR build green.
|
||||
|
||||
The Coolify auto-deploy fired with `force=true` via the MCP API call and reported "finished" in 18 seconds — the same `:latest` digest-cache fault that hit v1.4.20 and v1.4.21. The CI workflow logged `COOLIFY_WEBHOOK or COOLIFY_TOKEN secret missing — skipping auto-deploy` (the same maintainer-action gap the v1.4.22 brief flagged for v1.4.23 prep), so the auto-deploy path didn't even attempt to run. The host-side fallback was the recipe captured in `.planning/coolify-auto-deploy-howto.md`: `docker pull ghcr.io/mbombeck/healthlog:1.4.23` on `apps-01` (digest `sha256:b20c25a49b…`), `docker tag …:1.4.23 …:latest`, `docker compose up -d --force-recreate app` in `/data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss`. Container recreated cleanly, health check turned green inside ~30 seconds, `/api/version` flipped to 1.4.23 at 2026-05-11T06:40:49Z.
|
||||
|
||||
## iOS readiness — what v1.4.23 gives the iOS app on day zero
|
||||
|
||||
Every server-side contract the v1.5 P1 plan assumed is now locked in `main`:
|
||||
|
||||
- `POST /api/auth/login` with `X-Client-Type: native` returns the bearer + refresh + `deviceId` tuple.
|
||||
- `POST /api/auth/refresh` accepting `hlr_*` and returning a fresh pair, with per-device reuse-detection scoping the replay-blast radius.
|
||||
- `POST /api/devices` accepting `apnsToken` + `apnsEnvironment` with the cross-user-hijack guard duplicated at the APNs-token layer.
|
||||
- `GET /api/auth/me/devices` for the iOS Settings → Devices tab.
|
||||
- `DELETE /api/devices/[id]` for the iOS APNs-rotation cleanup path.
|
||||
- `POST /api/measurements/batch` for the Apple Health sync (P2 itself, but contract is locked in v1.4.23).
|
||||
- `GET /api/measurements?from=&to=&type=` — already shipped in v1.4 for the web app, unchanged.
|
||||
- `docs/api/openapi.yaml` regenerates byte-identically from `src/lib/openapi/routes.ts` and covers all eight P1-touched routes. The iOS DTO codegen has a stable target.
|
||||
|
||||
Thirteen open iOS-DTO questions were consolidated in `.planning/phase-W4-v1423-report.md` (lines 137-199) and routed across v1.5 phases in `.planning/phase-W6-v1423-product-lead-review.md` section C. P1 owns three (APNs token hex wire format, `apnsEnvironment` mapping at `#if DEBUG` site, `X-Device-Id` header on `/api/auth/me/devices`); P2 owns five (the `HEALTHKIT` → `APPLE_HEALTH` rename on the iOS DTO, `externalId` shape, `sleepStage` numeric codepoint, no pre-conversion on the wire, unknown-identifier park-for-retry behaviour); P3 owns four (multi-device cascade behaviour, `collapseId` shape, device-list channel rendering, refresh + device-deletion race handling); P4 owns one (token rotation cadence). The iOS Swift code lands answers as it lands; nothing on the server side blocks the answer.
|
||||
|
||||
Server-side files touched in v1.5 P1: zero. The whole P1 wave is iOS Swift work against a stable server contract.
|
||||
|
||||
## Quality
|
||||
|
||||
- Two format-sweep + planning commits on `develop` (`f1e6630` prettier sweep, `766a9ae` W1 research + W5/W6 review packs), one OpenAPI regen (`4183552`), one release-prep (`d2331d9 chore(release): v1.4.23`)
|
||||
- Release-merge `0dc0e16 Release v1.4.23` on `main`
|
||||
- W2/W3/W4/W5 produced 28 atomic commits across `develop` between v1.4.22 tag and the release-prep commit
|
||||
- W6 reconcile produced 11 atomic commits across `develop` (one CRITICAL, all nine HIGH, four simplify, three MEDIUM)
|
||||
- Tests: 2191 → 2236 unit (+45 across the W5/W6 arc), 110 / 112 integration with the two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock failures carried from v1.4.22. Typecheck clean, lint baseline (21 warnings, no new — none were new in this release), `openapi:check` clean.
|
||||
|
||||
## Branch model
|
||||
|
||||
`develop` is the daily target; `main` is release-only and follows tags. v1.4.23 is the fourth tag through the `develop` → `main` release-merge model since it landed in v1.4.20 F1. Future GHCR builds and Coolify auto-deploy fire only on `main`. The Coolify "Watch image registry for new digests" UI toggle is the load-bearing piece — verified on this release, runbook captured in `.planning/coolify-auto-deploy-howto.md`.
|
||||
|
||||
## Carry-overs
|
||||
|
||||
- **Coolify auto-deploy still needs maintainer action.** Third release in a row that landed via the host-side retag fallback because the `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` repo secrets aren't set (and even when the MCP API fires `force=true`, the `:latest` digest in the local Docker cache pins the previous version). The W4 F8 runbook + the `::notice::` line on the workflow now make the failure mode visible; the durable fix is the two secrets + the Coolify "Watch image registry for new digests" UI toggle. Documented in `.planning/coolify-auto-deploy-howto.md`.
|
||||
- **`coach-prefs.test.ts` integration `NextRequest` URL mock regression** — pre-existing failure that predates v1.4.23 (surfaced during W6 Session A; reproduces with reconcile changes stashed). Two test cases fail; non-blocking for the release. Tracked in `.planning/v1423-backlog.md` under "Test infrastructure" — needs investigation into how `NextRequest`'s URL parsing interacts with the test harness's mocked `cookies()`.
|
||||
- **Pearson incomplete-beta replacement** — v1.4.23 W5 H6 raised `MIN_PAIRED_N` from 14 → 20 as a conservative surfacing-gate fix. The rigorous replacement is the v1.4.24 candidate and should land before correlation auto-discovery widens in v1.5/v1.6.
|
||||
- **OpenAPI drift gate is warn-only for v1.4.23.** Generator covers ~880 lines vs the legacy hand-maintained spec's 5 468. Flip to hard-fail in v1.4.24 and complete registry coverage in parallel; the risk window is "every PR between v1.4.23 and v1.4.24" — pull v1.4.24 forward if the iOS DTO codegen starts compiling against the spec sooner than expected.
|
||||
- **Settings-cog vs per-message-controls debate.** Design pushback during the W6 review raised concern that the dual surface duplicates intent. Defer to v1.4.24 once the first-week thumbs data shows whether per-user prompt prefs drift from per-message ratings.
|
||||
- **The remaining security MEDs + all LOWs** — listed in `.planning/v1423-backlog.md` lines 26-54. Intra-batch dedup accounting, idempotency 422 retry hint on the OpenAPI 422 description, APNs key-file path redaction in wide-event meta, refresh-token failure audit `userId` extension. None are user-facing; all flagged for v1.4.24.
|
||||
|
||||
## Strategic next
|
||||
|
||||
v1.5 P1 (iOS first launch — login + dashboard + widget) is now a ~5-day iOS Swift sprint against locked server contracts. The detailed P1 plan with concrete iOS file paths lives in `.planning/phase-W6-v1423-product-lead-review.md` section C. P2 (Apple Health sync) is ~5 days of iOS work on `HealthKitService` + `SyncCoordinator` against `POST /api/measurements/batch`. P3 (Coach extended for HRV / Sleep / Resting HR / Steps) ratchets `PROMPT_VERSION` 4.23.0 → 5.0.0 and copies the web `<CoachDrawer>` UX to iOS. P4 (per-metric APNs alerts) leans on the W3 scaffolding for the send-side. P5 (web polish + v1.5 release brief) is unchanged from the v1.4.22 plan — the Insights page split now has a concrete sub-tree map because v1.4.23 froze the schema underneath it. P6 (cross-user feedback aggregation cron) becomes "wire the existing aggregator to a daily `pg-boss` schedule + append OMIT/REPHRASE rules to `PROMPT_VERSION` when a bucket's helpful-rate drops below 50%" — the aggregator code already exists in v1.4.23.
|
||||
|
||||
GitHub release: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.23
|
||||
@@ -1,463 +0,0 @@
|
||||
# HealthLog v1.4.25 — release summary
|
||||
|
||||
## Release brief
|
||||
|
||||
v1.4.25 is the largest feature delta in the v1.4.x line and the first time the
|
||||
loud / polish / foundation three-beat ran through a multi-cluster
|
||||
expansion on top of the v1.4.20-introduced develop-to-main release-merge model.
|
||||
Insights expands from a single mother page into seven dedicated metric
|
||||
sub-pages with a shared tab strip; GLP-1 medication tracking lands
|
||||
end-to-end across the dashboard tile, the weight-chart marker overlay,
|
||||
the therapy timeline, the plateau-detection rule in the daily briefing,
|
||||
the dedicated doctor-report section, a Research-Mode drug-level chart
|
||||
behind an MDR acknowledgment dialog, an EMA-sourced drug knowledge layer,
|
||||
the EMA titration ladder, cadence visualisation, compliance chips, a
|
||||
21-entry by 5-category side-effect taxonomy, and a pen-and-vial
|
||||
inventory with a 30-day in-use clock; the cross-source priority resolver
|
||||
becomes a two-axis metric × deviceType lookup with a per-user Settings
|
||||
surface; per-user timezone threads through ten analytics and presentation
|
||||
surfaces; Withings coverage roughly doubles with twelve new measurement
|
||||
types, webhook-driven BP / temperature ingestion, plus the new Activity
|
||||
sync (steps + distance + active-energy) and Sleep v2 sync (stage-level
|
||||
segments); the onboarding flow rebuilds as a nested-route wizard with a
|
||||
welcome carousel, goals chip-picker, source selection grid, baseline
|
||||
form, and a welcome-back resume banner; Personal Records ship end-to-end
|
||||
with a pg-boss detection worker, push opt-in, and a metric-trend badge;
|
||||
Health Score gains a per-component provenance accordion; the Coach runs
|
||||
on native first-party prompts across all six locales bound by a
|
||||
1800-assertion refusal-probe matrix on every push; the OpenAPI drift gate
|
||||
flips to hard-fail so the v1.5 iOS DTO codegen can rely on
|
||||
`docs/api/openapi.yaml` being authoritative; and the GHCR image is now
|
||||
multi-arch so Apple Silicon Macs and arm64 clouds pull native.
|
||||
Migrations 0043–0060 are additive and forward-only. `PROMPT_VERSION`
|
||||
4.24.0 → 4.25.0 carries GROUND RULE 9 (Coach refuses GLP-1 dose
|
||||
recommendations) and GROUND RULE 15 (Coach refuses drug-level estimates
|
||||
with explicit EU MDR 2017/745 + MDCG 2021-24 cites in the refusal copy).
|
||||
|
||||
## Live state
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------- |
|
||||
| URL | `https://healthlog.bombeck.io` |
|
||||
| `/api/version` | `1.4.25` |
|
||||
| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.25 |
|
||||
| Branch model | `develop` → `main` release-merge model, sixth tag through it (v1.4.20 onwards) |
|
||||
| Tag commit | `17b8d8d2 chore(release): v1.4.25` |
|
||||
| Predecessor | `v1.4.24` at `ca03c609` |
|
||||
|
||||
## What shipped
|
||||
|
||||
### Insights expansion — seven dedicated metric sub-pages
|
||||
|
||||
`/insights` becomes a navigation hub. `/insights/blutdruck`,
|
||||
`/insights/gewicht`, `/insights/puls`, `/insights/stimmung`,
|
||||
`/insights/medikamente`, `/insights/bmi` and `/insights/schlaf` each
|
||||
render the metric's full chart, range bands, trend annotations and
|
||||
correlation rows beneath the mother-page hero. Tab strip lifts above
|
||||
each chart for cross-metric scanning. Targets (`/insights/zielwerte`)
|
||||
redesigns around a new `<TargetCard>` primitive with a conditional
|
||||
Coach-handoff card that hides cleanly in rule-based mode. Sleep
|
||||
sub-page renders awake / REM / core / deep as a stacked column over
|
||||
the last 7 / 14 / 30 nights, sourced from `Measurement.sleepStage`.
|
||||
|
||||
### GLP-1 medication tracking — end-to-end across ten surfaces
|
||||
|
||||
`Medication.treatmentClass` enum (`GLP1`, `STANDARD`),
|
||||
`MedicationDoseChange` history table, an `InjectionSite` enum with
|
||||
eight site values, and a body-map picker that proposes the next
|
||||
rotation site from the last fourteen days. Dashboard tile carries
|
||||
next-injection schedule plus week-over-week weight delta. Weight chart
|
||||
gains vertical injection markers. Therapy timeline plots titration
|
||||
alongside the weight trace. Plateau-detection in `/api/insights/briefing`
|
||||
flags four-week stalls with referral framing. Doctor-report PDF gains
|
||||
a dedicated GLP-1 section. Drug-level chart on the medication detail
|
||||
page renders behind a Research-Mode gate; the acknowledgment dialog
|
||||
cites EU MDR 2017/745 + MDCG 2021-24, EMA EPAR and Schneck / Urva 2024
|
||||
before the user can opt in. EMA titration ladder helpers walk the
|
||||
sourced schedule and frame as reference-not-advice. Cadence + compliance
|
||||
helpers expand the schedule into expected slots, pair each with the
|
||||
closest actual intake, and render a timeline + chip strip; honour
|
||||
per-user timezone so cross-midnight doses bucket correctly regardless
|
||||
of locale. Side-effect taxonomy (Migration 0059) covers 21 entries × 5
|
||||
categories, severity Likert 1-5, with `category` derived server-side so
|
||||
the taxonomy cannot drift between client and server. Pen-and-vial
|
||||
inventory (Migration 0056) carries a SEALED → IN_USE → EXPIRED state
|
||||
machine with a 30-day in-use clock from `markAsFirstUseAt`; a daily
|
||||
03:00 cron flips expired in-use items in one `updateMany`. State
|
||||
machine re-runs on every PATCH so a back-dated first-use immediately
|
||||
moves a stale pen to EXPIRED.
|
||||
|
||||
### Cross-source priority — two-axis architecture
|
||||
|
||||
`User.sourcePriorityJson` (Migration 0048) stores the per-user resolver:
|
||||
a single-axis ladder by metric type plus an optional per-device-type
|
||||
override. `pickCanonicalSource()` walks the metric axis first; a
|
||||
per-device override (e.g. "BP from Withings BPM Connect, weight from
|
||||
Withings Body+") wins within that ladder. Defaults: cumulative + sleep +
|
||||
HRV + RHR favour Apple Health → Withings → manual; point measurements
|
||||
favour Withings → Apple Health → manual. The `__default__` sentinel is
|
||||
retired in favour of a null bucket. Settings → Sources screen exposes
|
||||
the ladder for drag-reorder per metric and per-device override picker.
|
||||
|
||||
### Per-user timezone — Option B threaded through ten surfaces
|
||||
|
||||
New `User.timezone` column defaults to `Europe/Berlin`. CSV exporter
|
||||
emits ISO-8601 with offset; formatters honour the user's tz; Profile
|
||||
picker covers all IANA zones; admin sets the default for new accounts;
|
||||
signup detects the browser tz; doctor-report PDF dates use the user's
|
||||
tz; chart x-axes take a `timezone` prop; Coach snapshot timestamps
|
||||
land in the user's tz; `MoodEntry.tz` records local-day grouping for
|
||||
weekday correlation (Migration 0044); the weight-weekday correlator
|
||||
buckets in the user's tz. Cadence + compliance helpers honour the
|
||||
same argument.
|
||||
|
||||
### Withings — expanded coverage + Activity + Sleep v2
|
||||
|
||||
Twelve new `MeasurementType` values (HRV, body temperature, SpO2 v2,
|
||||
VO2 max, fat-free mass, fat mass, muscle mass, skin temperature,
|
||||
pulse-wave velocity, vascular age, visceral fat — Migration 0049).
|
||||
Webhook coverage extended to BP and temperature so the latency
|
||||
collapses from ~1 h polling to seconds. OAuth scope upgraded to include
|
||||
`user.activity` with an in-app reconnect banner. Activity sync calls
|
||||
`getactivity`, ingests one row per day, anchors at noon UTC so
|
||||
positive-offset users bucket cleanly into their local day. Sleep v2
|
||||
sync calls `sleepv2_get` and writes per-night stage segments through
|
||||
the new `sleepStage` composite (Migration 0055 widens the Measurement
|
||||
unique index to include `sleep_stage` with `NULLS NOT DISTINCT`).
|
||||
Backed by pg-boss `activity-sync` and `sleep-v2-sync` queues plus
|
||||
webhook enqueue hooks on the new subscription channels.
|
||||
|
||||
### Onboarding rebuild — six-step nested-route wizard
|
||||
|
||||
`/onboarding/[step]` powered by a new `<OnboardingShell>` primitive:
|
||||
value-prop carousel, goals chip-picker, source 4-card selection grid
|
||||
(Apple Health marked coming-soon), baseline form, source-connect step,
|
||||
done step. `User.onboardingStep` (Migration 0057, with Migration 0060
|
||||
backfilling and flipping the column to `NOT NULL DEFAULT 0`) drives
|
||||
resume-state. A welcome-back banner surfaces on the entry page when
|
||||
an in-progress wizard is detected. `POST /api/onboarding/step`
|
||||
conditions the update on `{ id, onboardingStep: current,
|
||||
onboardingCompletedAt: null }` and returns 409 on concurrent advance
|
||||
to close the read-then-write race.
|
||||
|
||||
### Personal Records — schema, worker, badge, push opt-in
|
||||
|
||||
Migration 0054 introduces the `PersonalRecord` table +
|
||||
`PersonalRecordDirection` helper. `GET /api/personal-records` clamps
|
||||
`?limit` (default twenty-five, max two hundred). A pg-boss worker
|
||||
sweeps MAX / MIN per metric and the workout slots on every batch-ingest
|
||||
plus a thirty-minute fallback cron (concurrency five; warmup gate so
|
||||
the very first datapoint per metric does not promote itself).
|
||||
Metric-trend tiles render a PR badge when the record landed in the
|
||||
last thirty days, with WCAG-AA contrast in dark mode. A per-user push
|
||||
opt-in toggle (default off) wires into the existing dispatcher cascade.
|
||||
|
||||
### Workouts — schema + typed batch-ingest endpoint
|
||||
|
||||
Migration 0053 introduces `Workout` + `WorkoutRoute` tables; the Zod
|
||||
boundary in `src/lib/validations/workout.ts` covers a twenty-member
|
||||
sport-type union and a `GeoJSON LineString` route column in JSONB.
|
||||
`POST /api/workouts/batch` accepts up to five hundred rows per call,
|
||||
wraps `withIdempotency()`, returns the same `inserted | duplicate |
|
||||
skipped` envelope as the measurements batch, and reconciles inserted
|
||||
vs duplicate counts correctly under contention. Rate-limited,
|
||||
size-capped.
|
||||
|
||||
### Coach — native first-party prompts across six locales
|
||||
|
||||
Coach + insights system prompts move from EN body + `REPLY LANGUAGE`
|
||||
footer to native per-locale bodies. The safety contract matrix lives
|
||||
as YAML per locale (single source of truth across drafting + tests).
|
||||
GROUND RULE 9 forbids GLP-1 dose recommendations; GROUND RULE 15
|
||||
forbids drug-level estimates and cites EU MDR 2017/745 + MDCG 2021-24
|
||||
in the refusal copy. The refusal-probe matrix in CI exercises 15
|
||||
GROUND RULES across six locales with 20+ adversarial paraphrasings
|
||||
each — 1800+ assertions on every push. Drift-guard tests pin the
|
||||
YAML matrix in lockstep with the validator-derived enums.
|
||||
|
||||
### OpenAPI drift gate — hard-fail flip
|
||||
|
||||
`pnpm openapi:check` CI step now red-bars a PR on any drift between
|
||||
the Zod registry and `docs/api/openapi.yaml`. The v1.4.23 warn-only
|
||||
window closes; iOS DTO codegen can rely on the spec being
|
||||
authoritative.
|
||||
|
||||
### Multi-arch GHCR image + Coolify auto-deploy toggle
|
||||
|
||||
GHCR publish workflow builds `linux/amd64` on `ubuntu-latest` plus
|
||||
`linux/arm64` on `ubuntu-24.04-arm` and merges the manifest. Apple
|
||||
Silicon Macs and arm64 clouds pull native. The Coolify auto-deploy
|
||||
gate is now an explicit `vars.COOLIFY_AUTO_DEPLOY=true|false`
|
||||
maintainer toggle in the deploy workflow instead of the implicit
|
||||
secret-presence gate that silently no-op'd in v1.4.21-23.
|
||||
|
||||
### Security cluster
|
||||
|
||||
`WITHINGS_WEBHOOK_SECRET` no longer lands as a URL path-segment in
|
||||
`http.path` on every Wide Event. The logging stack now carries a
|
||||
parameterised `PATH_SECRET_PATHS` registry seeded with the
|
||||
`/api/withings/webhook/[token]` shape; `WideEventBuilder.setHttp`
|
||||
rewrites path + route segments to `[REDACTED]` before they reach
|
||||
stdout / the in-memory ring buffer / Loki. Existing query-string
|
||||
redaction is unaffected. `POST /api/medications/[id]/glp1` now parses
|
||||
through bounded Zod schemas, enforces length caps on notes,
|
||||
finite-number guards on dose value, bounded `effectiveFrom`, writes
|
||||
an `auditLog` row on every change and inherits the 30-per-minute-per-
|
||||
user rate limit from the rest of the medication surface.
|
||||
Batch-ingest endpoints (`POST /api/measurements/batch`,
|
||||
`POST /api/workouts/batch`, `DELETE /api/measurements/by-external-ids`)
|
||||
carry a sixty-per-minute-per-user token-bucket rate limit. Source-
|
||||
priority and doctor-report-prefs PUTs emit `AuditLog` rows.
|
||||
|
||||
## Migrations
|
||||
|
||||
| Migration | Purpose |
|
||||
|---|---|
|
||||
| 0051 `measurement_device_type` | Adds `deviceType` enum + column to `Measurement` for the two-axis source-priority resolver. |
|
||||
| 0052 `apple_health_enum_extensions` | Adds `ENVIRONMENT_AUDIO_EXPOSURE`, `HEADPHONE_AUDIO_EXPOSURE` and `TIME_IN_DAYLIGHT` to `MeasurementType`. |
|
||||
| 0053 `workout_and_route` | Introduces `Workout` and `WorkoutRoute` tables for the typed workout-batch ingest endpoint and the v1.5 iOS workout sync. |
|
||||
| 0054 `personal_record` | `PersonalRecord` table + `PersonalRecordDirection` helper. Unique index is `NULLS DISTINCT` by convention; the detection worker carries the application-level idempotency guard. |
|
||||
| 0055 `measurement_sleepstage_composite` | Widens the `Measurement` unique index to include `sleep_stage` with `NULLS NOT DISTINCT` (Postgres 16 native) so Withings sleep v2 segments dedup correctly. |
|
||||
| 0056 `medication_inventory_item` | `MedicationInventoryItem` table for the GLP-1 pen/vial inventory with the SEALED → IN_USE → EXPIRED state machine. |
|
||||
| 0057 `user_onboarding_step` | Adds `onboardingStep` nullable to `User` for the wizard resume-state. Originally nullable; superseded by Migration 0060. |
|
||||
| 0058 `user_research_mode` | Adds `researchModeAck*` columns to `User` for the GLP-1 Research-Mode MDR acknowledgment. |
|
||||
| 0059 `medication_side_effect` | `MedicationSideEffect` table for the 21-entry × 5-category taxonomy. |
|
||||
| 0060 `onboarding_step_not_null` | Backfills existing `User.onboardingStep` rows to 0 and flips the column to `NOT NULL DEFAULT 0`. Closes the tri-state ambiguity left by 0057. |
|
||||
|
||||
All ten are additive and forward-only. Migration 0057's comment was
|
||||
rewritten to match PG11+ fast-path `ADD COLUMN DEFAULT` semantics
|
||||
during the reconcile pass.
|
||||
|
||||
## Test coverage delta
|
||||
|
||||
| Suite | v1.4.24 | v1.4.25 | Delta |
|
||||
|---|---|---|---|
|
||||
| Unit | 2 244 | 3 828 (across 344 files) | +1 584 |
|
||||
| Integration | 140 | ~170 (across 11 files) | +30 |
|
||||
|
||||
The unit-test growth is dominated by the Coach refusal-probe matrix
|
||||
(1800+ assertions across 15 GROUND RULES × 6 locales × 20+ paraphrasings)
|
||||
plus the feature-cluster helper suites: GLP-1 plateau-detection text formatter;
|
||||
two-axis source-priority resolution; source-priority Settings reducer;
|
||||
medication-card GLP-1 variant + injection-site-picker; doctor-report
|
||||
prefs PUT contract; personal-records pagination clamp; `berlinIsoWeekday`
|
||||
timezone-aware suite; sleep-stage chart i18n drift-guard; Health-Score
|
||||
provenance i18n drift-guard; api-handler private-field crash regression;
|
||||
pure cadence + compliance helpers with TZ assertions across UTC+0,
|
||||
UTC+9, UTC-8; side-effect taxonomy drift-guard; EMA titration ladder
|
||||
helpers; pen-inventory state-machine + 30-day clock helpers; PR
|
||||
detection worker (MAX / MIN per metric + workout slots + warmup gate
|
||||
+ null-slot dup regression + zero-duration guard); Withings activity
|
||||
TZ regression across Berlin / Los Angeles / Tokyo / Auckland; Withings
|
||||
sleep v2 segment ingest; log-redaction path-segment rule + `setHttp`
|
||||
rewrite; GLP-1 endpoint Zod + audit + rate-limit; onboarding step
|
||||
concurrent-advance regression; inventory PATCH state-machine back-dated
|
||||
first-use regression; bulk-update contract for the expire-stale cron;
|
||||
brand-name guard + sentinel-preservation + safety-contract parity for
|
||||
the YAML matrix; locale auto-discovery + fallback-chain runtime guard.
|
||||
|
||||
Integration: two-axis canonical-source resolution end-to-end;
|
||||
`requireAuth()` narrow-scope on unscoped route; batch-ingest race
|
||||
reconciliation under contention; workout-batch race + size-cap + ingest
|
||||
end-to-end; PR-detection end-to-end; cross-source priority for
|
||||
Withings Activity + Apple Health; Withings sleep-stage composite ingest;
|
||||
per-user timezone end-to-end (Pacific/Auckland).
|
||||
|
||||
e2e green on the W2 CI fix (coach-prefs URL mock + Pixel-5 selector
|
||||
hardening) plus the Fix-I hot-fix that re-anchored the dashboard
|
||||
insight-card and the mobile x-axis tick locators class-independently.
|
||||
Typecheck clean. Lint baseline carried (no new warnings). OpenAPI
|
||||
drift-check clean against the regenerated spec.
|
||||
|
||||
## Code quality gates
|
||||
|
||||
The W21 review ran eight read-only reviewers in parallel: code,
|
||||
security, design, senior-dev, simplifier, product-lead, dead-code,
|
||||
i18n-runtime. Combined output was 103 findings (2 Critical, 20 High,
|
||||
41 Medium, 40 Low) plus a strategic product-lead assessment.
|
||||
|
||||
**Critical (2 / 2 applied)**: sec-C1 (Withings webhook secret leaking
|
||||
into `http.path` on every Wide Event) and product-lead-C1 (CHANGELOG
|
||||
frozen at the initial release-prep surface, omitting the bulk of the
|
||||
later feature work). Both closed before tag — sec-C1 via Fix-J
|
||||
(`PATH_SECRET_PATHS` registry + `setHttp` redaction);
|
||||
product-lead-C1 via the W22 release-redo editorial pass.
|
||||
|
||||
**High (20 / 20 applied)**: sec-H1 (GLP-1 endpoint missing Zod + audit
|
||||
+ rate-limit) closed in Fix-K. Six design-Highs (onboarding tap targets,
|
||||
range-bar palette, PR-badge contrast) closed in Fix-L. Two code-Highs
|
||||
(inventory state machine bypass on PATCH, per-row expire loop) closed
|
||||
in Fix-M. Seven simplifier-Highs (`readError` dup, `templateFill`,
|
||||
`statusLabel` nested ternary, `ResearchModeStatus` shared type,
|
||||
`findDrugIdByBrand`, `assertMedicationOwnership` across 11 routes,
|
||||
`MedicationInventoryState` import) closed in Fix-N. Code-H2 (cadence +
|
||||
compliance ignore per-user timezone) closed in Fix-O. Dead-H4
|
||||
(research-mode-staleness module never wired) resolved by deletion in
|
||||
Fix-O. One product-lead-H1 (test count footer stale) and
|
||||
product-lead-H2 (Deferred-to-v1.4.26 section lists already-shipped
|
||||
items) closed in W22.
|
||||
|
||||
**Medium (17 of 41 applied, 24 deferred to v1.4.26)**: 17 ship-now
|
||||
Mediums landed across Fix-J through Fix-O; the deferred 24 split
|
||||
between schema-shape decisions (`User.onboardingGoals` column;
|
||||
`/api/audit-log` consumer decision), module-surface narrowing
|
||||
(`glp1-pk.ts`, `glp1-knowledge.ts`, `scheduling/*` unused exports),
|
||||
148 remaining dead i18n keys after the W15 380-key sweep, and seven
|
||||
orphan endpoint go / no-go decisions that need maintainer call. Full
|
||||
deferrals matrix lives in `.planning/phase-W21-reconcile-plan.md` lines
|
||||
220-265.
|
||||
|
||||
**Low (40 / 40 deferred to v1.4.27)**: appendix in the reconcile plan
|
||||
covers them — none are user-facing; one (i18n-Low-1, e2e
|
||||
`healthlog_locale` vs `healthlog-locale` cookie typo) was
|
||||
opportunistically picked up in Fix-P because the touch was a single
|
||||
character.
|
||||
|
||||
Seven touch-disjoint Fix-J through Fix-P surfaces totalling 22 atomic
|
||||
commits ran across `develop` between the W21 audit and the W22
|
||||
release-prep. Three sequenced merge windows preserved touch-disjoint
|
||||
diffs without conflict (Fix-O before Fix-N on the cadence route;
|
||||
Fix-L before Fix-N on the onboarding components; Fix-M before Fix-O
|
||||
on the PR worker test). No file had two agents editing the same lines.
|
||||
|
||||
## Strategic posture
|
||||
|
||||
### v1.5 iOS readiness
|
||||
|
||||
The iOS sprint has the contracts it needs to start Day 1. Locked
|
||||
server contracts after v1.4.25:
|
||||
|
||||
- `POST /api/measurements/batch` — Apple Health bulk ingest with
|
||||
`inserted | duplicate | skipped` per-entry envelope, idempotency
|
||||
wrapper, rate-limited (v1.4.23, unchanged here).
|
||||
- `DELETE /api/measurements/by-external-ids` — HealthKit deletion-sync
|
||||
(new in v1.4.25).
|
||||
- `POST /api/workouts/batch` — HKWorkout-aligned typed ingest with
|
||||
20-sport-type union + GeoJSON LineString routes (new in v1.4.25).
|
||||
- `POST /api/devices` — APNs token registration with paired
|
||||
`apnsEnvironment` (v1.4.23, unchanged).
|
||||
- `POST /api/onboarding/step` — wizard progression with rate limit +
|
||||
audit + 409-on-concurrent (new in v1.4.25).
|
||||
- `GET /api/personal-records?metricType=X` — paginated PR reads.
|
||||
- `User.sourcePriorityJson` two-axis resolver with
|
||||
`pickCanonicalSource()` — iOS sets Apple Health as the cumulative +
|
||||
sleep + HRV + RHR favourite; Withings stays the point-measurement
|
||||
default.
|
||||
- `Measurement.deviceType` enum with iOS-17/18 HK long-tail mappings
|
||||
ready to extend.
|
||||
- `MedicationInventoryItem` — pen/vial 30-day clock available for an
|
||||
iOS pen-scanner integration.
|
||||
- `Workout` + `WorkoutRoute` tables (the table-side; endpoint signature
|
||||
finalised during the iOS sprint).
|
||||
- `docs/api/openapi.yaml` regenerates byte-identically from the Zod
|
||||
registry; the drift gate is now hard-fail.
|
||||
|
||||
### MDR boundary preservation
|
||||
|
||||
The GLP-1 Research-Mode surface ships with three concentric defences:
|
||||
chart gating (`User.researchModeAck*` columns require an active
|
||||
acknowledgment before render), dialog acknowledgment (cites EU MDR
|
||||
2017/745, MDCG 2021-24, EMA EPAR and Schneck / Urva 2024 before the
|
||||
user can opt in; version-bump re-prompts), and Coach refusal at the
|
||||
prompt layer (GROUND RULE 15 across all six locales, refusal-probe
|
||||
matrix in CI). The titration ladder display is framed as
|
||||
reference-not-advice and bound by the same GROUND RULE 15. Drug-level
|
||||
chart-side 90-day staleness wiring stayed deferred for v1.4.26 (the
|
||||
helper module shipped tested in W19c-Safety but the chart conditional
|
||||
isn't wired — the unused helper was deleted during Fix-O after the
|
||||
reconcile pass; the 90-day gate becomes a clean v1.4.26 surface when
|
||||
the maintainer decides to wire it).
|
||||
|
||||
### Branch model
|
||||
|
||||
`develop` is the daily target; `main` is release-only and follows tags.
|
||||
v1.4.25 is the sixth tag through the develop-to-main release-merge model
|
||||
introduced in v1.4.20 F1. GHCR auto-deploy fires only on `main`; the
|
||||
explicit `vars.COOLIFY_AUTO_DEPLOY=true|false` toggle closes the
|
||||
silent-miss pattern that hit v1.4.21 / v1.4.22 / v1.4.23.
|
||||
|
||||
## 0-10 score per area
|
||||
|
||||
| Area | Score | Note |
|
||||
|---|---|---|
|
||||
| Insights | 9 | Seven-sub-page split lands cleanly; Targets redesign with conditional Coach-handoff is the tightest the layout has been; mother page slims to general status + Coach hero. Half-point off for the duplicate `StatusCard` regression that needed a Fix-I hotfix mid-release. |
|
||||
| Dashboard | 9 | Top-tile polish (single-line headings, inline trend arrow, baseline alignment across tiles) closes the v1.4.24 reviewer note; comparison-overlay default removed in favour of per-chart preference; VO2 max secondary-metric tile lands as opt-in. |
|
||||
| Coach | 9 | Native per-locale prompts replace the REPLY LANGUAGE footer across six locales; GROUND RULE 9 + GROUND RULE 15 close the MDR boundary at the prompt layer; 1800+ refusal-probe assertions in CI on every push. Half-point off for FR/ES/IT/PL prose still being LLM-drafted (structural matrix covers safety; the prose itself benefits from a native human pass — queued for v1.4.26). |
|
||||
| Settings | 8 | Source-priority screen ships with drag-reorder + per-device override + audit-log; doctor-report per-section toggles + mood-default-off lands; Research-Mode toggle with version re-prompt banner lands. Single point off for the settings-cog vs per-message-controls debate (v1.4.23 carryover) still unresolved. |
|
||||
| Admin | 8 | Login overview gains location + provider columns + CSV polish; coach-feedback header layout shift closed; notification-status-card icon parity restored; mobile admin nav gains Overview link. Two points off for the seven orphan endpoint go / no-go decisions still pending a maintainer call. |
|
||||
| Mobile (web) | 8 | 44-px touch-target floor across onboarding wizard, top-bar, section strips and injection-site picker hits WCAG 2.5.5; dashboard tile baseline alignment regression-pinned. Two points off because the e2e workflow needed the Fix-I hot-fix re-anchor on dashboard insight-text + chart-tick locators (now class-independent). |
|
||||
| Tests | 9 | 2 244 → 3 828 unit (+1 584); 140 → ~170 integration; new drift-guards across Apple Health mapping, side-effect taxonomy triangle, sleep-stage chart i18n, Health-Score provenance, safety-contract YAML matrix, plus per-user TZ regressions and the 1800-assertion refusal-probe matrix. Half-point off for the two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock failures still carried from v1.4.23. |
|
||||
| CI | 9 | OpenAPI drift gate flips to hard-fail; refusal-probe matrix runs on every push; multi-arch GHCR builds clean after the Fix-G/H lowercase fix-up; explicit `vars.COOLIFY_AUTO_DEPLOY` toggle closes the silent-miss pattern. Half-point off for the Fix-H lowercase regression that needed catching post-PR-open. |
|
||||
| Docs | 9 | CHANGELOG expand in W22 brings the public artifact in lockstep with what shipped; per-section delta is complete; deferred-to-v1.4.26 / v1.5 sections are accurate. Half-point off because the W22 reconcile was the C-1 Critical of the release and only ran after the W21 product-lead caught it. |
|
||||
| Repo hygiene | 9 | 380 dead i18n keys dropped; dead prompt constants removed; `<InsightsPageHero>` + `<IntakeTimeline>` + `<ComplianceCharts>` + orphan query keys + `/api/insights/general-status` orphan all gone; `MedicationDetailSection` wrapper + shared route-guards / read-error / dose-string / research-mode-types modules collapse 11+ duplications. Half-point off for the 148 remaining dead i18n keys queued for a v1.4.26 second-pass. |
|
||||
| Demo readiness | 8 | Multi-arch image is live so the demo VPS pulls native arm64; W11b demo-deploy report covers the runbook; OpenAPI spec is authoritative for iOS DTO codegen. Two points off because the demo seeding still depends on the manual host-side fallback when Coolify auto-deploy is toggled off, and because the iOS Swift app itself is queued for v1.5 (the demo is web-only until then). |
|
||||
|
||||
## v1.4.26 backlog
|
||||
|
||||
24 deferred Mediums + 40 Lows + maintainer-decision items split across
|
||||
the following themes:
|
||||
|
||||
- `User.onboardingGoals` schema column + server-side persistence
|
||||
(today the goals chip-picker holds the choice client-side only;
|
||||
the dashboard-widgets seed feature reads from it in v1.4.26).
|
||||
- `advance()` hook extraction across the three onboarding step
|
||||
components (simplifier surfaced; deferred to avoid collision with
|
||||
the v1.4.25 touch-target pass).
|
||||
- 148 dead i18n keys remaining after the W15 380-key sweep (need a
|
||||
second-pass call-site verification).
|
||||
- Drug-level chart-side 90-day staleness clock wiring (acknowledgment
|
||||
dialog already re-prompts on version bump and Coach refuses at
|
||||
GROUND RULE 15; the clock is defence-in-depth).
|
||||
- FR / ES / IT / PL Coach + insights prose hand-review by native
|
||||
speakers (structural refusal-probe matrix covers safety; the user-
|
||||
facing prose benefits from a human pass).
|
||||
- Sleep sub-page stacked-column visual polish (stage colours + legend
|
||||
density).
|
||||
- VO2 max chart-row card on `/insights/<metric>` (dashboard tile
|
||||
shipped; chart card queued alongside the iOS body-composition page).
|
||||
- Mood verbal-labels persistence behind a per-user toggle.
|
||||
- Coach `lastYear` baseline + row-tap-to-prefill polish.
|
||||
- Lazy-loaded locale JSON bundles (all six locales import
|
||||
synchronously today, ~675 KB to every client).
|
||||
- iOS-18 long-tail HK identifier mappings (sleep apnea, GAD-7 /
|
||||
PHQ-9, running form, paddle / row / ski distances, pregnancy /
|
||||
cycle, FHIR clinical).
|
||||
- Seven orphan endpoint go / no-go decisions
|
||||
(`/api/admin/ai-settings`, `/api/admin/backup/test`,
|
||||
`/api/admin/status-overview`,
|
||||
`/api/monitoring/{glitchtip,umami}/test`).
|
||||
- `glp1-pk.ts` and `glp1-knowledge.ts` unused-export decisions
|
||||
(internalise vs wire dashboard chip).
|
||||
- Pearson incomplete-beta replacement (v1.4.23 carryover already
|
||||
partially closed with the conservative MIN_PAIRED_N raise — the
|
||||
rigorous replacement is the v1.4.26 candidate before correlation
|
||||
auto-discovery widens in v1.5/v1.6).
|
||||
|
||||
## Carry-overs
|
||||
|
||||
- The two pre-existing `coach-prefs.test.ts` `NextRequest` URL mock
|
||||
failures still carry through from v1.4.23. Tracked in
|
||||
`.planning/v1426-backlog.md`; non-blocking for the release.
|
||||
- Manual-workout route `findFirst` serial pattern stays in place
|
||||
(no manual workout UI today; contract correctness only matters
|
||||
once v1.5 UI lands).
|
||||
- The Coolify auto-deploy MCP API path still fires
|
||||
`force=true` and reports "finished" in ~18 seconds without a real
|
||||
`docker pull`, but the new `vars.COOLIFY_AUTO_DEPLOY` toggle plus
|
||||
the "Watch image registry for new digests" UI checkbox capture the
|
||||
durable fix; the host-side retag fallback recipe lives in
|
||||
`.planning/coolify-auto-deploy-howto.md` for the cases where the
|
||||
toggle is off.
|
||||
|
||||
## Strategic next
|
||||
|
||||
The v1.5 sprint starts with iOS first launch (login + dashboard +
|
||||
widget) against locked server contracts — pure Swift work, zero
|
||||
server-side touches. P2 (Apple Health sync) calls the v1.4.23
|
||||
`POST /api/measurements/batch` against the v1.4.25 device-type +
|
||||
two-axis resolver. P3 (Coach extended for HRV / sleep / resting HR /
|
||||
steps) ratchets `PROMPT_VERSION` 4.25.0 → 5.0.0 and copies the web
|
||||
`<CoachDrawer>` UX to iOS. P4 (per-metric APNs alerts) leans on the
|
||||
v1.4.23 APNs scaffolding for the send-side. P5 (workouts + GeoJSON
|
||||
routes) calls the v1.4.25 `POST /api/workouts/batch` against the
|
||||
shipped Workout + WorkoutRoute tables.
|
||||
@@ -1,109 +0,0 @@
|
||||
# v1.4.27 release summary
|
||||
|
||||
## Overview
|
||||
|
||||
v1.4.27 polishes the settings shell rhythm and lands the offline-first
|
||||
geolocation pipeline that drives the `/admin/login-overview` table.
|
||||
Two MaxMind GeoLite2 databases (`GeoLite2-City` + `GeoLite2-ASN`) are
|
||||
now bundled into the runtime image; the resolver in
|
||||
`src/lib/geo.ts` reads them with `mmdb-lib` and falls back to the
|
||||
existing `ipwho.is` HTTPS path only when the local DB cannot resolve
|
||||
the address.
|
||||
|
||||
## GeoLite2 attribution
|
||||
|
||||
This product includes GeoLite2 data created by MaxMind, available from
|
||||
<https://www.maxmind.com>. The GeoLite2 databases (`GeoLite2-City`
|
||||
and `GeoLite2-ASN`) are distributed under the
|
||||
[Creative Commons Attribution-ShareAlike 4.0 International License][cc-by-sa-4].
|
||||
|
||||
The same attribution renders on the `/about` page so an end user can
|
||||
discover it without inspecting the repository.
|
||||
|
||||
[cc-by-sa-4]: https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
## Operating the offline databases
|
||||
|
||||
The MMDB files are too large to vendor in git. They are downloaded
|
||||
outside the Docker build by `scripts/fetch-geolite2.sh`. The operator
|
||||
runs the script before `docker build` with a free MaxMind license
|
||||
key:
|
||||
|
||||
```
|
||||
MAXMIND_LICENSE_KEY=xxxx ./scripts/fetch-geolite2.sh
|
||||
```
|
||||
|
||||
The script writes `GeoLite2-City.mmdb` and `GeoLite2-ASN.mmdb` to
|
||||
`assets/geolite2/`. The Dockerfile then copies the folder into
|
||||
`/opt/geolite2/` so the runtime resolver finds the databases on disk.
|
||||
If the script is skipped the image builds cleanly, and the resolver
|
||||
falls back to the online `ipwho.is` provider — the v1.4.26 behaviour.
|
||||
|
||||
The offline tier is **optional** as of v1.4.27 R5. When
|
||||
`MAXMIND_LICENSE_KEY` is unset the `Fetch GeoLite2 databases` step in
|
||||
`.github/workflows/docker-publish.yml` emits a `::warning::`, drops an
|
||||
`.empty` marker into `assets/geolite2/`, and continues — the build no
|
||||
longer blocks on a missing secret. The runtime resolver detects the
|
||||
marker on the first public-IP lookup, sends a one-shot admin
|
||||
notification with the GitHub Actions secrets URL, and continues to
|
||||
serve audits from the `ipwho.is` fallback. `/api/version` exposes
|
||||
`offlineGeoEnabled: boolean` so the admin status page can render the
|
||||
state at a glance.
|
||||
|
||||
MaxMind reissues the databases on the first Tuesday of each month;
|
||||
re-run the script before each release to pick up new ASN allocations
|
||||
and city renames.
|
||||
|
||||
## Schema delta
|
||||
|
||||
`AuditLog` grows two nullable columns:
|
||||
|
||||
- `asn Int?` — the autonomous-system number resolved from the
|
||||
`GeoLite2-ASN` DB at audit-creation time.
|
||||
- `carrier String?` — the matching `autonomous_system_organization`
|
||||
string (e.g. `"Deutsche Telekom AG"`).
|
||||
|
||||
Both columns are additive and `IF NOT EXISTS`-guarded so the
|
||||
migration is idempotent on the demo server which may already carry
|
||||
hand-edited columns from a prior partial run.
|
||||
|
||||
## Backfill helper
|
||||
|
||||
`src/lib/jobs/geo-backfill.ts` walks rows where `location IS NULL`
|
||||
and `createdAt > now() - 30 days`, capped at 5 000 rows per pass, and
|
||||
re-resolves them through the offline DB. Useful after a fresh
|
||||
GeoLite2 DB roll so the historical rows that landed during an
|
||||
`ipwho.is` outage finally pick up a city + carrier label.
|
||||
|
||||
## Image-size budget
|
||||
|
||||
The two MMDB files add ~80 MB to the production image. The
|
||||
`tzdata`-bearing Alpine runner stage already sits at ~330 MB
|
||||
compressed; the v1.4.27 image lands well under the 90 MB image-size
|
||||
delta budget set out in the fix plan.
|
||||
|
||||
## Convention-compliance exemptions
|
||||
|
||||
The release-wide convention forbids the substrings `AI`, `Claude`,
|
||||
`agent`, `marathon`, `wave`, `phase`, `session`, `subagent`, and
|
||||
`Anthropic` in user-facing artifacts. Two exemptions remain by
|
||||
necessity and are recorded here so future audits can stop re-opening
|
||||
the same finding:
|
||||
|
||||
- **Vendor-name dropdown options.** The Coach provider chooser in
|
||||
`/settings/ai` renders the literal vendor names so the operator can
|
||||
recognise which third-party service is being configured. The keys
|
||||
`settings.ai.providerOptions.anthropic` and
|
||||
`settings.ai.activeProviderOptions.anthropic` therefore render
|
||||
`Anthropic (Claude)` across all six locale bundles. Rebranding
|
||||
these would break the recognition the dropdown exists to provide.
|
||||
The exemption applies to any vendor label whose product is the
|
||||
setting being chosen, e.g. `OpenAI`, `Anthropic`, `Codex`.
|
||||
- **GitHub repository URL on `/about`.** The source-link on the
|
||||
public about page renders
|
||||
`https://github.com/MBombeck/HealthLog`. The username segment is an
|
||||
irreducible technical identifier for the URL and cannot be hidden
|
||||
without breaking the link. The directive's spirit — never surface
|
||||
the maintainer's full name in prose — is honoured: the `/about`
|
||||
page never spells "Marc" or "Bombeck" in body copy, only inside
|
||||
the URL.
|
||||
@@ -1,432 +0,0 @@
|
||||
# HealthLog v1.4.6 — Findings & Spec
|
||||
|
||||
Status: 2026-05-09 — handoff from the v1.4.5 deploy session. All findings
|
||||
were produced by **four parallel audit passes** (dashboard visual, AI section,
|
||||
admin panel, AI insights data window) against the v1.4.5 codebase
|
||||
(`e8f4820`). A Playwright probe also visually verified the dashboard tile
|
||||
strip at five viewports. **Verify each finding by reading the cited
|
||||
file:line BEFORE editing — automated audits occasionally reference stale state.**
|
||||
|
||||
The single working-tree change carried over from the v1.4.5 session:
|
||||
`src/components/charts/trend-card.tsx:85` — added `w-full` so the tile
|
||||
component fills its grid cell. **Do not commit alone — fold into v1.4.6.**
|
||||
|
||||
---
|
||||
|
||||
## Tier 1 — Release blockers for v1.4.6
|
||||
|
||||
### T1 — Dashboard tile fill ✅ (already in working tree)
|
||||
|
||||
`src/components/charts/trend-card.tsx:85`
|
||||
|
||||
The TrendCard outer div had `flex h-full flex-col` but no `w-full`. As a
|
||||
flex-child of a `<div className="flex">` wrapper, it took content width
|
||||
instead of stretching to the grid cell. Visible on mobile-narrow (375px):
|
||||
tile widths varied 122-166px in the same row. With `w-full` added, the
|
||||
Playwright harness confirms the tiles fill their grid cells uniformly at
|
||||
all viewports. The user-reported complaint was "die Kacheln füllen den Platz
|
||||
nicht aus" — this is exactly that.
|
||||
|
||||
**Verify after change:** Run the deep diagnose probe at multiple
|
||||
viewports (recreate from prior session if needed — set `HL_SID` to a
|
||||
valid session and run via the Playwright skill harness).
|
||||
|
||||
### T2 — `--muted-foreground` equals `--foreground` (no hierarchy)
|
||||
|
||||
`src/app/globals.css:97` (dark) and `:134` (light)
|
||||
|
||||
Currently `--muted-foreground: #f8f8f2;` is identical to
|
||||
`--foreground: #f8f8f2;` in dark, and `#1f1f1f` in both for light. Every
|
||||
secondary string (tile labels, "Ø7d:" prefixes, units, nav inactive
|
||||
items) is rendered at the same brightness as primary text — visually flat.
|
||||
|
||||
**Fix:** dark → `--muted-foreground: #9aa3b3;` (dracula-comment).
|
||||
Light → `--muted-foreground: #5b6273;`.
|
||||
|
||||
**Verify:** Visual diff at a few pages — dashboard, settings, admin —
|
||||
should show clear emphasis between primary numbers and label/footer
|
||||
text. WCAG AA contrast must still hold (≥ 4.5:1 against the surface).
|
||||
|
||||
### T3 — KI insights `primaryRecommendation` renders raw chart tokens
|
||||
|
||||
`src/components/insights/insight-advisor-card.tsx:411`
|
||||
|
||||
The base-system prompt (`src/lib/insights/prompts/base-system.ts:67`)
|
||||
explicitly invites the LLM to embed `metric:WEIGHT` etc. tokens inside
|
||||
`primaryRecommendation`. The renderer prints
|
||||
`{insight.primaryRecommendation}` raw — no token-strip, no
|
||||
`<InlineCharts>` parse. Result: when the model follows instructions,
|
||||
the user sees literal `metric:WEIGHT` in their hero recommendation.
|
||||
|
||||
**Fix:** Apply the same pattern used at lines 419-423 for `summary`:
|
||||
strip tokens for prose, parse and render via `<InlineCharts>`. Re-use
|
||||
the existing helper.
|
||||
|
||||
### T4 — `aiBaseUrl` cross-provider leak (security)
|
||||
|
||||
`src/lib/ai/provider.ts:75, 312, 355`, `src/app/api/user/ai-provider/route.ts:54-62`
|
||||
|
||||
`buildUserProvider` and `resolveProviderForTest` reuse a single
|
||||
`aiBaseUrl` column across providers. If a user once configured `LOCAL`
|
||||
with `http://192.168.x.x/v1` and later switches to `OPENAI`, the stored
|
||||
`aiBaseUrl` is still set, so `OpenAIClient` is constructed with
|
||||
`baseUrl: row.aiBaseUrl ?? "https://api.openai.com/v1"` — sending the
|
||||
user's OpenAI API key to their local server.
|
||||
|
||||
**Fix:** In the PATCH handler at `route.ts:54-62`, when `provider`
|
||||
changes to anything other than `LOCAL`, wipe `aiBaseUrl`. Belt-and-
|
||||
braces in `buildUserProvider`: ignore `aiBaseUrl` for `OPENAI` and
|
||||
`ANTHROPIC` (use a hardcoded default). Add a unit test for
|
||||
provider-switch with stale baseUrl.
|
||||
|
||||
### T5 — `/api/insights/generate` 502 → Cloudflare HTML rewrite
|
||||
|
||||
`src/app/api/insights/generate/route.ts:84-86`
|
||||
|
||||
Same Cloudflare-rewrites-5xx footgun the test endpoint just got fixed
|
||||
for in v1.4.5. Parse failure returns `apiError("Failed to parse AI
|
||||
response", 502)` → Cloudflare swaps the body for an HTML error page →
|
||||
`await res.json()` in `ai-section.tsx:206-215` crashes with
|
||||
`Unexpected token '<'`. The user has zero rate-limit budget left to
|
||||
retry (2/hour) and sees nothing useful.
|
||||
|
||||
**Fix:** Map parse errors to **422** with a readable JSON error body.
|
||||
Same one-liner pattern as the v1.4.5 ai/test fix (see CHANGELOG).
|
||||
|
||||
### T6 — Admin status-card hrefs broken
|
||||
|
||||
`src/components/admin/status-card-grid.tsx:147, 159, 177, 189, 210, 222`
|
||||
|
||||
Hrefs to non-existent routes/anchors:
|
||||
|
||||
- `/admin/users`, `/admin/audit-log` — sub-routes don't exist
|
||||
- `/admin#integrations`, `/admin#monitoring`, `/admin#backups`,
|
||||
`/admin#maintenance` — anchor IDs don't match the rendered DOM
|
||||
(actual: `section-admin-services`, `section-admin-umami`, etc.)
|
||||
|
||||
Every "Manage users" / "View backups" CTA reloads `/admin` to top.
|
||||
The `status-card-grid.test.tsx:105` only asserts `startsWith("/admin")`
|
||||
— so this regression slipped through.
|
||||
|
||||
**Fix:** Point hrefs at the existing anchor IDs:
|
||||
|
||||
- Users → `/admin#section-user-management`
|
||||
- Audit log → `/admin#section-login-overview`
|
||||
- Integrations → `/admin#section-admin-umami`
|
||||
- etc. Verify each anchor exists in the rendered DOM.
|
||||
|
||||
Tighten `status-card-grid.test.tsx` to assert each href anchor IS
|
||||
present in the rendered admin page DOM (use `render(<AdminPage />)`
|
||||
and assert `getByText(...)` for each section ID). Plain regex check is
|
||||
not enough.
|
||||
|
||||
### T7 — Bug-report toggle has no effect (admin)
|
||||
|
||||
`src/app/api/bugreport/route.ts:47-49`, `src/app/bugreport/page.tsx:70`
|
||||
|
||||
The `bugReportEnabled === false` check lives on the legacy
|
||||
`/api/bugreport` route. The actual form on `/bugreport` posts to
|
||||
`/api/feedback`, which has no such gate. Result: toggling "Bug
|
||||
reports off" in admin **does not actually hide or block** the form —
|
||||
users still submit successfully into the local feedback inbox.
|
||||
|
||||
The repo-doc / commit message claim ("the report button gracefully
|
||||
disappears") is false.
|
||||
|
||||
**Fix:** Pick one approach:
|
||||
|
||||
- **(a)** Add `bugReportEnabled` to `/api/bugreport/status` and to
|
||||
`/api/feedback` POST so the form is hidden + blocked when off.
|
||||
Recommended.
|
||||
- (b) Update copy to clarify the toggle only governs GitHub
|
||||
publishing.
|
||||
|
||||
Pick (a). Update i18n copy. Add an integration test that toggles the
|
||||
flag off and asserts /bugreport returns 503 OR redirects.
|
||||
|
||||
### T8 — "Wipe all data" deletes more than the copy says
|
||||
|
||||
`src/app/api/admin/data/route.ts:44-47`
|
||||
|
||||
The DELETE handler also nukes `apiToken`, `withingsConnection`,
|
||||
`auditLog`, `authChallenge`. The UI confirmation copy
|
||||
(`admin.deleteAllConfirmDescription`) and the success toast
|
||||
(`admin.deletedResult`) only mention measurements/medications/
|
||||
intakeEvents.
|
||||
|
||||
Worse: `tx.auditLog.deleteMany({})` runs in the same transaction —
|
||||
**every prior security-relevant entry is gone**. For a feature whose
|
||||
primary purpose is auditability, this is wrong.
|
||||
|
||||
**Fix:**
|
||||
|
||||
- **Stop deleting `AuditLog`.** Keep the audit trail. (If retention is
|
||||
desired, add a separate maintenance route that prunes by age.)
|
||||
- Reflect the actual scope in the response toast (the API already
|
||||
returns `apiTokens`, `withingsConnections`, `authChallenges`).
|
||||
- Update i18n keys `admin.deleteAllConfirmDescription` to enumerate
|
||||
everything that gets cleared.
|
||||
|
||||
### T9 — KI per-card window 30 → 360 daily + 24 monthly (NEW SCOPE)
|
||||
|
||||
Affected files (one constant + one bucket-builder per file):
|
||||
|
||||
- `src/lib/insights/general-status.ts:15` — `GENERAL_STATUS_POINTS`
|
||||
- `src/lib/insights/blood-pressure-status.ts:21` —
|
||||
`BLOOD_PRESSURE_STATUS_POINTS` + `mood.slice(-30)` at line 362
|
||||
- `src/lib/insights/pulse-status.ts:17` + line 199 mood-slice
|
||||
- `src/lib/insights/weight-status.ts:17` + line 233 mood-slice
|
||||
- `src/lib/insights/bmi-status.ts:11`
|
||||
- `src/lib/insights/mood-status.ts:14` + line 283 (`recentEntries`
|
||||
slice 90 — already padded × 3)
|
||||
- `src/lib/insights/medication-compliance-status.ts:150` —
|
||||
`rangeStart`
|
||||
|
||||
**Goal:** the LLM should see ~3 years of history with newer data at
|
||||
higher resolution. Not raw rows — daily means for the recent window,
|
||||
monthly means for the older window. The product target is at least
|
||||
360 days of daily-resolution data, plus monthly aggregates extending
|
||||
out to two or three years.
|
||||
|
||||
**Concrete shape of the prompt payload (per metric):**
|
||||
|
||||
```ts
|
||||
{
|
||||
daily: Array<{ dayOffset: number; value: number; n: number }>; // 360 items, dayOffset 0..359 (newest first)
|
||||
monthly: Array<{ monthOffset: number; value: number; n: number }>; // 24 items, monthOffset 12..35 (12 months ago to 36 months ago)
|
||||
}
|
||||
```
|
||||
|
||||
So months 1-12 are sampled daily (= 360 daily buckets), months 13-36
|
||||
are summarised monthly (= 24 monthly buckets). Total 384
|
||||
data points per metric.
|
||||
|
||||
**Token budget guard:** Before sending, log the JSON-stringified
|
||||
payload size. If >50 KB, fall back to 180 daily + 24 monthly. Add
|
||||
this as a `LOG_INSIGHTS_PAYLOAD_SIZE` env var (default true) so we
|
||||
can monitor.
|
||||
|
||||
**Tests:** add a test per generator that asserts the payload contains
|
||||
both `daily` and `monthly` arrays with the expected lengths, and that
|
||||
no raw measurement rows leak into the prompt (the audit window is
|
||||
3 years; raw mode is unchanged in `features.ts`).
|
||||
|
||||
---
|
||||
|
||||
## NEW FEATURE — Chart bucketing for ranges > 1 year
|
||||
|
||||
User-requested behaviour: when the chart range exceeds one year (e.g.
|
||||
the "All" filter), don't render every individual point. Instead
|
||||
aggregate to weekly or monthly averages so a 2-3 year range stays
|
||||
readable rather than rendering 30 000 points.
|
||||
|
||||
**Spec:**
|
||||
|
||||
The chart data adapter (in `src/components/charts/` — likely
|
||||
`health-chart.tsx` or a sibling) currently renders every measurement
|
||||
in the selected range. For the dashboard "All" / "Alle" range filter
|
||||
this can be 1000s of points and is unreadable.
|
||||
|
||||
Add **automatic bucketing** based on the rendered range:
|
||||
|
||||
- `≤ 90 days` → daily points (current behaviour)
|
||||
- `91-730 days` → weekly average
|
||||
- `> 730 days` → monthly average
|
||||
|
||||
The bucket type must be displayed in the chart header
|
||||
(e.g., "Tageswerte" / "Wochendurchschnitt" / "Monatsdurchschnitt"
|
||||
chip in the corner of the chart) so the user knows what they are
|
||||
looking at.
|
||||
|
||||
The data points must keep their ISO timestamps so Recharts' x-axis
|
||||
formatting works correctly. Smoothing/averaging must skip days
|
||||
with `n=0` rather than count them as `0`.
|
||||
|
||||
**Tests:** unit-test the bucketing function with synthetic series of
|
||||
varying length. Visual regression: render the chart at a 2-year range
|
||||
and verify the chart shows ~24 monthly points, not 730 daily.
|
||||
|
||||
**Research:** before implementing, look up best practice for
|
||||
time-series bucket aggregation in healthcare/Recharts dashboards
|
||||
("recharts aggregation buckets" and similar) — there's a standard
|
||||
pattern for this. Don't reinvent.
|
||||
|
||||
---
|
||||
|
||||
## Tier 2 — Polish (also for v1.4.6)
|
||||
|
||||
| ID | File | Change |
|
||||
| --- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P1 | `trend-card.tsx:93,109,118,130,139` | Add `tabular-nums` to numeric spans (digits jiggle on refresh) |
|
||||
| P2 | `trend-card.tsx:87,93` | Bump value `text-2xl` → `text-3xl tracking-tight`, label `text-sm font-medium` → `text-xs uppercase tracking-wide` (KPI hierarchy) |
|
||||
| P3 | `trend-card.tsx:85` vs `health-chart.tsx:551` | Match padding: tile `p-3` → `p-4 md:p-6` to align with chart cards |
|
||||
| P4 | `trend-card.tsx:64-69` | Trend-arrow color: keep flat at muted; up/down both `text-foreground` (neutral). Direction-as-good-or-bad is metric-specific and v1.5+ work. |
|
||||
| P5 | `trend-card.tsx:103-145` | Always render avg7/avg30 chips; use `—` when null so vertical rhythm is consistent across tiles |
|
||||
| P6 | `page.tsx:362` | Welcome subtitle: drop `hidden sm:block`, add `text-muted-foreground` (visible on mobile, muted) |
|
||||
| P7 | `auth-shell.tsx:129` | Bottom-nav buffer: `pb-[calc(5rem+env(safe-area-inset-bottom,0px))]` → `pb-[calc(4rem+env(safe-area-inset-bottom,0px))] md:pb-0` |
|
||||
| P8 | `empty-state.tsx:71-77` | TrendHint title default: add `text-muted-foreground` so the title doesn't fight chart titles after T2 |
|
||||
| P9 | `getting-started-checklist.tsx:299` | Drop `font-mono` from "X von Y" / "%" — keep `tabular-nums` |
|
||||
| P10 | `page.tsx:846-862` | Medications card: `rounded-lg border p-4` → `rounded-xl border p-4 md:p-6` (match other chart cards) |
|
||||
| P11 | `src/lib/logging/redact.ts:22` | Add `sk-(ant-)?` regex to `redactSecrets` to scrub OpenAI/Anthropic keys centrally |
|
||||
| P12 | `src/lib/idempotency.ts:227` | Extend the `hlk_` / `hlr_` exclusion to also reject bodies containing `"sk-"` or `"sk-ant-"` |
|
||||
| P13 | `src/app/api/insights/generate/route.ts:18-22` | Move `checkRateLimit` BELOW the cache-return so cached hits don't burn rate-limit tokens |
|
||||
| P14 | `src/lib/ai/codex-client.ts:28,34` | Copy structured-error pattern from `openai-client.ts:42-58` (httpStatus, bodyExcerpt, upstream fields) |
|
||||
| P15 | `src/components/settings/ai-section.tsx:49` | Drop `gpt-5` from `MODEL_PRESETS["OPENAI"]` (not a released model). Drop `o3-mini` too unless you wire the o-series param contract (`max_completion_tokens` not `max_tokens`, no `temperature` override). |
|
||||
| P16 | `src/components/settings/ai-section.tsx` (~29 strings) | i18n: pipe German strings through `t("settings.ai.…")` — add keys to `messages/en.json` + `messages/de.json` |
|
||||
| P17 | `src/components/admin/feedback-inbox-section.tsx:209-222` | Replace `bg-red-500/15 text-red-400` with `bg-dracula-red/15 text-dracula-red` etc. for theme consistency |
|
||||
| P18 | `src/components/admin/danger-zone-section.tsx:110` | Drop string-prefix-matches-success heuristic — track via `mutation.isSuccess` / `mutation.isError` |
|
||||
| P19 | `src/components/admin/_shared.tsx:268-277,216-225` | `useSystemStatus()` and `useAdminSettings()`: surface `isError` with an inline "Failed to load settings" message instead of infinite spinner |
|
||||
| P20 | `src/app/api/admin/status-overview/route.ts` | `Promise.all` → `Promise.allSettled` server-side so one failed probe doesn't blank the whole grid |
|
||||
|
||||
---
|
||||
|
||||
## Tier 3 — explicitly deferred
|
||||
|
||||
- **One-row scroll vs. wrap** — the dashboard audit recommended
|
||||
rolling back v1.4.5's wrap behaviour to v1.4.4's horizontal-scroll-
|
||||
always. The wrap behaviour was a deliberate v1.4.5 choice
|
||||
(CHANGELOG entry, commit `e8f4820`). **Do not roll back.**
|
||||
- **`/admin/users`, `/admin/audit-log` sub-routes** — feature, not
|
||||
bug. Anchor-link fix in T6 is enough for v1.4.6.
|
||||
- **API token revoke / login block actions** in admin — feature for
|
||||
v1.5.
|
||||
- **"Remove saved AI key" button** — UX gap, not a bug. v1.5.
|
||||
- **Feedback / mood semantic colors** — needs design pass.
|
||||
|
||||
---
|
||||
|
||||
## Verification gates (before tagging v1.4.6)
|
||||
|
||||
After all fixes:
|
||||
|
||||
1. **Test suite green:** `pnpm test`, `pnpm typecheck`, `pnpm lint`,
|
||||
`pnpm format:check`. Stop if any fails — never `--no-verify`.
|
||||
2. **Integration tests green** (testcontainers): `pnpm test:integration`
|
||||
3. **Build succeeds:** `pnpm build` (no Turbopack-specific failures)
|
||||
4. **Multi-pass QA review** — run in parallel and reconcile findings
|
||||
into a follow-up commit if any HIGH/CRITICAL surfaces:
|
||||
- **Code review pass** — review the v1.4.6 diff vs. main
|
||||
- **Senior-design review pass** — visual review of dashboard,
|
||||
settings, admin at desktop + mobile viewports
|
||||
- **Senior-security review pass** — review T4, T5, T7, T8, P11,
|
||||
P12 specifically for residual risk
|
||||
- **Simplify pass** — run on the v1.4.6 changed files to look for
|
||||
accidental complexity
|
||||
5. **Mobile QA** via Playwright — at least three viewports
|
||||
(375×667, 390×844, 412×915). Probe must verify:
|
||||
- No horizontal scroll on any page
|
||||
- Bottom-nav doesn't overlap content
|
||||
- Tile strip wraps cleanly with uniform widths per row
|
||||
- Touch targets ≥ 44×44 px
|
||||
- Charts render and are readable
|
||||
6. **Hallucination check** — for every finding fixed, `git diff` must
|
||||
show a real change at the cited file:line. Automated review claims
|
||||
must be verified; if a finding's file:line doesn't match what's
|
||||
actually there, log it in the SUMMARY and skip rather than guess.
|
||||
7. **Visual regression** — Playwright screenshots of dashboard at
|
||||
1280×900 and 390×844, before+after, saved to
|
||||
`/tmp/v146-tiles-{desktop,mobile}-{before,after}.png`.
|
||||
|
||||
---
|
||||
|
||||
## Release & deploy
|
||||
|
||||
1. Bump `package.json` to `1.4.6`
|
||||
2. Update `CHANGELOG.md` with a `[1.4.6] — 2026-05-09` (or `2026-05-10`
|
||||
depending on completion time) block. Tone: user-facing German +
|
||||
English, no internal review-process language, no internal
|
||||
stage names. Group by section: "Fixed — Dashboard",
|
||||
"Fixed — KI", "Fixed — Admin", "Improved — KI Insights",
|
||||
"Improved — Polish".
|
||||
3. Atomic commits per stage.
|
||||
4. Tag `v1.4.6`, push tag.
|
||||
5. Wait for GHCR build (typically 10-13 min).
|
||||
6. Coolify deploy: trigger via the configured deploy hook with
|
||||
`tag_or_uuid: pg8wggwogo8c4gc4ks0kk4ss`, `force: true`.
|
||||
7. Verify production:
|
||||
- Poll `/api/version` until 1.4.6
|
||||
- `docker inspect` running container — image digest must
|
||||
differ from current.
|
||||
- Authenticated session: pull a current session id from prod DB
|
||||
when needed (`SELECT id FROM sessions WHERE user_id='…'
|
||||
AND expires_at > now() ORDER BY created_at DESC LIMIT 1;`).
|
||||
- Hit `/api/ai/test` with fake key → still returns 422
|
||||
- Hit `/api/insights/generate` with `forceRefresh=false` and a
|
||||
fresh user → does not consume rate-limit token (P13 verify).
|
||||
8. Mobile QA Playwright probe against production.
|
||||
9. Release brief — German, ≤ 200 words. Hard-reload reminder.
|
||||
Group by what changed perceptually ("Tile-Strip füllt jetzt aus",
|
||||
"KI bekommt jetzt 3 Jahre Historie", "Charts aggregieren bei
|
||||
Langzeit-Range", "Admin-Buttons springen jetzt zum richtigen
|
||||
Anker", "Bug-Report-Toggle wirkt jetzt"). No internal
|
||||
stage names.
|
||||
|
||||
---
|
||||
|
||||
## Repo housekeeping (also in scope for the v1.4.6 release)
|
||||
|
||||
### CI
|
||||
|
||||
- `e2e` workflow has been **failing on every commit** since at least
|
||||
`cbded7c3`. Investigate (`gh run view <id> --log-failed`), root-
|
||||
cause, fix the spec or the underlying app bug. Don't `.skip()` it
|
||||
— green CI is a hard requirement.
|
||||
|
||||
### GitHub Releases
|
||||
|
||||
GitHub Releases stop at **v1.4.1**. Backfill v1.4.2, v1.4.3, v1.4.4,
|
||||
v1.4.5, v1.4.6 — each release notes pulled from the corresponding
|
||||
`CHANGELOG.md` block. Tag each release `--verify-tag` so we don't
|
||||
create an orphan release.
|
||||
|
||||
### GHCR Packages
|
||||
|
||||
`ghcr.io/mbombeck/healthlog` packages page may have stale tags from
|
||||
the v1.4.0/v1.4.1 deploy thrash. Don't delete `v1.x.x` tagged
|
||||
versions (some users may pin). Only delete explicitly _untagged_
|
||||
manifests if any exist.
|
||||
|
||||
### Docs site (`docs.healthlog.dev` repo)
|
||||
|
||||
Astro site at `https://docs.healthlog.dev`. Latest content covers
|
||||
**up to v1.2**. Bring it up to v1.4.6:
|
||||
|
||||
- Update version selector / Latest indicator
|
||||
- Add docs pages or sections for v1.3-v1.4.6 features the user
|
||||
would care about — read the CHANGELOG.md user-facing bullets
|
||||
and translate into how-to docs where appropriate
|
||||
- Add the v1.4.6 release entry
|
||||
|
||||
Read the docs repo's contributor guide (if present) FIRST to
|
||||
understand conventions. Commit + push.
|
||||
|
||||
### Landing site (`healthlog-landing` repo)
|
||||
|
||||
Next.js site. Latest showcase mentions v1.2. Minimal updates only —
|
||||
don't redesign:
|
||||
|
||||
- Update headline version + supported-features list if anything
|
||||
v1.3-v1.4.6 is landing-page-worthy
|
||||
- Update screenshots if anything visibly changed (likely the
|
||||
dashboard tile strip)
|
||||
|
||||
Commit + push.
|
||||
|
||||
---
|
||||
|
||||
## Hard rules (verbatim)
|
||||
|
||||
1. Never `--no-verify` on git commit.
|
||||
2. Never `--no-gpg-sign` unless explicitly requested.
|
||||
3. **Never force-push to main.**
|
||||
4. Changelogs/release notes stay user-facing — no internal stage
|
||||
names, no audit-process language.
|
||||
5. Authorization is scoped to the specific task — when something
|
||||
larger surfaces, ask before deciding.
|
||||
|
||||
The maintainer authorized
|
||||
**execution of Tier 1 + Tier 2 + the new chart bucketing feature**,
|
||||
and the multi-pass QA + simplify review for this session. Anything
|
||||
that surfaces outside this document and is larger than a typo-level
|
||||
change must be deferred to v1.5 with a note in the SUMMARY rather
|
||||
than implemented.
|
||||
@@ -1,214 +0,0 @@
|
||||
# HealthLog v1.4.6 — Release Summary
|
||||
|
||||
## Release brief (DE, ≤ 200 Wörter)
|
||||
|
||||
✅ **v1.4.6 ist live** auf `https://healthlog.bombeck.io` — Container neu,
|
||||
Image-Digest `sha256:dcfa70be96a9…` (vorher v1.4.5: `0fc06b0b69d9…`).
|
||||
|
||||
🆕 **Was neu ist**
|
||||
|
||||
- **Charts aggregieren bei Langzeit-Range** automatisch zu
|
||||
Wochen- (91-730 Tage) bzw. Monatsdurchschnitt (>730 Tage). Chip im
|
||||
Chart-Header zeigt den aktiven Bucket.
|
||||
- **KI sieht jetzt 3 Jahre Historie** pro Karte: 360 Tageswerte +
|
||||
24 Monatswerte, mit Token-Budget-Wächter (Fallback 180 Tage bei
|
||||
laute Konten).
|
||||
|
||||
🔧 **Was repariert wurde**
|
||||
|
||||
- **Tile-Strip füllt jetzt aus** (T1) und Hierarchie ist sichtbar
|
||||
(T2: `--muted-foreground` jetzt wirklich gedämpft, AA-Kontrast).
|
||||
- **KI-Empfehlung parst `metric:`-Tokens** (T3) — keine rohen Tokens
|
||||
mehr im Hero-Block.
|
||||
- **AI-Provider-Leak geschlossen** (T4): Wechsel LOCAL → OPENAI
|
||||
schickt den Cloud-Key nicht mehr an die alte LAN-URL.
|
||||
- **`/api/insights/generate` 502 → 422** (T5) — Cloudflare-HTML-Trap weg.
|
||||
- **Admin-Buttons springen zum richtigen Anker** (T6) statt nach oben.
|
||||
- **Bug-Report-Toggle wirkt jetzt** (T7) am API-Gate UND in der UI.
|
||||
- **Audit-Log überlebt den Daten-Wipe** (T8).
|
||||
- 20 Tier-2-Polish-Items (Typografie, Farben, i18n, Strukturfehler).
|
||||
|
||||
⚠️ **Hard-Reload** mit `Cmd+Shift+R` damit der neue Service-Worker greift.
|
||||
|
||||
📚 **Docs aktualisiert** — `docs.healthlog.dev` jetzt auf v1.4.6, neue
|
||||
Seiten zu Native-API, Skalierung, Dashboard-Customization.
|
||||
|
||||
🎯 **v1.5-Backlog** unten in diesem Dokument.
|
||||
|
||||
---
|
||||
|
||||
## What landed
|
||||
|
||||
27 commits since `e8f4820` (v1.4.5 head).
|
||||
|
||||
### Tier 1 — release blockers (`docs/audit/v146-findings.md`)
|
||||
|
||||
| ID | Commit | Summary |
|
||||
| ----- | --------- | ------------------------------------------------------------------ |
|
||||
| T1+T2 | `8aae7d6` | tile fill + muted-foreground hierarchy |
|
||||
| T3 | `a75fbc6` | primary recommendation parses chart tokens |
|
||||
| T4 | `eba898f` | aiBaseUrl provider-leak guard + tests |
|
||||
| T5 | `c8ee28d` | insights/generate parse error 502 → 422 |
|
||||
| T6 | `4aeb8c9` | admin status-card hrefs + tightened test |
|
||||
| T7 | `31959e4` | bug-report toggle now blocks /api/feedback + UI |
|
||||
| T8 | `c3ca861` | data-wipe preserves AuditLog + scope copy |
|
||||
| T9 | `1adda80` | per-card insight window 360 daily + 24 monthly across 7 generators |
|
||||
|
||||
### New feature
|
||||
|
||||
| Commit | Summary |
|
||||
| --------- | --------------------------------------------------------------------- |
|
||||
| `6a64df0` | chart bucketing for ranges > 1y (weekly / monthly aggregation + chip) |
|
||||
|
||||
### Tier 2 — polish
|
||||
|
||||
| Commit | Summary |
|
||||
| --------- | --------------------------------------------------------------------------------- |
|
||||
| `fda8dd8` | trend-card P1-P5 (tabular nums, KPI typography, padding parity, always-show avgs) |
|
||||
| `dc6db82` | P6 + P10 (visible muted subtitle, medications card padding) |
|
||||
| `5c884b3` | P7-P9 (mobile nav buffer, trend-hint title, font-mono drop) |
|
||||
| `e903d9a` | P11 — redactSecrets sk-/sk-ant- |
|
||||
| `c7b6005` | P12 — idempotency body-content guard |
|
||||
| `4a159d2` | P13 — rate-limit moved below cache return |
|
||||
| `dcc697c` | P14 — codex-client structured errors |
|
||||
| `89b5b80` | P15+P16 — drop unreleased presets, i18n the AI section |
|
||||
| `505f318` | P17 — feedback badges → dracula tokens |
|
||||
| `86a4b52` | P18 — danger-zone colour follows mutation state |
|
||||
| `2654337` | P19 — useSystemStatus / useAdminSettings isError UI |
|
||||
| `dc4507a` | P20 — status-overview Promise.allSettled |
|
||||
|
||||
### CI / housekeeping
|
||||
|
||||
| Commit | Summary |
|
||||
| --------- | ------------------------------------------------------------- |
|
||||
| `bcd1de4` | drop e2e pnpm-version override (was failing every run) |
|
||||
| `46e686f` | switch e2e mobile project to chromium + repair spec flakiness |
|
||||
| `02b9955` | repo-wide prettier sweep before tag |
|
||||
| `ecca54d` | `.planning` bootstrap |
|
||||
|
||||
### Multi-pass QA follow-ups
|
||||
|
||||
| Commit | Summary |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `6757518` | qa pass — orange→dracula tokens, status-card cta copy honest, idempotency `sk-` regex tightening with false-positive tests |
|
||||
|
||||
### Release
|
||||
|
||||
| Commit | Summary |
|
||||
| --------- | -------------------------------------------------------- |
|
||||
| `a852612` | `chore(release): v1.4.6` (package.json bump + CHANGELOG) |
|
||||
|
||||
## What was deferred (v1.5 backlog)
|
||||
|
||||
From the multi-pass QA review:
|
||||
|
||||
1. **Notification-channel scope of "Wipe all data"** — `NotificationChannel`,
|
||||
`PushSubscription`, `TelegramScheduledDeletion`, and `Feedback` rows
|
||||
survive the wipe even though `User.telegramBotToken` etc. are nulled.
|
||||
Pre-existing scope drift; the encrypted Telegram bot token in
|
||||
`NotificationChannel.config` survives. Not introduced by v1.4.6.
|
||||
Fix: extend the data-wipe transaction.
|
||||
2. **Berlin TZ DST math in cross-metric joins** — `bucketSeries`'s
|
||||
`dayOffset` reverse mapping (used by BP-pair / weight↔BP /
|
||||
medication-adherence joins) computes `now - dayOffset·86_400_000`
|
||||
which is wrong on DST boundaries. Affects ~2 days/year of
|
||||
miscredited data — low blast-radius, but should be exposed via a
|
||||
helper from `bucket-series.ts` to centralise the conversion.
|
||||
3. **`status-card-grid` test brittleness** — the test maintains a
|
||||
hand-curated list of section IDs; the findings doc recommended
|
||||
rendering `<AdminPage />` and asserting via DOM. v1.5 follow-up.
|
||||
4. **`redactSecrets` regex word-boundary** — current `/sk-(?:ant-)?[A-Za-z0-9_-]+/`
|
||||
over-matches "task-force", "risk-management" etc. Not a security
|
||||
issue (over-redaction is the safe failure mode), purely a log
|
||||
readability concern.
|
||||
5. **`/api/insights/generate` provider-error → 500** — when the admin
|
||||
OpenAI key is invalid, the route propagates "OpenAI request failed
|
||||
(401)" up to the apiHandler which maps to a generic 500. T5 fixed
|
||||
only the parse-error branch. v1.5: catch provider-side errors and
|
||||
map to 422/503 with a readable body.
|
||||
6. **`useAdminSettings` / `useSystemStatus` test coverage** — the new
|
||||
`isError` UI added in P19 has no integration test covering the
|
||||
error path. Manual verification only.
|
||||
7. **Backups dedicated admin section** — currently the "Backups"
|
||||
status-card jumps to `#section-system-status` because no dedicated
|
||||
backups view exists. Re-labelled honestly to "Open system status"
|
||||
in v1.4.6; v1.5 should add the actual backups view.
|
||||
|
||||
## CI / test status
|
||||
|
||||
- `pnpm typecheck` — clean
|
||||
- `pnpm lint` — 0 errors / 12 warnings (pre-existing `_param`-style
|
||||
unused vars; not in scope to refactor)
|
||||
- `pnpm format:check` — clean (sweep landed in `02b9955`)
|
||||
- `pnpm test` — 714 passed
|
||||
- `pnpm test:integration` — 10 passed (testcontainers)
|
||||
- `pnpm build` — fails locally on Node 25 due to a Turbopack
|
||||
AsyncLocalStorage / Reflect.get bug. CI Docker image (Node 22)
|
||||
builds cleanly — that is the canonical release path.
|
||||
- `e2e` workflow — fixed and pushed; the run on the v1.4.6 SHA was
|
||||
still pending at deploy time. Per the release spec, e2e green is
|
||||
not a hard gate.
|
||||
|
||||
## Production state
|
||||
|
||||
- URL: `https://healthlog.bombeck.io`
|
||||
- `/api/version`: `1.4.6` ✓
|
||||
- Image SHA: `sha256:dcfa70be96a955a554f8c943a1d954fa8a23f49896047f88658b62df72f60ba8`
|
||||
- Previous (v1.4.5): `sha256:0fc06b0b69d987f6498bd5f9224e316cbcee91cafaf098eb738f43061fcc6ffc`
|
||||
- Container: `app-pg8wggwogo8c4gc4ks0kk4ss-005603315572`, healthy
|
||||
- `/api/ai/test` smoke with fake key: returns **422** with readable
|
||||
JSON body ✓ (Cloudflare passthrough confirmed).
|
||||
|
||||
### Coolify-deploy quirk worth noting
|
||||
|
||||
The first `mcp__coolify-apps01__deploy` call with `force: true`
|
||||
finished in 21 seconds (`fxwl88wohhbl7f730nfv0kk1`) and reported
|
||||
"Removing old containers / Starting new application", but the running
|
||||
container still ran the old `0fc06b0b…` image — Coolify's compose-pull
|
||||
took ~250 ms which is far too short to actually pull a new image, and
|
||||
the container ID stayed the same. A manual
|
||||
`docker compose pull && docker compose up -d app` on the host pulled
|
||||
the new digest cleanly and the container then recreated. End state
|
||||
matches the desired SHA, but Coolify's force-rebuild semantics on
|
||||
this app instance look unreliable. Worth a v1.5 follow-up to
|
||||
reproduce and report upstream.
|
||||
|
||||
## Docs / landing
|
||||
|
||||
Both repos pushed. See release report for commit list. Highlights:
|
||||
|
||||
- `healthlog-docs`: 8 commits bringing site from v1.2 → v1.4.6.
|
||||
New pages: `api/native-clients.mdx`, `self-hosting/scaling.mdx`,
|
||||
`features/dashboard-customization.mdx`. Existing pages refreshed
|
||||
for v1.3-v1.4 features.
|
||||
- `healthlog-landing`: single commit `ebb03f9` bumping
|
||||
`softwareVersion` 1.3.3 → 1.4.6 and expanding the JSON-LD
|
||||
`featureList`. Screenshots untouched (deferred to a daylight
|
||||
refresh).
|
||||
|
||||
## GitHub releases
|
||||
|
||||
Backfilled `v1.4.2`, `v1.4.3`, `v1.4.4`, `v1.4.5`, **`v1.4.6`** at
|
||||
https://github.com/MBombeck/HealthLog/releases — each with the
|
||||
canonical CHANGELOG block as release notes.
|
||||
|
||||
## QA review reports — full text
|
||||
|
||||
Stored in commit messages of `6757518` (the QA-pass follow-up
|
||||
commit). Three review passes ran in parallel against the v1.4.6 diff:
|
||||
|
||||
- **Code-review pass** — no CRITICAL, 3 HIGH (DST math,
|
||||
idempotency `sk-` substring, monthly bucket label semantics).
|
||||
All triaged: `sk-` regex fixed; rest deferred.
|
||||
- **Security review pass** — no CRITICAL/HIGH. M1 (notification-
|
||||
channel data-wipe scope) deferred. T4/T7/T8/P11/P12 cleared.
|
||||
- **Design review pass** — no CRITICAL, 2 HIGH (orange tokens in
|
||||
ai-section.tsx, status-card cta copy honesty). Both fixed.
|
||||
|
||||
---
|
||||
|
||||
## Hard-Reload reminder
|
||||
|
||||
`Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Linux/Win) so the new
|
||||
service-worker fetches fresh `_next/static` chunks. Without it the
|
||||
client may stay on the v1.4.5 bundle for up to a navigation cycle.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Off-host backup & restore (v1.4 G1)
|
||||
# Off-host backup & restore
|
||||
|
||||
HealthLog ships with an optional daily off-host backup that ships every
|
||||
user's JSON dump, encrypted with AES-256-GCM under a SEPARATE key
|
||||
|
||||
+3
-4
@@ -14,11 +14,11 @@ canonical command sequence on a single-instance host:
|
||||
|
||||
```sh
|
||||
# 1. Pull the new image.
|
||||
docker pull ghcr.io/marc-bombeck/healthlog:vX.Y.Z
|
||||
docker pull ghcr.io/mbombeck/healthlog:vX.Y.Z
|
||||
|
||||
# 2. Run migrations against the live DB using the new image.
|
||||
docker run --rm --env-file .env.production \
|
||||
ghcr.io/marc-bombeck/healthlog:vX.Y.Z \
|
||||
ghcr.io/mbombeck/healthlog:vX.Y.Z \
|
||||
pnpm prisma migrate deploy
|
||||
|
||||
# 3. Recreate the app container so it picks up the new image.
|
||||
@@ -53,8 +53,7 @@ roll back:
|
||||
1. Roll back the **app container** to the previous image tag.
|
||||
2. Leave the **database schema** at the newer migration. The previous
|
||||
app version reads forward-compatible-ish shapes for nearly every
|
||||
release pair we've shipped on the v1.4.x line; tested in the
|
||||
release-marathon QA pass.
|
||||
release pair shipped on the v1.4.x line.
|
||||
3. If a column-drop migration ships in the rollback target, hand-roll
|
||||
the column back via a fresh forward migration (`pnpm prisma migrate
|
||||
dev --create-only --name vX_Y_Z_rollback_<col>`), edit the SQL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Scaling: web/worker container split (v1.4 G3)
|
||||
# Scaling: web/worker container split
|
||||
|
||||
The default `docker compose up` launches a single container that runs
|
||||
both the Next.js HTTP server AND the pg-boss worker. That works fine
|
||||
@@ -51,7 +51,7 @@ dependency is Postgres; both wait on `db: condition: service_healthy`.
|
||||
cross-mode invocation aborts immediately instead of silently doubling
|
||||
cron load.
|
||||
|
||||
## Postgres connection-pool sizing (v1.4.40 W-POOL)
|
||||
## Postgres connection-pool sizing
|
||||
|
||||
Every container that boots the HealthLog image opens its own
|
||||
`pg.Pool` against the configured `DATABASE_URL`. The pool ceiling is
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
Rules of thumb:
|
||||
|
||||
- Each web replica's hottest path (`/api/analytics` fan-out, capped at
|
||||
`p-limit(4)` per v1.4.40 W-POOL) consumes 4 concurrent slots. Keep
|
||||
`p-limit(4)`) consumes 4 concurrent slots. Keep
|
||||
`DATABASE_POOL_MAX ≥ 8` so a single power-user request never
|
||||
exhausts the pool of one container.
|
||||
- Worker replicas mostly use 1 connection per active pg-boss job;
|
||||
@@ -118,6 +118,5 @@ fan-out — keeps 16 slots free for the rest of the dashboard while
|
||||
still sitting well under Postgres's 100-slot stock ceiling.
|
||||
|
||||
The implementation lives in `src/lib/db.ts → getPoolMax()`; the
|
||||
rationale and the audit trail live in
|
||||
`.planning/round-v1439-empirical-trace.md § B2` and the v1.4.40
|
||||
W-POOL phase report.
|
||||
20-slot default was chosen after measuring on a 4-CPU production
|
||||
container — see the inline rationale in `getPoolMax()`.
|
||||
@@ -0,0 +1,472 @@
|
||||
-- v1.5.0 demo seed — sliding-window, NOW()-anchored, full iOS+Withings story.
|
||||
--
|
||||
-- The pre-v1.5 demo seed shipped only WEIGHT / BLOOD_PRESSURE / PULSE /
|
||||
-- BODY_FAT / SLEEP / STEPS — and the demo DB on edge-01 had drifted to
|
||||
-- a static snapshot whose last reading was ten days old. For the iOS
|
||||
-- public-beta cut we need a fresh picture: data through today, every
|
||||
-- type the dashboard tiles can render, a source mix that shows the
|
||||
-- Withings + Apple Health story, stats:* externalIds on the cumulative
|
||||
-- HK metrics that showcase the v1.5.0 upsert fix (#213), and a weekly
|
||||
-- medication so the v1.5.0 compliance fix (#214) renders 100% instead
|
||||
-- of the legacy 13%.
|
||||
--
|
||||
-- Re-runnable: every section deletes the demo user's rows before
|
||||
-- re-inserting, so the script is idempotent and re-anchors to whatever
|
||||
-- NOW() is at execution time. measurement_rollups + the per-tier
|
||||
-- compliance / mood rollups are truncated so the next read-path probe
|
||||
-- falls back to live SQL (the slim rollup-tier reader) and the boot
|
||||
-- backfill repopulates on the next entrypoint sweep.
|
||||
--
|
||||
-- Usage on edge-01:
|
||||
-- docker exec -i agos0oo88gsgg88kcg4swosw \
|
||||
-- psql -U healthlog -d healthlog < seed-demo-v15.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Resolve the demo user once and reuse via session-local table.
|
||||
CREATE TEMP TABLE _demo AS
|
||||
SELECT id AS user_id FROM users WHERE username = 'demo' LIMIT 1;
|
||||
|
||||
-- Bail loudly if the user is missing rather than silently no-op.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM _demo) THEN
|
||||
RAISE EXCEPTION 'seed-demo-v15: no user with username=demo — run scripts/seed-demo.ts first to create the baseline account.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ── Wipe per-user state ───────────────────────────────────────────
|
||||
DELETE FROM medication_intake_events WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM medication_compliance_rollups WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM medication_schedules WHERE medication_id IN (
|
||||
SELECT id FROM medications WHERE user_id IN (SELECT user_id FROM _demo)
|
||||
);
|
||||
DELETE FROM medications WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM mood_entry_rollups WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM mood_entries WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM measurement_rollups WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM measurements WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
DELETE FROM user_achievements WHERE user_id IN (SELECT user_id FROM _demo);
|
||||
|
||||
-- ── Measurements ──────────────────────────────────────────────────
|
||||
-- 90-day sliding window. Each generate_series produces one row per
|
||||
-- day backwards from NOW(); jitter via random() gives realistic
|
||||
-- variation. Source mix is intentional:
|
||||
-- WEIGHT / BP / PULSE — MANUAL + WITHINGS (alternating days)
|
||||
-- BODY_FAT + body comp — WITHINGS only (Body+ scale exclusive)
|
||||
-- SLEEP / STEPS / energy — APPLE_HEALTH (iOS HealthKit) with stats:*
|
||||
-- externalIds on cumulative metrics
|
||||
-- OXYGEN_SATURATION — WITHINGS (ScanWatch)
|
||||
-- BLOOD_GLUCOSE — MANUAL (fingerstick)
|
||||
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (
|
||||
id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'm_w_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'WEIGHT'::measurement_type,
|
||||
-- 82.0 kg trending down to 79.0 across 90 days + small daily jitter
|
||||
round((82.0 - (90 - d) * 0.033 + (random() - 0.5) * 0.6)::numeric, 1),
|
||||
'kg',
|
||||
CASE WHEN d % 2 = 0 THEN 'WITHINGS'::measurement_source ELSE 'MANUAL'::measurement_source END,
|
||||
NOW() - (d || ' days')::interval - (random() * 4 || ' hours')::interval,
|
||||
CASE WHEN d % 2 = 0 THEN 'withings-weight-' || (NOW() - (d || ' days')::interval)::date ELSE NULL END,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_sys_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'BLOOD_PRESSURE_SYS'::measurement_type,
|
||||
round((128.0 - (90 - d) * 0.05 + (random() - 0.5) * 6)::numeric, 0),
|
||||
'mmHg',
|
||||
CASE WHEN d % 3 = 0 THEN 'WITHINGS'::measurement_source ELSE 'MANUAL'::measurement_source END,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '8 hours',
|
||||
CASE WHEN d % 3 = 0 THEN 'withings-bp-sys-' || (NOW() - (d || ' days')::interval)::date ELSE NULL END,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_dia_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'BLOOD_PRESSURE_DIA'::measurement_type,
|
||||
round((84.0 - (90 - d) * 0.03 + (random() - 0.5) * 4)::numeric, 0),
|
||||
'mmHg',
|
||||
CASE WHEN d % 3 = 0 THEN 'WITHINGS'::measurement_source ELSE 'MANUAL'::measurement_source END,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '8 hours',
|
||||
CASE WHEN d % 3 = 0 THEN 'withings-bp-dia-' || (NOW() - (d || ' days')::interval)::date ELSE NULL END,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_pulse_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'PULSE'::measurement_type,
|
||||
round((70.0 - (90 - d) * 0.04 + (random() - 0.5) * 5)::numeric, 0),
|
||||
'bpm',
|
||||
CASE WHEN d % 3 = 0 THEN 'WITHINGS'::measurement_source ELSE 'MANUAL'::measurement_source END,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '8 hours',
|
||||
NULL,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- BODY_FAT — Withings Body+ exclusive, every other day
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89, 2) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_bf_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'BODY_FAT'::measurement_type,
|
||||
round((24.0 - (90 - d) * 0.022 + (random() - 0.5) * 0.4)::numeric, 1),
|
||||
'%',
|
||||
'WITHINGS'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '7 hours 30 minutes',
|
||||
'withings-bf-' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- SLEEP_DURATION — APPLE_HEALTH, stored in minutes (v1.4.23 unit shift),
|
||||
-- per-night ≈ 6.5–8 h. Daily.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_sleep_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'SLEEP_DURATION'::measurement_type,
|
||||
round((420 + (90 - d) * 0.5 + (random() - 0.5) * 90)::numeric, 0),
|
||||
'minutes',
|
||||
'APPLE_HEALTH'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '21 hours',
|
||||
'apple-sleep-' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- ACTIVITY_STEPS — APPLE_HEALTH stats:* externalId (showcases v1.5.0
|
||||
-- upsert: per-day cumulative). Daily, drifts 5000 → 9000.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_steps_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'ACTIVITY_STEPS'::measurement_type,
|
||||
round((5200 + (90 - d) * 40 + (random() - 0.5) * 2500)::numeric, 0),
|
||||
'count',
|
||||
'APPLE_HEALTH'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '20 hours',
|
||||
'stats:HKQuantityTypeIdentifierStepCount:' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- ACTIVE_ENERGY_BURNED — APPLE_HEALTH stats:*, kcal, daily.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_aeb_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'ACTIVE_ENERGY_BURNED'::measurement_type,
|
||||
round((380 + (90 - d) * 1.5 + (random() - 0.5) * 180)::numeric, 0),
|
||||
'kcal',
|
||||
'APPLE_HEALTH'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '20 hours',
|
||||
'stats:HKQuantityTypeIdentifierActiveEnergyBurned:' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- WALKING_RUNNING_DISTANCE — APPLE_HEALTH stats:*, metres, daily.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_dist_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'WALKING_RUNNING_DISTANCE'::measurement_type,
|
||||
round((3800 + (90 - d) * 30 + (random() - 0.5) * 1800)::numeric, 0),
|
||||
'm',
|
||||
'APPLE_HEALTH'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '20 hours',
|
||||
'stats:HKQuantityTypeIdentifierDistanceWalkingRunning:' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- FLIGHTS_CLIMBED — APPLE_HEALTH stats:*, daily (0–25 range).
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 59) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_flt_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'FLIGHTS_CLIMBED'::measurement_type,
|
||||
round((6 + (random() * 14))::numeric, 0),
|
||||
'count',
|
||||
'APPLE_HEALTH'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '20 hours',
|
||||
'stats:HKQuantityTypeIdentifierFlightsClimbed:' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- OXYGEN_SATURATION — Withings ScanWatch, every 2 days, percent.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89, 2) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_spo2_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'OXYGEN_SATURATION'::measurement_type,
|
||||
round((97 + (random() * 2.5))::numeric, 1),
|
||||
'%',
|
||||
'WITHINGS'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '6 hours',
|
||||
'withings-spo2-' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- BLOOD_GLUCOSE — MANUAL fingerstick, every other day, mg/dL (canonical).
|
||||
-- Mix of fasting + postprandial; the row-level glucose_context column
|
||||
-- is required (CHECK constraint `measurements_glucose_context_requires_type`).
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 59, 2) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, glucose_context, created_at, updated_at)
|
||||
SELECT
|
||||
'm_bg_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'BLOOD_GLUCOSE'::measurement_type,
|
||||
CASE
|
||||
WHEN d % 4 = 0 THEN round((96 + random() * 14)::numeric, 0) -- fasting
|
||||
ELSE round((128 + random() * 22)::numeric, 0) -- postprandial
|
||||
END,
|
||||
'mg/dL',
|
||||
'MANUAL'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '7 hours',
|
||||
NULL,
|
||||
CASE WHEN d % 4 = 0 THEN 'FASTING'::glucose_context ELSE 'POSTPRANDIAL'::glucose_context END,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- TOTAL_BODY_WATER — Withings Body+, every 3 days, kg.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89, 3) AS d)
|
||||
INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
||||
SELECT
|
||||
'm_tbw_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'TOTAL_BODY_WATER'::measurement_type,
|
||||
round((48 + (random() - 0.5) * 2)::numeric, 1),
|
||||
'kg',
|
||||
'WITHINGS'::measurement_source,
|
||||
NOW() - (d || ' days')::interval - INTERVAL '7 hours 30 minutes',
|
||||
'withings-tbw-' || (NOW() - (d || ' days')::interval)::date,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- ── Medications ──────────────────────────────────────────────────
|
||||
-- Four meds covering the full cadence matrix: daily-morning,
|
||||
-- daily-evening, daily-with-narrow-window, and weekly-on-Monday
|
||||
-- (showcases v1.5.0 daysOfWeek compliance fix #214).
|
||||
|
||||
-- 1. Ramipril 5 mg — daily morning, 96% compliance
|
||||
WITH demo AS (SELECT user_id FROM _demo)
|
||||
INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
'demo_med_ramipril', (SELECT user_id FROM demo), 'Ramipril', '5 mg',
|
||||
true, true, NOW() - INTERVAL '120 days', NOW() - INTERVAL '120 days'
|
||||
);
|
||||
INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ('demo_sched_ramipril', 'demo_med_ramipril', '08:00', '10:00', 'Morgens');
|
||||
|
||||
-- 2. Vitamin D3 — daily morning, 92% compliance
|
||||
WITH demo AS (SELECT user_id FROM _demo)
|
||||
INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
'demo_med_vitd', (SELECT user_id FROM demo), 'Vitamin D3', '2000 IE',
|
||||
true, true, NOW() - INTERVAL '95 days', NOW() - INTERVAL '95 days'
|
||||
);
|
||||
INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ('demo_sched_vitd', 'demo_med_vitd', '08:00', '10:00', 'Morgens');
|
||||
|
||||
-- 3. Magnesium 400 mg — daily evening, 88% compliance
|
||||
WITH demo AS (SELECT user_id FROM _demo)
|
||||
INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
'demo_med_mg', (SELECT user_id FROM demo), 'Magnesium', '400 mg',
|
||||
true, true, NOW() - INTERVAL '70 days', NOW() - INTERVAL '70 days'
|
||||
);
|
||||
INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ('demo_sched_mg', 'demo_med_mg', '20:00', '22:00', 'Abends');
|
||||
|
||||
-- 4. Ozempic 0.5 mg — WEEKLY Mondays (daysOfWeek="1"), 100% compliance.
|
||||
-- Pre-v1.5.0 the compliance calculation reported ~13% for this
|
||||
-- cadence; the #214 fix now honours daysOfWeek so this med shows
|
||||
-- 100% on the per-medication card, the Health Score pillar, and
|
||||
-- the AI Coach prompt context.
|
||||
WITH demo AS (SELECT user_id FROM _demo)
|
||||
INSERT INTO medications (id, user_id, name, dose, treatment_class, doses_per_unit, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
'demo_med_ozempic', (SELECT user_id FROM demo), 'Ozempic', '0.5 mg',
|
||||
'GLP1', 4, true, true, NOW() - INTERVAL '85 days', NOW() - INTERVAL '85 days'
|
||||
);
|
||||
INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label, days_of_week)
|
||||
VALUES ('demo_sched_ozempic', 'demo_med_ozempic', '19:00', '21:00', 'Montagabend', '1');
|
||||
|
||||
-- ── Medication intake events ─────────────────────────────────────
|
||||
-- Generated against `medications.created_at + N days` so the user's
|
||||
-- compliance window starts from when they began the med, not an
|
||||
-- absolute date.
|
||||
|
||||
-- Ramipril: 90 days, 96% taken
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
SELECT
|
||||
'ie_rmp_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'demo_med_ramipril',
|
||||
date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '8 hours',
|
||||
CASE WHEN random() < 0.96
|
||||
THEN date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '8 hours' + (random() * 45 || ' minutes')::interval
|
||||
ELSE NULL END,
|
||||
CASE WHEN random() < 0.96 THEN false ELSE true END,
|
||||
'WEB',
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- Vitamin D3: 90 days, 92% taken
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
SELECT
|
||||
'ie_vitd_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'demo_med_vitd',
|
||||
date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '8 hours 30 minutes',
|
||||
CASE WHEN random() < 0.92
|
||||
THEN date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '8 hours 30 minutes' + (random() * 50 || ' minutes')::interval
|
||||
ELSE NULL END,
|
||||
CASE WHEN random() < 0.92 THEN false ELSE true END,
|
||||
'WEB',
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- Magnesium: last 60 days only (med started 70 days ago, ramp-up), 88% taken
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 59) AS d)
|
||||
INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
SELECT
|
||||
'ie_mg_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'demo_med_mg',
|
||||
date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '20 hours',
|
||||
CASE WHEN random() < 0.88
|
||||
THEN date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '20 hours' + (random() * 60 || ' minutes')::interval
|
||||
ELSE NULL END,
|
||||
CASE WHEN random() < 0.88 THEN false ELSE true END,
|
||||
'WEB',
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM days;
|
||||
|
||||
-- Ozempic: every Monday in the last 84 days (≈12 Mondays), all taken
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
mondays AS (
|
||||
SELECT generate_series(
|
||||
date_trunc('week', NOW())::date - 84,
|
||||
date_trunc('week', NOW())::date,
|
||||
INTERVAL '7 days'
|
||||
)::date AS day
|
||||
)
|
||||
INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
SELECT
|
||||
'ie_oz_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
'demo_med_ozempic',
|
||||
day + INTERVAL '19 hours 30 minutes',
|
||||
day + INTERVAL '19 hours 30 minutes' + (random() * 60 || ' minutes')::interval,
|
||||
false,
|
||||
'WEB',
|
||||
day::timestamp
|
||||
FROM mondays;
|
||||
|
||||
-- ── Mood entries ─────────────────────────────────────────────────
|
||||
-- 90 days, ~95% logged; trends from OKAY/GUT toward SUPER_GUT.
|
||||
WITH demo AS (SELECT user_id FROM _demo),
|
||||
days AS (SELECT generate_series(0, 89) AS d)
|
||||
INSERT INTO mood_entries (
|
||||
id, user_id, date, mood, score, tags, note, source, mood_logged_at, tz, synced_at, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'mo_' || replace(gen_random_uuid()::text, '-', ''),
|
||||
(SELECT user_id FROM demo),
|
||||
(NOW() - (d || ' days')::interval)::date::text,
|
||||
CASE
|
||||
WHEN sc = 5 THEN 'SUPER_GUT'
|
||||
WHEN sc = 4 THEN 'GUT'
|
||||
WHEN sc = 3 THEN 'OKAY'
|
||||
WHEN sc = 2 THEN 'SCHLECHT'
|
||||
ELSE 'LAUSIG'
|
||||
END,
|
||||
sc,
|
||||
CASE sc
|
||||
WHEN 5 THEN '["productive","exercise","well-rested"]'
|
||||
WHEN 4 THEN '["focused","social","creative"]'
|
||||
WHEN 3 THEN '["okay","quiet"]'
|
||||
WHEN 2 THEN '["tired","stressed"]'
|
||||
ELSE '["exhausted","pain"]'
|
||||
END,
|
||||
NULL,
|
||||
'WEB',
|
||||
date_trunc('day', NOW() - (d || ' days')::interval) + INTERVAL '21 hours' + (random() * 50 || ' minutes')::interval,
|
||||
'Europe/Berlin',
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval,
|
||||
NOW() - (d || ' days')::interval
|
||||
FROM (
|
||||
SELECT d,
|
||||
-- Score trends upward from ~3 to ~4.5 over 90 days + noise.
|
||||
LEAST(5, GREATEST(1,
|
||||
round((3.0 + (90 - d) * 0.018 + (random() - 0.5) * 1.2))::int
|
||||
)) AS sc
|
||||
FROM days
|
||||
WHERE random() > 0.05 -- ~5% missed days
|
||||
) ranked;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ── Summary report ───────────────────────────────────────────────
|
||||
SELECT 'measurements_total' AS scope, COUNT(*)::text AS n FROM measurements WHERE user_id IN (SELECT id FROM users WHERE username = 'demo')
|
||||
UNION ALL
|
||||
SELECT 'measurements_by_type', COUNT(DISTINCT type)::text FROM measurements WHERE user_id IN (SELECT id FROM users WHERE username = 'demo')
|
||||
UNION ALL
|
||||
SELECT 'mood_entries', COUNT(*)::text FROM mood_entries WHERE user_id IN (SELECT id FROM users WHERE username = 'demo')
|
||||
UNION ALL
|
||||
SELECT 'medications', COUNT(*)::text FROM medications WHERE user_id IN (SELECT id FROM users WHERE username = 'demo')
|
||||
UNION ALL
|
||||
SELECT 'intake_events', COUNT(*)::text FROM medication_intake_events WHERE user_id IN (SELECT id FROM users WHERE username = 'demo')
|
||||
UNION ALL
|
||||
SELECT 'measurement_rollups', COUNT(*)::text FROM measurement_rollups WHERE user_id IN (SELECT id FROM users WHERE username = 'demo');
|
||||
Reference in new issue
Block a user