Self-hosted Familien-Gesundheitsplattform: Apple-Health-Sync, Mehrbenutzer, Auswertung und ein KI-Assistent, der die eigenen Gesundheitsdaten erklärt — nicht diagnostiziert. Azure-basiert, .NET 8, iOS-App mit HealthKit.
- .NET-8-Solution mit
/api/v1-Struktur - Web-Dashboard im selben Prozess (Blazor Server, static rendering) — Login/Registrierung, Health-Übersicht, Berichte, Benutzerverwaltung, KI-Gateway, alles auch im Browser nutzbar (siehe unten)
- AI-Provider-Abstraktion: Claude (Sonnet 5, Anthropic API) + GPT-5.6 (Azure OpenAI Service), Standard admin-konfigurierbar über das KI-Gateway (siehe unten)
- iOS-Grundgerüst (xcodegen, Team KQGPPH4S33, Bundle
email.nimtz.nimpulse) - HealthKit: Autorisierungsanfrage für praktisch alle iOS-Gesundheitsdatentypen (siehe docs/HEALTHKIT.md)
- Benutzerverwaltung: Registrierung/Login (JWT für die API, Cookie fürs Web-Dashboard — dasselbe Backend), Rollen Admin/Member, erster registrierter Account wird automatisch Admin
- Health-Daten-Sync: iOS liest alle Quantity-/Category-HealthKit-Typen und lädt sie userbezogen hoch (SQLite, Upsert nach HealthKit-UUID)
- Reports: Aggregation (Summe/Durchschnitt/Min/Max) pro Typ, Tag/Woche/Monat
- KI-Gateway: Admin wählt Standard-AI-Provider/-Modell/-Keys zur Laufzeit (Settings-Screen in App und Web-Dashboard) — keine Keys mehr im 1-Click-Deploy-Formular
- KI-Coach: fortlaufender Chat mit Gesundheitsdaten-Kontext,
/coach(Web) + Chat-Screen (iOS) - EF Core Migrations statt
EnsureCreated()(Baseline-Bootstrap für Bestandsdaten) - Dashboard: Tages-Score (v1-Formel), Tag-für-Tag-Navigation, Dark Mode — Web (
/) + eigener iOS-Dashboard-Tab (Neubau, existierte vorher nicht) - PDF-Export der Reports (QuestPDF, direkt gestreamt) — Web (
/reports) + iOS (Sharesheet im Dashboard) - Proaktive Wochenzusammenfassung: Hintergrund-Dienst generiert einmal pro Woche und Nutzer eine KI-Zusammenfassung, erscheint als Nachricht im KI-Coach
- 1-Click-Azure-Deploy-Template (App Service + Azure Files/SQLite + Blob für Berichte)
- Invite-Links für Familienmitglieder statt Admin-legt-direkt-an (später, falls gewünscht)
Sprachumfang: Deutsch + Englisch (bewusst kein EFIGS+NL für dieses Projekt).
POST /api/v1/auth/register— offene Selbstregistrierung; der erste Account im System wird automatisch Admin, alle danach Member.POST /api/v1/auth/login— E-Mail/Passwort, liefert ein JWT (Gültigkeit konfigurierbar überAuth:TokenLifetimeHours, Standard 30 Tage).GET /api/v1/auth/me— aktueller Nutzer ([Authorize]).GET/POST/DELETE /api/v1/admin/users— Admin-only: Familienmitglieder direkt anlegen (mit Initial-Passwort) statt auf Selbstregistrierung angewiesen zu sein.
Alle /api/v1/health/*- und /api/v1/ai/*-Endpoints brauchen ein Authorization: Bearer <token>-Header. Passwort-Hashing über PasswordHasher<User> (ASP.NET Core Identity Core, ohne den vollen Identity-Unterbau). Kein E-Mail-Versand/Passwort-Reset bisher — kein E-Mail-Gateway vorhanden.
Läuft im selben Prozess/Container wie die API (Blazor Server, static server rendering — kein SignalR-Circuit, klassische Formular-Posts/Redirects reichen für dieses Admin-/Family-Scale-UI). Kein separates Hosting/Deploy nötig.
| Route | Zugriff | Zweck |
|---|---|---|
/login, /register |
Öffentlich | Anmeldung/Erstanlage. /login leitet automatisch zu /register weiter, solange noch kein Benutzer existiert. |
/ |
Angemeldet | Dashboard — Tages-Score, Kopf-Kacheln und 7-Tage-Chart für den gewählten Tag, Tag-für-Tag-Navigation (?date=yyyy-MM-dd). |
/reports |
Angemeldet | Dieselbe Aggregation wie GET /api/v1/health/reports, als Tabelle mit Typ-/Zeitraum-Filter. |
/admin |
Admin | Benutzerliste, neue Benutzer anlegen, löschen. |
/settings |
Admin | KI-Gateway-Konfiguration (siehe unten). |
Auth: Cookie fürs Web-Dashboard, Bearer/JWT für die iOS-App und andere API-Clients — beide Schemes laufen nebeneinander (Program.cs, "Smart"-Policy-Scheme wählt anhand des Authorization-Headers), teilen sich dieselben Claims/Rollen.
Dark Mode folgt standardmäßig prefers-color-scheme, der "Design"-Knopf in der Nav-Leiste überschreibt das per localStorage (kein Blazor-Interactivity nötig, reines Inline-JS in App.razor).
GET /api/v1/health/score?date=yyyy-MM-dd (Datum optional, Default heute) — v1-Formel aus Schritten (Ziel 10.000/Tag), aktiver Energie (Ziel 500 kcal/Tag) und Ruhepuls (100 Punkte bei ≤60 bpm, linear fallend auf 50 bei 100 bpm), gewichtet 40/30/30. Fehlt ein Metrik-Typ am gewählten Tag, verteilt sich das Gewicht proportional auf die übrigen; ganz ohne Daten gibt es keinen Score statt einer erfundenen Zahl. Ausdrücklich ein transparenter Startpunkt, keine medizinische Bewertung (DailyScoreService.cs). Web (/) und iOS (Dashboard-Tab) zeigen denselben Score über denselben Endpoint. GET /api/v1/health/reports akzeptiert optional denselben date-Parameter, um den 7-Tage-Chart auf einen vergangenen Tag zu verankern statt auf "jetzt".
GET/PUT /api/v1/settings/ai (Admin-only), auch als Formular unter /settings — legt fest, welcher Provider standardmäßig antwortet (claude, azure-openai oder openai), welches Modell/Deployment, und die API-Keys selbst. Drei Provider zur Wahl: Claude (Anthropic), Azure OpenAI (eigene Azure-Subscription, deployment-name-basiert) und OpenAI direkt (eigener OpenAI-Account, freies Modell-Textfeld z. B. gpt-5 — kein Azure-Resource nötig). Alles DB-backed (SQLite, AiGatewaySettings-Tabelle) statt appsettings/Umgebungsvariablen — der 1-Click-Deploy fragt keine AI-Keys mehr ab, ein Admin setzt sie einmalig nach dem ersten Login. GET maskiert die Keys (nur hasClaudeApiKey/hasAzureOpenAiApiKey/hasOpenAiApiKey, nie der Wert selbst); ein leeres Key-Feld beim Speichern lässt den bestehenden Key unangetastet. In der iOS-App: Zahnrad-Symbol → Einstellungen (nur für Admins sichtbar).
GET /api/v1/health/reports?type=stepCount&period=day|week|month&days=30&date=yyyy-MM-dd — aggregiert Quantity-Samples in Zeit-Buckets (Anzahl, Summe, Durchschnitt, Min, Max). Dieselbe Basis trägt Tages-, Wochen- und Monatsübersichten. date ist optional und verankert das Fenster auf einen bestimmten Tag statt "die letzten N Tage ab jetzt" (für Tag-Navigation im Dashboard).
GET /api/v1/health/reports/pdf (dieselben Parameter) liefert dieselbe Aggregation als PDF (ReportPdfService, QuestPDF) — direkt gestreamt, nicht auf dem Server gespeichert. Web: "PDF exportieren"-Link auf /reports. iOS: Button im Dashboard, öffnet das System-Sharesheet. Der ARM-Template-Blob-Container reports (infra/azuredeploy.json) ist dafür vorbereitet, aber v1 nutzt ihn noch nicht — ein Later-Schritt für teilbare PDF-Links.
dotnet build
ASPNETCORE_URLS="http://0.0.0.0:5289" dotnet run --project src/NimPulse.Api0.0.0.0 binden (nicht 127.0.0.1), sonst erreicht ein Gerät im selben WLAN (z. B. das iPhone beim Testen) den Server nicht.
AI-Keys lokal setzen, statt sie in appsettings.json einzutragen:
export Ai__Claude__ApiKey="sk-ant-..."
export Ai__AzureOpenAi__Endpoint="https://<resource>.openai.azure.com/"
export Ai__AzureOpenAi__ApiKey="..."
export Ai__AzureOpenAi__DeploymentName="<deployment-name>"Auth__JwtSigningKey unbedingt für echte Deployments setzen — der appsettings-Default ist absichtlich ein erkennbar unsicherer Platzhalter.
cd ios
xcodegen generate
open NimPulse.xcodeprojNach jeder neuen .swift-Datei erneut xcodegen generate ausführen. Sources/Networking/APIConfig.swift zeigt auf die LAN-IP des Rechners, auf dem die API läuft (ipconfig getifaddr en0) — bei Netzwerkwechsel anpassen.
Klick den Deploy to Azure-Button oben. Pflichtparameter: siteName. AI-Provider/-Keys werden nicht beim Deploy abgefragt — nach dem ersten Login unter /settings (Web) oder Einstellungen (iOS) setzen. jwtSigningKey wird automatisch pro Deployment generiert, wenn leer gelassen.
Der EF-Core-SQLite-Provider übersetzt Where/OrderBy/Max/Min auf DateTimeOffset-Spalten (z. B. HealthSample.StartDate) server-seitig nicht zuverlässig — teils mit NotSupportedException, teils mit InvalidOperationException: ... could not be translated, sobald ein Vergleich mit einem weiteren Prädikat kombiniert wird. Betroffene Stellen (ReportService, HealthController.GetSamples, AdminUsersController.List) filtern/sortieren deshalb bewusst erst nach ToListAsync() clientseitig. Bei neuen Queries auf StartDate/CreatedAt/SyncedAt denselben Zweischritt verwenden — direkt in der DB-Query vergleichen/sortieren bricht.
Seit v0.7.0 läuft das Schema über echte dotnet ef migrations (src/NimPulse.Core/Migrations/) statt Database.EnsureCreated() + manuellem ALTER TABLE (das hatte zuvor schon einmal einen echten Vorfall verursacht: nachträglich hinzugefügte AiGatewaySettings-Spalten fehlten auf dem laufenden Azure-Deployment). Beim Start entscheidet BootstrapDatabase in Program.cs: existieren bereits Tabellen, aber keine __EFMigrationsHistory (= eine alte EnsureCreated()-DB), wird die Baseline-Migration 20260804104900_InitialBaseline als "bereits angewendet" markiert, ohne ihre CREATE TABLEs erneut auszuführen — danach läuft Database.Migrate() normal für alles Weitere. Bei einer komplett neuen DB läuft die Baseline-Migration regulär durch. Jede künftige Schema-Änderung ist ab jetzt eine echte Migration, kein manueller ALTER TABLE-Block mehr.
/coach (Web) bzw. das Sprechblasen-Symbol oben links in der iOS-App — ein fortlaufender Chat pro Nutzer (ChatMessages-Tabelle), nicht nur ein zustandsloser Einzel-Request. Vor jeder Antwort baut ChatCoachService (src/NimPulse.Core/Ai/ChatCoachService.cs) automatisch einen Kontext aus den eigenen Gesundheitsdaten der letzten 7 Tage (Schritte, aktive Energie, Herzfrequenz, Ruhepuls, Körpergewicht) in den System-Prompt ein, damit der gewählte AI-Provider (Claude/Azure OpenAI/OpenAI, siehe KI-Gateway oben) über echte Trends spricht statt nur auf getippten Text zu reagieren. GET /api/v1/ai/chat/history + POST /api/v1/ai/chat bedienen iOS und andere API-Clients; Coach.razor im Web ruft denselben ChatCoachService direkt auf (keine Zwischen-HTTP-Runde). /coach ist aktuell die einzige Seite mit @rendermode InteractiveServer — alle anderen Seiten bleiben bewusst Static SSR.
Proaktive Wochenzusammenfassung: WeeklyInsightBackgroundService (src/NimPulse.Api/BackgroundServices/) tickt alle 6h, prüft pro Nutzer über GeneratedInsight (Unique-Index UserId+WeekStart), ob diese ISO-Woche schon eine Zusammenfassung existiert, und generiert sonst eine über WeeklyInsightService (denselben ChatCoachService-Gesundheitskontext, anderer Prompt). Kein E-Mail-Gateway, keine Push-Notifications nötig — die Zusammenfassung landet einfach als neue Assistenten-Nachricht in der bestehenden Chat-Historie, sichtbar beim nächsten Öffnen von /coach bzw. dem iOS-Chat. KI-Provider-Fehler (z. B. kein Key konfiguriert) lassen den Tick einfach ohne neuen Eintrag durchlaufen, statt den Hintergrund-Dienst abzuwürgen.
src/NimPulse.Api/ # ASP.NET Core Web-API (/api/v1), Auth/Admin/Health/AI-Gateway-Controller
src/NimPulse.Core/ # Domänenlogik: Users, Auth (JWT), Health (EF Core/SQLite), Ai, Settings
ios/ # SwiftUI-App, xcodegen-verwaltet (Login, Health-Sync, Settings)
infra/azuredeploy.json # 1-Click ARM-Template
docs/ # HealthKit-Datenumfang, weitere Notizen
