From 2436dd9cac2d630cb23f6a31c715baeb4900e886 Mon Sep 17 00:00:00 2001 From: Sebastian Mayer Date: Wed, 27 May 2026 22:15:16 +0200 Subject: [PATCH] Initial commit: Hellth Hub monorepo Mobile-first Web-App fuer persoenliches Health-, Ernaehrungs-, Aktivitaets- und Einkaufs-Tracking im Home-Lab-Betrieb. Stack: pnpm Monorepo - Next.js 16 + React 19 + Tailwind (apps/admin) - Hono + tRPC + Better Auth mit 2FA und Passkeys (packages/api) - PostgreSQL + Drizzle ORM (packages/db) - Docker fuer Postgres und pgAdmin. Module: - Auth mit Login, 2FA (TOTP + Backup-Codes), Passkeys und Admin-Recovery - Tagesansicht mit Essen, Trinken, Gewicht, Aktivitaeten und Coach-Hinweisen - Lebensmittelkatalog mit Naehrwerten, Portionen, OCR/Barcode/Open Food Facts - Rezepte mit Live-Naehrwertberechnung und Community-Vorschlaegen - Essensplanung und Wochenbudget mit Sportbonus - Einkaufslisten mit Maerkten und Laufweg-Sortierung - Hellfireclub mit Aktivitaetspunkten und gemeinsamer Aktivitaetsplanung - Wearables-Vorbereitung (Fitbit / Google Fit / Health Connect) Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules.md | 95 + .claude/settings.json | 14 + .env.example | 29 + .gitignore | 47 + .npmrc | 4 + AGENTS.md | 24 + CLAUDE.md | 11 + README.md | 40 + apps/admin/app/(authed)/aktivitaeten/page.tsx | 371 + .../app/(authed)/aktivitaetsfeed/page.tsx | 84 + apps/admin/app/(authed)/benutzer/page.tsx | 187 + .../admin/app/(authed)/einkaufsliste/page.tsx | 135 + .../admin/app/(authed)/einstellungen/page.tsx | 779 ++ .../(authed)/einstellungen/punkte/page.tsx | 169 + apps/admin/app/(authed)/erinnerungen/page.tsx | 245 + apps/admin/app/(authed)/gewicht/page.tsx | 201 + apps/admin/app/(authed)/hellfireclub/page.tsx | 477 + apps/admin/app/(authed)/layout.tsx | 23 + apps/admin/app/(authed)/lebensmittel/page.tsx | 451 + apps/admin/app/(authed)/maerkte/page.tsx | 335 + apps/admin/app/(authed)/onboarding/page.tsx | 316 + apps/admin/app/(authed)/page.tsx | 757 ++ apps/admin/app/(authed)/planung/page.tsx | 324 + .../(authed)/rezepte/eier-muffins/page.tsx | 192 + apps/admin/app/(authed)/rezepte/page.tsx | 475 + .../app/(authed)/rezepte/pruefung/page.tsx | 226 + .../(authed)/sicherheit/2fa-setup/page.tsx | 262 + apps/admin/app/(authed)/statistiken/page.tsx | 213 + apps/admin/app/(authed)/wearables/page.tsx | 109 + apps/admin/app/(authed)/wochenbudget/page.tsx | 165 + apps/admin/app/(authed)/ziele/page.tsx | 142 + apps/admin/app/_components/auth-guard.tsx | 109 + .../app/_components/data-page-layout.tsx | 22 + apps/admin/app/_components/empty-state.tsx | 9 + apps/admin/app/_components/form-field.tsx | 20 + apps/admin/app/_components/help-link.tsx | 31 + apps/admin/app/_components/mobile-nav.tsx | 59 + .../app/_components/news-blocknote-editor.tsx | 141 + apps/admin/app/_components/page-header.tsx | 25 + apps/admin/app/_components/providers.tsx | 67 + .../admin/app/_components/reminder-runner.tsx | 86 + .../app/_components/rich-text-editor.tsx | 459 + apps/admin/app/_components/sidebar.tsx | 263 + apps/admin/app/_components/theme-provider.tsx | 67 + apps/admin/app/globals.css | 87 + apps/admin/app/layout.tsx | 22 + apps/admin/app/login/otp/page.tsx | 188 + apps/admin/app/login/page.tsx | 272 + apps/admin/lib/auth-client.ts | 53 + apps/admin/lib/hellth-data.test.ts | 288 + apps/admin/lib/hellth-data.ts | 1149 +++ apps/admin/lib/news-list-filters.ts | 107 + apps/admin/lib/page-editor.ts | 82 + apps/admin/lib/trpc.ts | 4 + apps/admin/next-env.d.ts | 6 + apps/admin/next.config.ts | 24 + apps/admin/package.json | 54 + apps/admin/postcss.config.mjs | 7 + apps/admin/public/hellth-sw.js | 12 + apps/admin/tsconfig.json | 29 + claude-info.md | 370 + docker/docker-compose.yml | 40 + docker/pgadmin/servers.json | 14 + docs/INDEX.md | 13 + docs/ai/INDEX.md | 3 + docs/ai/quick-reference.md | 20 + docs/design-guide.md | 20 + .../Screenshot_20260516-114113.png | Bin 0 -> 130043 bytes .../Screenshot_20260516-114122.png | Bin 0 -> 136537 bytes .../Screenshot_20260516-114130.png | Bin 0 -> 117444 bytes .../Screenshot_20260516-114142.png | Bin 0 -> 99551 bytes .../Screenshot_20260516-114155.png | Bin 0 -> 106453 bytes .../Screenshot_20260516-114204.png | Bin 0 -> 164713 bytes .../Screenshot_20260516-114211.png | Bin 0 -> 162848 bytes .../Screenshot_20260516-114310.png | Bin 0 -> 362578 bytes .../Screenshot_20260516-114321.png | Bin 0 -> 108149 bytes .../Screenshot_20260516-114328.png | Bin 0 -> 104452 bytes .../Rezept/Screenshot_20260516-114336.png | Bin 0 -> 494552 bytes .../Rezept/Screenshot_20260516-114347.png | Bin 0 -> 139364 bytes .../Rezept/Screenshot_20260516-114359.png | Bin 0 -> 174414 bytes .../Screenshot_20260516-114227.png | Bin 0 -> 233284 bytes .../Screenshot_20260516-114232.png | Bin 0 -> 403680 bytes .../Screenshot_20260516-114247.png | Bin 0 -> 144680 bytes .../Screenshot_20260516-114255.png | Bin 0 -> 170727 bytes .../Screenshot_20260516-114300.png | Bin 0 -> 164270 bytes docs/projekt.md | 29 + docs/security.md | 18 + package.json | 21 + packages/api/package.json | 40 + packages/api/src/app.ts | 177 + packages/api/src/errors/codes.ts | 11 + packages/api/src/index.ts | 20 + packages/api/src/lib/auth.ts | 114 + packages/api/src/lib/health-coach.test.ts | 56 + packages/api/src/lib/health-coach.ts | 115 + packages/api/src/lib/html-sanitize.ts | 62 + packages/api/src/lib/request-host.ts | 31 + packages/api/src/lib/runtime-security.test.ts | 47 + packages/api/src/lib/runtime-security.ts | 43 + packages/api/src/lib/security-audit.ts | 26 + packages/api/src/lib/uploads.ts | 47 + packages/api/src/scripts/seed-admin.ts | 77 + packages/api/src/scripts/seed-demo-recipes.ts | 125 + packages/api/src/scripts/seed-health.ts | 181 + packages/api/src/trpc/client.ts | 1 + packages/api/src/trpc/context.ts | 31 + packages/api/src/trpc/health.test.ts | 591 ++ packages/api/src/trpc/index.ts | 12 + packages/api/src/trpc/init.ts | 98 + packages/api/src/trpc/routers/health.ts | 2419 +++++ packages/api/src/trpc/routers/security.ts | 73 + packages/api/src/trpc/routers/users.ts | 291 + packages/api/tsconfig.json | 15 + packages/db/drizzle.config.ts | 13 + packages/db/drizzle/0000_wealthy_korg.sql | 322 + packages/db/drizzle/0001_salty_shape.sql | 1 + .../db/drizzle/0002_silent_puppet_master.sql | 69 + packages/db/drizzle/0003_empty_the_spike.sql | 22 + .../db/drizzle/0004_motionless_vulcan.sql | 9 + packages/db/drizzle/0005_green_jocasta.sql | 41 + .../db/drizzle/0006_reflective_fenris.sql | 21 + packages/db/drizzle/0007_organic_makkari.sql | 5 + .../db/drizzle/0008_stiff_sebastian_shaw.sql | 13 + .../db/drizzle/0009_aspiring_power_pack.sql | 399 + packages/db/drizzle/0010_polite_caretaker.sql | 45 + .../db/drizzle/0011_lame_robin_chapel.sql | 17 + .../db/drizzle/0012_right_yellowjacket.sql | 22 + packages/db/drizzle/0013_club_api_bridge.sql | 17 + ...4_store_preferences_meal_plan_calories.sql | 12 + .../db/drizzle/0015_recipe_submissions.sql | 37 + .../db/drizzle/0016_recipe_preferences.sql | 18 + .../db/drizzle/0017_store_change_requests.sql | 30 + packages/db/drizzle/meta/0000_snapshot.json | 2231 +++++ packages/db/drizzle/meta/0001_snapshot.json | 2237 +++++ packages/db/drizzle/meta/0002_snapshot.json | 2501 +++++ packages/db/drizzle/meta/0003_snapshot.json | 2670 ++++++ packages/db/drizzle/meta/0004_snapshot.json | 2706 ++++++ packages/db/drizzle/meta/0005_snapshot.json | 2960 ++++++ packages/db/drizzle/meta/0006_snapshot.json | 3124 +++++++ packages/db/drizzle/meta/0007_snapshot.json | 3156 +++++++ packages/db/drizzle/meta/0008_snapshot.json | 3262 +++++++ packages/db/drizzle/meta/0009_snapshot.json | 6223 +++++++++++++ packages/db/drizzle/meta/0010_snapshot.json | 3746 ++++++++ packages/db/drizzle/meta/0011_snapshot.json | 3588 ++++++++ packages/db/drizzle/meta/0012_snapshot.json | 3756 ++++++++ packages/db/drizzle/meta/_journal.json | 132 + packages/db/package.json | 30 + packages/db/src/index.ts | 25 + packages/db/src/migrate.ts | 28 + packages/db/src/schema/auth.ts | 92 + packages/db/src/schema/enums.ts | 77 + packages/db/src/schema/health.ts | 636 ++ packages/db/src/schema/index.ts | 4 + packages/db/src/schema/platform-settings.ts | 10 + packages/db/tsconfig.json | 15 + pnpm-lock.yaml | 8173 +++++++++++++++++ pnpm-workspace.yaml | 3 + turbo.json | 31 + 158 files changed, 68424 insertions(+) create mode 100644 .claude/rules.md create mode 100644 .claude/settings.json create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 apps/admin/app/(authed)/aktivitaeten/page.tsx create mode 100644 apps/admin/app/(authed)/aktivitaetsfeed/page.tsx create mode 100644 apps/admin/app/(authed)/benutzer/page.tsx create mode 100644 apps/admin/app/(authed)/einkaufsliste/page.tsx create mode 100644 apps/admin/app/(authed)/einstellungen/page.tsx create mode 100644 apps/admin/app/(authed)/einstellungen/punkte/page.tsx create mode 100644 apps/admin/app/(authed)/erinnerungen/page.tsx create mode 100644 apps/admin/app/(authed)/gewicht/page.tsx create mode 100644 apps/admin/app/(authed)/hellfireclub/page.tsx create mode 100644 apps/admin/app/(authed)/layout.tsx create mode 100644 apps/admin/app/(authed)/lebensmittel/page.tsx create mode 100644 apps/admin/app/(authed)/maerkte/page.tsx create mode 100644 apps/admin/app/(authed)/onboarding/page.tsx create mode 100644 apps/admin/app/(authed)/page.tsx create mode 100644 apps/admin/app/(authed)/planung/page.tsx create mode 100644 apps/admin/app/(authed)/rezepte/eier-muffins/page.tsx create mode 100644 apps/admin/app/(authed)/rezepte/page.tsx create mode 100644 apps/admin/app/(authed)/rezepte/pruefung/page.tsx create mode 100644 apps/admin/app/(authed)/sicherheit/2fa-setup/page.tsx create mode 100644 apps/admin/app/(authed)/statistiken/page.tsx create mode 100644 apps/admin/app/(authed)/wearables/page.tsx create mode 100644 apps/admin/app/(authed)/wochenbudget/page.tsx create mode 100644 apps/admin/app/(authed)/ziele/page.tsx create mode 100644 apps/admin/app/_components/auth-guard.tsx create mode 100644 apps/admin/app/_components/data-page-layout.tsx create mode 100644 apps/admin/app/_components/empty-state.tsx create mode 100644 apps/admin/app/_components/form-field.tsx create mode 100644 apps/admin/app/_components/help-link.tsx create mode 100644 apps/admin/app/_components/mobile-nav.tsx create mode 100644 apps/admin/app/_components/news-blocknote-editor.tsx create mode 100644 apps/admin/app/_components/page-header.tsx create mode 100644 apps/admin/app/_components/providers.tsx create mode 100644 apps/admin/app/_components/reminder-runner.tsx create mode 100644 apps/admin/app/_components/rich-text-editor.tsx create mode 100644 apps/admin/app/_components/sidebar.tsx create mode 100644 apps/admin/app/_components/theme-provider.tsx create mode 100644 apps/admin/app/globals.css create mode 100644 apps/admin/app/layout.tsx create mode 100644 apps/admin/app/login/otp/page.tsx create mode 100644 apps/admin/app/login/page.tsx create mode 100644 apps/admin/lib/auth-client.ts create mode 100644 apps/admin/lib/hellth-data.test.ts create mode 100644 apps/admin/lib/hellth-data.ts create mode 100644 apps/admin/lib/news-list-filters.ts create mode 100644 apps/admin/lib/page-editor.ts create mode 100644 apps/admin/lib/trpc.ts create mode 100644 apps/admin/next-env.d.ts create mode 100644 apps/admin/next.config.ts create mode 100644 apps/admin/package.json create mode 100644 apps/admin/postcss.config.mjs create mode 100644 apps/admin/public/hellth-sw.js create mode 100644 apps/admin/tsconfig.json create mode 100644 claude-info.md create mode 100644 docker/docker-compose.yml create mode 100644 docker/pgadmin/servers.json create mode 100644 docs/INDEX.md create mode 100644 docs/ai/INDEX.md create mode 100644 docs/ai/quick-reference.md create mode 100644 docs/design-guide.md create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114113.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114122.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114130.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114142.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114155.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114204.png create mode 100644 docs/inspiration/bitepal/Einstellungen/Screenshot_20260516-114211.png create mode 100644 docs/inspiration/bitepal/Ernährungsliste/Screenshot_20260516-114310.png create mode 100644 docs/inspiration/bitepal/Ernährungsliste/Screenshot_20260516-114321.png create mode 100644 docs/inspiration/bitepal/Ernährungsliste/Screenshot_20260516-114328.png create mode 100644 docs/inspiration/bitepal/Rezept/Screenshot_20260516-114336.png create mode 100644 docs/inspiration/bitepal/Rezept/Screenshot_20260516-114347.png create mode 100644 docs/inspiration/bitepal/Rezept/Screenshot_20260516-114359.png create mode 100644 docs/inspiration/bitepal/Tagesansicht/Screenshot_20260516-114227.png create mode 100644 docs/inspiration/bitepal/Tagesansicht/Screenshot_20260516-114232.png create mode 100644 docs/inspiration/bitepal/Tagesansicht/Screenshot_20260516-114247.png create mode 100644 docs/inspiration/bitepal/Tagesansicht/Screenshot_20260516-114255.png create mode 100644 docs/inspiration/bitepal/Tagesansicht/Screenshot_20260516-114300.png create mode 100644 docs/projekt.md create mode 100644 docs/security.md create mode 100644 package.json create mode 100644 packages/api/package.json create mode 100644 packages/api/src/app.ts create mode 100644 packages/api/src/errors/codes.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/lib/auth.ts create mode 100644 packages/api/src/lib/health-coach.test.ts create mode 100644 packages/api/src/lib/health-coach.ts create mode 100644 packages/api/src/lib/html-sanitize.ts create mode 100644 packages/api/src/lib/request-host.ts create mode 100644 packages/api/src/lib/runtime-security.test.ts create mode 100644 packages/api/src/lib/runtime-security.ts create mode 100644 packages/api/src/lib/security-audit.ts create mode 100644 packages/api/src/lib/uploads.ts create mode 100644 packages/api/src/scripts/seed-admin.ts create mode 100644 packages/api/src/scripts/seed-demo-recipes.ts create mode 100644 packages/api/src/scripts/seed-health.ts create mode 100644 packages/api/src/trpc/client.ts create mode 100644 packages/api/src/trpc/context.ts create mode 100644 packages/api/src/trpc/health.test.ts create mode 100644 packages/api/src/trpc/index.ts create mode 100644 packages/api/src/trpc/init.ts create mode 100644 packages/api/src/trpc/routers/health.ts create mode 100644 packages/api/src/trpc/routers/security.ts create mode 100644 packages/api/src/trpc/routers/users.ts create mode 100644 packages/api/tsconfig.json create mode 100644 packages/db/drizzle.config.ts create mode 100644 packages/db/drizzle/0000_wealthy_korg.sql create mode 100644 packages/db/drizzle/0001_salty_shape.sql create mode 100644 packages/db/drizzle/0002_silent_puppet_master.sql create mode 100644 packages/db/drizzle/0003_empty_the_spike.sql create mode 100644 packages/db/drizzle/0004_motionless_vulcan.sql create mode 100644 packages/db/drizzle/0005_green_jocasta.sql create mode 100644 packages/db/drizzle/0006_reflective_fenris.sql create mode 100644 packages/db/drizzle/0007_organic_makkari.sql create mode 100644 packages/db/drizzle/0008_stiff_sebastian_shaw.sql create mode 100644 packages/db/drizzle/0009_aspiring_power_pack.sql create mode 100644 packages/db/drizzle/0010_polite_caretaker.sql create mode 100644 packages/db/drizzle/0011_lame_robin_chapel.sql create mode 100644 packages/db/drizzle/0012_right_yellowjacket.sql create mode 100644 packages/db/drizzle/0013_club_api_bridge.sql create mode 100644 packages/db/drizzle/0014_store_preferences_meal_plan_calories.sql create mode 100644 packages/db/drizzle/0015_recipe_submissions.sql create mode 100644 packages/db/drizzle/0016_recipe_preferences.sql create mode 100644 packages/db/drizzle/0017_store_change_requests.sql create mode 100644 packages/db/drizzle/meta/0000_snapshot.json create mode 100644 packages/db/drizzle/meta/0001_snapshot.json create mode 100644 packages/db/drizzle/meta/0002_snapshot.json create mode 100644 packages/db/drizzle/meta/0003_snapshot.json create mode 100644 packages/db/drizzle/meta/0004_snapshot.json create mode 100644 packages/db/drizzle/meta/0005_snapshot.json create mode 100644 packages/db/drizzle/meta/0006_snapshot.json create mode 100644 packages/db/drizzle/meta/0007_snapshot.json create mode 100644 packages/db/drizzle/meta/0008_snapshot.json create mode 100644 packages/db/drizzle/meta/0009_snapshot.json create mode 100644 packages/db/drizzle/meta/0010_snapshot.json create mode 100644 packages/db/drizzle/meta/0011_snapshot.json create mode 100644 packages/db/drizzle/meta/0012_snapshot.json create mode 100644 packages/db/drizzle/meta/_journal.json create mode 100644 packages/db/package.json create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/src/migrate.ts create mode 100644 packages/db/src/schema/auth.ts create mode 100644 packages/db/src/schema/enums.ts create mode 100644 packages/db/src/schema/health.ts create mode 100644 packages/db/src/schema/index.ts create mode 100644 packages/db/src/schema/platform-settings.ts create mode 100644 packages/db/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 turbo.json diff --git a/.claude/rules.md b/.claude/rules.md new file mode 100644 index 0000000..c1cbc27 --- /dev/null +++ b/.claude/rules.md @@ -0,0 +1,95 @@ +# Claude Project Rules - Hellth Hub + +## 1. Type Safety + +- TypeScript strict mode +- Kein `any` +- Keine unsicheren Type Assertions (`as X`) +- Drizzle-Query-Builder-Typen nutzen statt eigene Interfaces dupliziert beschreiben + +## 2. Trennung der Verantwortlichkeiten + +``` +Business-Logik → packages/api (tRPC Router, Coach, Domain-Helper) +DB-Logik → packages/db (Drizzle-Schema, Migrationen) +Apps → apps/admin (nur UI + dünner Glue zur API) +``` + +Die Admin-App enthält keine Business-Logik. Wenn Berechnung in beiden Seiten gebraucht wird (z. B. Nährwerte), gehört der Kanon in `packages/api/src/lib/`; das Frontend bekommt ihn über tRPC oder dupliziert nur die UI-nahe Anzeige-Variante in `apps/admin/lib/hellth-data.ts`. + +## 3. Domäne + +Hellth Hub ist eine Single-User-Health-App pro Benutzer-Account. Datenmodell siehe `packages/db/src/schema/health.ts` (Foods, Recipes, MealLogs, ShoppingLists, Stores, ActivityLogs, Wearables, ClubActivities, Coach …). + +Keine Naturfreunde-Begriffe wiederbeleben (News, Events, Fachgruppen, Mandanten, Häuser, Buchungen, `tenantId`, `verband_admin`, `haus_admin`, Mitgliedsausweise, Google-Wallet). + +## 4. Rollen + +- `user` (default) - normaler Benutzer +- `admin` - Plattform-Admin (z. B. `admin@onl1.eu`) +- Admins haben Pflicht-2FA (siehe `enforceTwoFactorForPrivilegedUsers` in `packages/api/src/trpc/init.ts`) + +Rechte immer **serverseitig** prüfen (tRPC-Procedure-Auswahl: `protectedProcedure` für eingeloggte User, `platformProcedure` für Admins). + +## 5. Performance + +- Keine N+1-Queries (Drizzle `.with()` / Joins / `inArray` für Batch-Loads nutzen) +- Listen-Endpoints mit Suche: Filterung auf DB-Seite (`ilike`), nicht in-Memory +- Index-Pfade respektieren (siehe Index-Definitionen in `schema/health.ts`) + +## 6. Sicherheit + +- Auth-Pflicht für alle Routen außer Login / Health-Check +- Keine Secrets im Repo - nur `.env` (in `.gitignore`) +- 2FA-Recovery nur für Admins (`platformProcedure`) +- Bei `twoFactorRecoveryRequired = true` muss 2FA-Neueinrichtung erzwungen werden +- Login auf `/login/otp` muss sowohl TOTP-Code als auch Backup-Code unterstützen +- Better-Auth-Konfiguration (`packages/api/src/lib/auth.ts`) ist Single Source of Truth für Auth-Optionen +- CSP / Security-Headers in `apps/admin/next.config.ts` und `packages/api/src/app.ts` synchron halten + +### User-Security-Felder (aktiv) + +``` +twoFactorEnabled +twoFactorRecoveryRequired +twoFactorRecoveryStartedAt +twoFactorRecoveryStartedBy +``` + +## 7. UI-Regeln + +- Mobile-first - Hellth Hub wird primär am Handy benutzt +- Klar, modern, funktional - keine visuellen Spielereien +- Bottom-Navigation auf Mobile, Sidebar auf Desktop (`apps/admin/app/_components/`) +- Neue Formulare über `FormField` und `inputClassName` aus `apps/admin/app/_components/form-field.tsx` +- Deutsche UI: echte Umlaute (`ä`, `ö`, `ü`, `ß`) +- UI-Inspiration siehe `docs/inspiration/bitepal/` + +## 8. DB-Änderungen + +- Schema-Änderung, Migration, API-Endpoint und UI immer zusammen denken +- `pnpm --filter @hellth/db db:generate` für Migrationen +- Migrationen nicht nachträglich umbenennen oder löschen (Drizzle tracked sie per Datei + Hash) +- Neue Tabellen brauchen passende Indizes für die Haupt-Queries + +## 9. Tests + +Mindestens absichern: +- Auth- und 2FA-Pfade +- Nährwert- / Kalorien- / Wochenbudget-Berechnung +- Hellfireclub-Punkte-Logik + +Vor PR / größerer Änderung lokal grün: + +```powershell +pnpm --filter @hellth/api test +pnpm --filter admin test +pnpm exec tsc --noEmit --project packages/api/tsconfig.json +$env:NEXT_PUBLIC_API_URL='http://localhost:3002'; pnpm --filter admin build +``` + +## 10. Arbeitsweise mit Claude + +- Bestehende User-Änderungen nicht zurücksetzen +- Keine destruktiven Git-Befehle ohne ausdrückliche Anweisung +- Cleanup an Altlasten nur nach expliziter Freigabe (siehe `claude-info.md` Abschnitt "Bekannte Altlasten") diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..bd5e8df --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(wc:*)", + "Bash(find:*)", + "Bash(pnpm --filter @hellth/db db:push:*)", + "Bash(pnpm --filter @hellth/db db:generate:*)", + "Bash(pnpm --filter @hellth/db db:migrate:*)", + "Bash(pnpm --filter @hellth/api test:*)", + "Bash(pnpm --filter admin test:*)", + "Bash(pnpm exec tsc:*)" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb4d82f --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# Datenbank +DATABASE_URL=postgresql://hellth:hellth_dev@localhost:5432/hellth_hub + +# Better Auth, in Produktion durch starkes Secret ersetzen +BETTER_AUTH_SECRET=change-me-in-production-use-openssl-rand-base64-32 +BETTER_AUTH_BASE_URL=http://localhost:3002 +PASSKEY_RP_NAME=Hellth Hub + +# API +API_PORT=3002 +TRUSTED_ORIGINS=http://localhost:3001 +NEXT_PUBLIC_API_URL=http://localhost:3002 +TRUST_PROXY_FORWARD_HEADERS=false +DATABASE_REQUIRE_SSL=false +ENABLE_DB_MIGRATIONS=false + +# Plattform-Admin, kommasepariert +PLATFORM_ADMIN_EMAILS=admin@domain.tld +NEXT_PUBLIC_PLATFORM_ADMIN_EMAILS=admin@domain.tld + +# Initialer Admin-User +ADMIN_EMAIL=admin@domain.tld +ADMIN_INITIAL_PASSWORD=please-set-a-strong-password-with-16-plus-chars + +# Wearables: Fitbit OAuth, optional +FITBIT_CLIENT_ID= +FITBIT_CLIENT_SECRET= +FITBIT_REDIRECT_URI=http://localhost:3002/api/wearables/fitbit/callback +WEARABLE_TOKEN_ENCRYPTION_KEY=change-me-32-byte-base64-key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6a4c14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build/Cache +.next/ +out/ +dist/ +build/ +.turbo/ +.cache/ +coverage/ + +# Logs +*.log +.logs/ +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Env/Secrets +.env +.env.* +!.env.example + +# OS/Editor +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Runtime/Uploads +uploads/ +tmp/ +temp/ + +# Python (for scripts/) +__pycache__/ +*.pyc +.pytest_cache/ + +# Local machine-specific agent settings +.claude/settings.local.json + +# Project-specific +credentials.txt diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..680fc53 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +onlyBuiltDependencies[]=esbuild +onlyBuiltDependencies[]=sharp +onlyBuiltDependencies[]=unrs-resolver +shamefully-hoist=true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..152527c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# Agent-Regeln - Hellth Hub + +Regeln für AI-Agents (Codex, Claude, weitere Assistenten) in diesem Repository. + +## Ziel + +- Einheitliche Arbeitsweise für AI-gestützte Änderungen. +- Klare Priorität bei möglichen Konflikten zwischen Markdown-Quellen. +- Hellth Hub bleibt fachlich eine Health- und Ernährungstracking-App. + +## Lesereihenfolge für Agents + +1. `AGENTS.md` +2. `docs/ai/quick-reference.md` +3. `docs/INDEX.md` +4. Fachliche Detaildokumente nach Bedarf + +## Grundregeln + +- Umlaute immer korrekt schreiben. +- Keine alten Altprojekt-, Tenant- oder Community-Portal-Begriffe in UI, API oder Doku einführen. +- Benutzerbezogene Einstellungen bleiben pro User gespeichert. +- Auth, 2FA, Admin-UI und Docker-Setup bleiben lauffähig. +- Keine Secrets in Markdown-Dateien speichern. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dfc836e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +# Claude Working Guidelines - Hellth Hub + +Hellth Hub ist eine mobile-first Health- und Ernährungstracking-App für den Home-Lab-Betrieb. + +Wichtig: + +- Umlaute immer korrekt schreiben. +- Keine alten Altprojekt-/Tenant-/Community-Begriffe in neue UI oder Doku übernehmen. +- Einstellungen und Health-Daten sind benutzerbezogen, nicht global. +- Bestehende Architektur nutzen: `apps/admin`, `packages/api`, `packages/db`. +- Änderungen mit Tests absichern, besonders bei Auth, Health-Daten, Wochenbudget und Einkaufslisten. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dac1da --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Hellth Hub + +Mobile-first Web-App für persönliches Health- und Ernährungstracking im Home-Lab. + +## Lokaler Start + +```bash +docker compose -f docker/docker-compose.yml up -d db +pnpm --filter @hellth/db db:migrate +pnpm --filter @hellth/api seed:admin +pnpm --filter @hellth/api seed:health +pnpm --filter @hellth/api dev +pnpm --filter admin dev +``` + +Danach: + +- Admin-UI: http://localhost:3001 +- API: http://localhost:3002 +- Health Check: http://localhost:3002/api/health + +## Aktuelle Module + +- Auth, Login, 2FA, Passkeys und Benutzerverwaltung +- Tagesansicht mit Essen, Trinken, Gewicht, Aktivitäten und Coach-Hinweisen +- Lebensmittelkatalog mit Nährwerten, Portionen, Favoriten und OCR-Vorbereitung +- Rezepte mit flexiblen Mengenangaben, Bewertungen und Meal-Prep-Bezug +- Essensplanung mit Tages- und Wochenlogik +- Wochenbudget für Kalorien inklusive Sportbonus +- Einkaufsliste mit Märkten, Abteilungen und Laufweg-Sortierung +- Ziele, Onboarding und persönliche Einstellungen pro Benutzer +- Erinnerungen, Aktivitätspunkte, Hellfireclub und Wearable-Vorbereitung + +## Verifikation + +```bash +pnpm --filter @hellth/api test -- --test-reporter=spec +pnpm --filter admin test +NEXT_PUBLIC_API_URL=http://localhost:3002 pnpm --filter admin build +``` \ No newline at end of file diff --git a/apps/admin/app/(authed)/aktivitaeten/page.tsx b/apps/admin/app/(authed)/aktivitaeten/page.tsx new file mode 100644 index 0000000..c9b7b0b --- /dev/null +++ b/apps/admin/app/(authed)/aktivitaeten/page.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { + BoltIcon, + CalendarDaysIcon, + FireIcon, + PlusIcon, + SparklesIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import { useMemo, useState } from "react"; +import { trpc } from "@/lib/trpc"; + +type TemplateCategory = "mobility" | "cardio" | "strength" | "habit" | string; +type ActivitySource = "manual" | "fitbit"; + +function todayKey() { + return new Date().toISOString().slice(0, 10); +} + +function startOfWeek(dateKey: string) { + const date = new Date(`${dateKey}T12:00:00`); + const day = date.getDay() === 0 ? 7 : date.getDay(); + date.setDate(date.getDate() - day + 1); + return date.toISOString().slice(0, 10); +} + +function pointsLabel(points: number) { + if (points >= 500) return "Starke Woche"; + if (points >= 250) return "Guter Lauf"; + if (points >= 100) return "In Bewegung"; + return "Startklar"; +} + +function categoryColor(category: TemplateCategory) { + if (category.includes("cardio")) return "bg-orange-50 text-orange-700"; + if (category.includes("strength")) return "bg-blue-50 text-blue-700"; + if (category.includes("mobility")) return "bg-emerald-50 text-emerald-700"; + if (category.includes("sport")) return "bg-amber-50 text-amber-700"; + return "bg-purple-50 text-purple-700"; +} + +function categoryLabel(category: TemplateCategory) { + if (category.includes("cardio")) return "Ausdauer"; + if (category.includes("strength")) return "Kraft"; + if (category.includes("mobility")) return "Beweglichkeit"; + if (category.includes("sport")) return "Sport"; + return "Routine"; +} + +function sourceLabel(source: string) { + if (source === "fitbit") return "Fitbit"; + if (source === "health_connect") return "Health Connect"; + return "manuell"; +} + +function estimatePoints(minutes: number, calories: number) { + return Math.max(5, Math.round(minutes * 1.2 + calories / 6)); +} + +export default function AktivitätenPage() { + const utils = trpc.useUtils(); + const [selectedDate, setSelectedDate] = useState(todayKey()); + const [templateQuery, setTemplateQuery] = useState(""); + const [templateCategory, setTemplateCategory] = useState<"all" | "fitbit" | "manual">("all"); + const [customName, setCustomName] = useState(""); + const [customMinutes, setCustomMinutes] = useState("15"); + const [customPoints, setCustomPoints] = useState("30"); + const [customCalories, setCustomCalories] = useState("60"); + const [customNote, setCustomNote] = useState(""); + const [customSource, setCustomSource] = useState("manual"); + + const templatesQuery = trpc.health.listActivityTemplates.useQuery(); + const activityWeekQuery = trpc.health.activityWeek.useQuery({ date: selectedDate }); + const addLogMutation = trpc.health.addActivityLog.useMutation({ + onSuccess: async () => { + await utils.health.activityWeek.invalidate(); + await utils.health.overview.invalidate(); + }, + }); + const saveTemplateMutation = trpc.health.saveActivityTemplate.useMutation({ + onSuccess: async () => { + await utils.health.listActivityTemplates.invalidate(); + setCustomName(""); + }, + }); + const deleteLogMutation = trpc.health.deleteActivityLog.useMutation({ + onSuccess: async () => { + await utils.health.activityWeek.invalidate(); + await utils.health.overview.invalidate(); + }, + }); + + const logs = activityWeekQuery.data?.logs ?? []; + const templates = templatesQuery.data ?? []; + const filteredTemplates = useMemo(() => { + const needle = templateQuery.trim().toLowerCase(); + return templates.filter((template) => { + const isFitbit = template.id.startsWith("fitbit-") || template.category.startsWith("fitbit_"); + if (templateCategory === "fitbit" && !isFitbit) return false; + if (templateCategory === "manual" && isFitbit) return false; + if (!needle) return true; + return [template.name, template.category].some((value) => value.toLowerCase().includes(needle)); + }); + }, [templateCategory, templateQuery, templates]); + const todayLogs = logs.filter((log) => log.date === selectedDate); + const dayPoints = todayLogs.reduce((sum, log) => sum + log.points, 0); + const weekPoints = logs.reduce((sum, log) => sum + log.points, 0); + const weekMinutes = logs.reduce((sum, log) => sum + log.minutes, 0); + const weekCalories = logs.reduce((sum, log) => sum + (log.calories ?? 0), 0); + + const weeklyDays = useMemo(() => { + const monday = new Date(`${startOfWeek(selectedDate)}T12:00:00`); + return Array.from({ length: 7 }, (_, index) => { + const date = new Date(monday); + date.setDate(monday.getDate() + index); + const key = date.toISOString().slice(0, 10); + const points = logs.filter((log) => log.date === key).reduce((sum, log) => sum + log.points, 0); + return { key, label: date.toLocaleDateString("de-DE", { weekday: "narrow" }), day: date.getDate(), points }; + }); + }, [logs, selectedDate]); + + function logTemplate(template: (typeof templates)[number]) { + const isFitbit = template.id.startsWith("fitbit-") || template.category.startsWith("fitbit_"); + addLogMutation.mutate({ + date: selectedDate, + name: template.name, + templateId: template.id, + minutes: template.defaultMinutes, + points: template.defaultPoints, + calories: template.defaultCalories, + source: isFitbit ? "fitbit" : "manual", + }); + } + + function saveCustomTemplate() { + if (!customName.trim()) return; + saveTemplateMutation.mutate({ + name: customName.trim(), + defaultMinutes: Number(customMinutes) || 5, + defaultPoints: Number(customPoints) || 10, + defaultCalories: Number(customCalories) || 0, + category: "habit", + icon: "custom", + }); + } + + function addCustomLog() { + if (!customName.trim()) return; + const minutes = Number(customMinutes) || 5; + const calories = Number(customCalories) || 0; + const points = Number(customPoints) || estimatePoints(minutes, calories); + addLogMutation.mutate({ + date: selectedDate, + name: customName.trim(), + minutes, + points, + calories, + source: customSource, + note: customNote.trim() || null, + }); + } + + return ( +
+
+
+
+

