diff --git a/docs/audit/2026-04-26-PLAN.md b/docs/audit/2026-04-26-PLAN.md new file mode 100644 index 000000000..a34f1784e --- /dev/null +++ b/docs/audit/2026-04-26-PLAN.md @@ -0,0 +1,235 @@ +# 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}(path, body?, schema?) → Result` +- 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)