Belohnungssystem

+

Aktivitätspunkte

+

+ Fitbit-kompatibler Katalog, verbrannte kcal und manuelle Nacherfassung für Training ohne Uhr. +

+
+
+
+

{dayPoints}

+

heute

+
+
+
+
+ +
+
+
+ +
+

{weekPoints}

+

Punkte diese Woche

+
+
+
+
+
+ +
+

{weekMinutes}

+

aktive Minuten

+
+
+
+
+
+ +
+

{weekCalories}

+

kcal Sportbonus

+
+
+
+
+
+ +
+

{pointsLabel(weekPoints)}

+

aktueller Status

+
+
+
+
+ +
+
+

Woche

+ setSelectedDate(event.target.value || todayKey())} + className="rounded-full border border-gray-200 px-4 py-3 text-sm font-bold" + /> +
+
+ {weeklyDays.map((day) => { + const active = day.key === selectedDate; + return ( + + ); + })} +
+
+ +
+
+
+

Fitness-Katalog

+
+ {(["all", "fitbit", "manual"] as const).map((key) => ( + + ))} +
+
+ setTemplateQuery(event.target.value)} + placeholder="Aktivität suchen" + className="mt-4 w-full rounded-2xl border border-gray-200 px-4 py-3 text-sm font-semibold" + /> +
+ {filteredTemplates.map((template) => ( + + ))} + {!filteredTemplates.length && !templatesQuery.isLoading ? ( +

Keine passende Vorlage gefunden.

+ ) : null} +
+
+ +