- New device kind Ventilation with driver Vallox (WebSocket protocol of the unit's web UI: READ_TABLES/WRITE_DATA, checksum, big-endian table); no migration (enums stored as text) - Read profile incl. remaining minutes, outdoor/supply/extract/exhaust air temperatures, humidity, CO2, fan speed; activate Home/Away/Boost/Fireplace (timed profiles use the duration configured on the unit) - Same SSRF protection as Shelly (shared connection handler), permissions, action log, audit - Admin dialog without generation/channel/credentials for ventilation, device card with values and profile buttons - Tests: unit (protocol vs. reference bytes, driver, loopback WebSocket, SSRF, rules), bUnit - ADR-0025, roadmap phase 19
29 KiB
Architektur – Digitaler Familienplaner (FamilyPlanner)
1. Überblick
FamilyPlanner ist eine Progressive Web App für Familien (Einkaufsliste, Essenplan, Aufgaben/Ämtchen, Kalender mit ICS-Abo, Chat). Lokal wird alles über .NET Aspire orchestriert, produktiv läuft es per Docker Compose auf einem Linux-Host.
Browser / installierte PWA (Blazor WebAssembly)
│ HTTPS, same-origin, Auth-Cookie
▼
┌───────────────────────────────────────────────────────────────┐
│ FamilyPlanner.Web (BFF) │
│ • liefert WASM-App, Manifest, Service Worker (statisch) │
│ • YARP: /api/**, /hubs/**, /ics/**, /caldav/** ──┐ │
│ • Security-Header (CSP, HSTS …) │ │
└───────────────────────────────────────────────────┼───────────┘
▼
┌───────────────────────────────────────────────────────────────┐
│ FamilyPlanner.Api │
│ • Minimal APIs (Vertical Slices pro Feature) │
│ • ASP.NET Core Identity + Cookie-Auth, Policies │
│ • SignalR Hubs (Chat, Live-Updates) │
│ • ICS-Feeds (Token-gesichert), ICS-Import, CalDAV (/caldav) │
└───────────────────────────┬───────────────────────────────────┘
│ EF Core 10
▼
SQL Server (Volume)
▲
┌───────────────────────────┴───────────────────────────────────┐
│ FamilyPlanner.MigrationService (Worker, läuft einmal) │
│ • wendet EF-Migrations an, seedet Rollen + Initial-Admin │
└───────────────────────────────────────────────────────────────┘
Siehe ADRs in docs/adr/ für die Begründungen.
2. Projektstruktur
| Projekt | Zweck |
|---|---|
src/FamilyPlanner.AppHost |
Aspire-Orchestrierung: SQL Server (persistentes Volume), Datenbank, MigrationService, Api, Web; Docker-Compose-Publishing |
src/FamilyPlanner.ServiceDefaults |
OpenTelemetry, Health Checks (/health, /alive), Service Discovery, Resilience |
src/FamilyPlanner.Domain |
Entities, Value Objects, Rollen-/Berechtigungsregeln, reine Domänenlogik (keine Abhängigkeiten) |
src/FamilyPlanner.Infrastructure |
EF Core AppDbContext, Identity, Migrations, Seeding, Interceptors (Audit/Soft Delete) |
src/FamilyPlanner.Shared |
DTOs/Contracts zwischen Api und WASM-Client |
src/FamilyPlanner.Api |
Minimal APIs (Features/<Feature>/…), Auth, SignalR, ICS |
src/FamilyPlanner.MigrationService |
Worker: Migrations + Seeding beim Start, beendet sich danach |
src/FamilyPlanner.Web |
Blazor-Host/BFF: statische Auslieferung der WASM-App + YARP-Proxy |
src/FamilyPlanner.Web.Client |
Blazor WebAssembly (MudBlazor), PWA |
tests/FamilyPlanner.UnitTests |
Domänenlogik, Rollen/Policies, Parser, ICS |
tests/FamilyPlanner.IntegrationTests |
End-to-End über Aspire.Hosting.Testing (echter SQL-Server-Container, flüchtige DB) |
deploy/ |
docker-compose.yml, .env.example, Caddyfile, Backup-/Restore-Skripte |
tools/ |
Hilfsskripte (z. B. make_icons.py zum Erzeugen der PWA-Icons, generate-vapid-keys.cs für Web-Push-Schlüssel) |
tests/FamilyPlanner.Web.Tests |
bUnit-Komponententests |
Vertical Slices in der Api
FamilyPlanner.Api/
Features/
Auth/AuthEndpoints.cs # /api/auth/*
Users/… # /api/admin/users/* (Phase 2)
Shopping/… # /api/shopping/* (Phase 3)
Meals/… Tasks/… Calendar/… Chat/… Dashboard/…
Home/… # Haus: Treiber-Abstraktion (IHomeDeviceDriver, Shelly, Vallox), /api/home/* und /api/admin/home/* (Phase 17/19)
CalDav/… # /caldav/* (CalDAV-Server) + /api/caldav/* (App-Passwörter), Phase 12c
Hubs/ # SignalR
Infrastructure/ # Auth-Setup, Security, Rate Limiting
Jeder Slice enthält Endpunkte, Validatoren (FluentValidation) und ggf. Handler. Geteilte Regeln liegen im Domain-Projekt.
3. Datenmodell (Ziel, wird phasenweise umgesetzt)
Alle Entitäten erben von AuditableEntity (Id: Guid, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy).
Löschbare Entitäten implementieren ISoftDeletable (IsDeleted, DeletedAt) – globaler Query-Filter.
Einkaufsliste, Essenplan, Aufgaben und Termine haben eine RowVersion (Optimistic Concurrency).
Alle Zeitpunkte werden als UTC (DateTimeOffset/DateTime UTC) gespeichert und in Europe/Berlin angezeigt.
| Bereich | Entitäten |
|---|---|
| Identity ✅ | AppUser (DisplayName, Color, AvatarUrl, BirthDate (optional, DateOnly, Phase 15), MustChangePassword, IsActive, ExpiresAt, PinHash, LastLoginAt, Soft Delete), AppRole (Admin, Familie, Kind, Gast), UserAvatar (Bild ≤ 1 MB) |
| Einstellungen ✅ | FamilySettings (FamilyName, TimeZoneId, MealPlanDays (7–10), MealTypes, TaskCategories, ChorePointsEnabled, ChildTasksRequireApproval, GuestsCanEditShoppingList, GuestChatEnabled, LearningPhotoRetentionDays, AiChatEnabled, AiChatChildrenAllowed) |
| Einkauf ✅ | ShoppingList (Name, IsDefault, SortOrder, Soft Delete), ShoppingItem (Client-Id, Name, Quantity, Category, IsDone, DoneAt, DoneBy, RowVersion, Soft Delete), ShoppingSuggestion (Name, Category, CategoryIsManual, UseCount, LastUsedAt). Die Items-DTOs liefern CreatedByName („hinzugefügt von …“); für Kinder prüft ShoppingPlausibility (Sonderzeichen/Ziffern, Zeichenwiederholung, Tastaturmuster, kleine Quatschliste) Neu-Einträge: verdächtige liefern 409 mit needsConfirmation, der Client fragt nach und sendet Confirm = true |
| Essen ✅ | MealPlanEntry (Date als DateOnly, MealType, DishName, Note, RecipeUrl, Ingredients, WishId, RowVersion, Soft Delete; eindeutig je Tag + Mahlzeit), Dish (Name, Note, RecipeUrl, Ingredients, UseCount = Planungen, WishCount, WishedBy, LastPlannedAt, Soft Delete; eindeutiger NormalizedName – dauerhafte Wunschliste und Autovervollständigung, /api/meals/wishlist), MealWish (DishName, Note, Wunschtag/-mahlzeit, Status offen/übernommen/abgelehnt, Soft Delete) |
| Aufgaben ✅ | TodoTask (Title, Notes, DueDate + DueTime → DueAt, ReminderMinutesBefore → RemindAt, Priority, Status inkl. „wartet auf Bestätigung“, Category, IsPrivate, IsChore, Points, NeedsApproval, AssignedToFamily, RecurrenceRule + RecurrenceMode, SeriesId/SeriesStart/SeriesIndex, CompletedBy/At, ApprovedBy/At, RowVersion, Soft Delete), TaskAssignment, TaskChecklistItem, TaskHistoryEntry (Verlauf), PointsLedgerEntry (Punktebuch). Kategorien kommen aus FamilySettings.TaskCategories; „wer/wann/genehmigt“ steht an der Aufgabe und im Verlauf (ADR-0014) |
| Kalender ✅ | FamilyCalendar (Name, Color, IsDefault, Soft Delete), CalendarPermission (Rolle oder Benutzer → None/Read/WriteOwn/Write), CalendarEvent (Title, Location, Notes, IsAllDay, BirthDate (Geburtstagsvorlage, Phase 15), StartUtc, EndUtc, TimeZoneId, RecurrenceRule, ExceptionDates, Uid, PersonIds, ForWholeFamily, Sequence, RowVersion, Soft Delete; Overrides als eigene Zeile mit SeriesMasterId + RecurrenceId), IcsFeed (Owner, Scope, CalendarId, TaskMode, TokenHash, TokenPrefix, LastAccessAt, RevokedAt), CalDavCredential (UserId, Label, SecretHash, SecretPrefix, LastUsedAt, RevokedAt; App-Passwort für CalDAV) – ADR-0015/0016/0017 |
| KI ✅ | AiProviderConfig (Name, Type Ollama/Claude/OpenAiCompatible, BaseUrl, Model, ApiKeyProtected, TimeoutSeconds, IsActive, AllowImages, AllowWebSearch, Purposes, DailyLimitPerUser, RowVersion, Soft Delete), AiUsage (Person, Anbieter, Zweck, Dauer, Tokens, Erfolg/Fehlerart – ohne Inhalt), AiExchange ✅ (12b: KI-Protokoll je Aufruf – Person, Anbieter, Modell, Zweck, Beginn, Dauer, Erfolg/Fehler, Bildanzahl, Online-Suche, Tokens, ContentStored, Prompt, Antwort; Prompt (inkl. System-Prompt) und Antwort immer, auch im KI-Chat und bei Fehlern (Phase 14); AiUserSettings ✅ (Phase 14: Admin-Vorgaben je Person – Anbieter, Modell, System-Prompt, Prompt deaktiviert); Aufbewahrung FamilySettings.AiLogRetentionDays, Standard 90) – ADR-0020; Zwecke Standard, Lernaufgaben, Foto-Prüfung, KI-Chat (letztere beiden ohne Rückfall auf den Standard, ADR-0022) |
| Chat ✅ | ChatChannel (Family / Direct / Ai – privater KI-Kanal je Person, DirectKey = ai:{userId}), ChatMember (LastReadAt), ChatMessage (Text, HasImage; KI-Antworten mit fester Absender-Id ChatRules.AiSenderId, kein Benutzerkonto), ChatAttachment (PNG/JPEG/WebP; GIF nur aus der Server-GIF-Suche), ChatReaction – ADR-0019, ADR-0022 |
| Lernen ✅ (10a) | LearningSet (Titel, Fach Mathe/Deutsch, Klassenstufe 1–6, optionaler Lesetext Passage (Phase 16, Plain-Text ≤ 3000 Zeichen, Kinder sehen ihn bei jeder Frage), Zielkind, Fälligkeit, Status, Modus Übung/Prüfung, MaxAttempts, TimeLimitMinutes, ShowSolutionsAfterSubmit, ShuffleItems, TotalPoints, GradeThresholds, Source Manual/Ai + AiPrompt/AiProvider/AiModel (von 10b für KI-Entwürfe gesetzt, nie für Kinder sichtbar), RowVersion, Soft Delete), LearningItem (Frage, Antworttyp Number/Text/SingleChoice/Paper (Paper je Aufgabe oder für das ganze Blatt in einem Schritt, 12a), CorrectAnswer, NumberTolerance, Options, Hint, Explanation, Difficulty, Points, RowVersion, Soft Delete), LearningAttempt (Nummer, Status, StartedAt, DeadlineAt, SubmittedAt, AutoSubmitted, ItemOrder, Ergebnis/Note, RowVersion), LearningAnswer (Antwort, Verdict Open/Correct/Partial/Wrong, Grader Auto/Ai/Parent, AwardedPoints, Reason, AiConfidence, PhotoPurgedAt), LearningPhoto ✅ (10c: Foto der Papierlösung je Versuch und Aufgabe, Bytes in der Datenbank); LearningSchedule ✅ (11c: Zeitplan für automatische Blätter – Kind, Ersteller, Fach, Klasse, Anzahl, Startschwierigkeit + Steigerung, Wochentage/Uhrzeit, Direkt-Zuweisen, HandledThroughAt, Retry/Sperre, Soft Delete, RowVersion) und LearningScheduleRun (Protokoll ohne Inhalt, eindeutig je Plantermin); LearningSet.ScheduleId; FamilySettings.LearningPhotoRetentionDays; PointsLedgerEntry.LearningSetId – ADR-0021 |
| Kameras ✅ (Phase 18) | HomeCamera (Name, StreamUrl mit Pfad/Query, nur Heimnetz, AuthUser, AuthPasswordProtected, ShowOnDashboard, SortOrder, RowVersion, Soft Delete), HomeCameraAccess (Freigabe für Rolle oder Person, nur ansehen) – ADR-0024 |
| Haus ✅ (Phase 17) | HomeRoom (Name, SortOrder, Soft Delete), HomeDevice (Name, Kind Schalter/Steckdose/Licht/Rollo/Sensor/Lüftungsanlage, Icon, RoomId, Driver (Shelly, Vallox – ADR-0025), Generation 1/2, Host (nur Heimnetz, normalisiert), Channel, AuthUser, AuthPasswordProtected (Data Protection, nie an den Client), SortOrder, RowVersion, Soft Delete), HomeDeviceAccess (Freigabe je Gerät für Rolle oder Person → View/Control), HomePlan (Name, SortOrder, ContentType, ImageVersion, Soft Delete) + HomePlanImage (Bytes in der Datenbank, ADR-0009), HomeDevicePlacement (Plan, Gerät, relative Position X/Y 0–1), HomeActionLog (Person, Gerät, Befehl, Erfolg/Fehlertext, Dauer; Namen als Momentaufnahme) – ADR-0023 |
| Dashboard ✅ | DashboardNote (Title, Text, Soft Delete; Freitext der Info-Kachel, wird nur als Text dargestellt) |
| Betrieb | AuditLogEntry ✅ (ActorName, Action, TargetType/Id/Name, Details, CreatedAt), PushSubscription ✅ (UserId, Endpoint + Hash, P256dh, Auth, LastSuccessAt, FailureCount) |
4. Rollen- und Berechtigungsmodell
Rollen: Admin, Familie, Kind, Gast (Konstanten in FamilyPlanner.Domain.Roles).
Die Rollenmatrix der Anforderungen ist als reine Funktion PermissionMatrix im Domain-Projekt abgebildet (ADR-0008):
je Permission und Rolle eine Zugriffsstufe Full (F), Restricted (R) = erreichbar, aber objektbezogene Prüfung im Slice,
oder None (–). Die Api registriert je Permission eine Policy Permission:<Name>; Endpunkte nutzen .RequirePermission(…).
| Permission | Admin | Familie | Kind | Gast | Bedeutung von R |
|---|---|---|---|---|---|
DashboardView |
F | F | F | R | Gast: eingeschränkte Kacheln |
DashboardNotesEdit |
F | F | – | – | Info-Kachel (Freitext-Notizen) pflegen; ansehen dürfen alle mit DashboardView |
ShoppingRead |
F | F | F | F | |
ShoppingWrite |
F | F | F | – ¹ | |
ShoppingDelete |
F | F | R | – | nur eigene Einträge |
MealPlanRead |
F | F | F | F | |
MealPlanEdit |
F | F | R | – | nur Wünsche vorschlagen |
TasksRead |
F | F | R | R | Kind: eigene + Familie; Gast: nur zugewiesene |
TasksEdit |
F | F | R | – | nur eigene, private Aufgaben |
TasksAssign |
F | F | – | – | |
TasksCompleteAssigned |
F | F | F | F | |
TasksApprove |
F | F | – | – | Eltern-Bestätigung von Ämtchen, Punktekorrektur (ADR-0014) |
CalendarRead |
F | F | F | R | nur freigegebene Kalender |
EventsEdit |
F | F | R | – | nur eigene Termine |
FamilyChat |
F | F | F | – ² | |
DirectMessages |
F | F | R | – | nur an Familienmitglieder |
AiUse |
F | F | – | – | KI-Funktionen nutzen (ADR-0020) |
LearningManage |
F | F | – | – | Übungsblätter anlegen/zuweisen/prüfen/bestätigen (ADR-0021) |
LearningSolve |
F | F | R | – | Kind: nur eigene, zugewiesene Blätter lösen; fremde → 404 (ADR-0021) |
HomeUse |
F | R | R | R | Haus-Seite: Geräte nur nach Freigabe je Gerät (Rolle oder Person; Admin immer); nicht freigegeben → 404, nur „ansehen“ → 403 beim Schalten (ADR-0023) |
HomeManage |
F | – | – | – | Hausverwaltung: Geräte, Räume, Freigaben, Pläne, Aktionsprotokoll (ADR-0023) |
UserManagement, CalendarManagement, SettingsManagement, AuditLogView |
F | – | – | – |
¹ F, wenn „Gäste dürfen die Einkaufsliste ergänzen/abhaken“ aktiviert ist. ² F, wenn „Gäste dürfen den Familienchat nutzen“ aktiviert ist.
Zusätzlich gibt es grobe Rollen-Policies (Admin, Parent, FamilyMember, AnyRole). Alle Policies – auch die
Fallback-Policy für jeden Endpunkt ohne eigene Angabe – verlangen: angemeldet, Konto nicht abgelaufen (Claim fp:expires_at)
und kein ausstehender Passwortwechsel. Ein Benutzer mit MustChangePassword = true erreicht nur /api/auth/*.
Das UI bekommt die wirksamen Berechtigungen über /api/auth/me und blendet anhand derselben Policy-Namen aus; es ist nie die einzige Sperre.
5. Authentifizierung
- ASP.NET Core Identity (Api), Cookie
__Host-fp.auth(HttpOnly, Secure, SameSite=Strict, Path=/). - Der Browser spricht ausschließlich mit dem Web-Host (same-origin); YARP leitet
/apiund/hubsan die Api weiter. Dadurch ist das Cookie First-Party, und es werden keine Tokens im Browser gespeichert (BFF-Prinzip, ADR-0002). - Bei 401 liefert die Api JSON-Status statt Redirect (
OnRedirectToLogin→ 401/403). - Anti-Forgery: SameSite=Strict + Pflicht-Header
X-Requested-With: FamilyPlannerauf allen nicht-GET-Requests (Custom-Header-Check, kann von fremden Origins nicht ohne CORS-Preflight gesetzt werden). - Login per Benutzername oder E-Mail, „Angemeldet bleiben“ (persistentes Cookie, 30 Tage gleitend), Lockout (5 Fehlversuche → 15 min), Rate Limiting (Fixed Window pro IP auf
/api/auth/login). - Seed-Admin aus Konfiguration (
Seed:AdminUserName,Seed:AdminPassword,Seed:AdminEmail),MustChangePassword = true. - Kalender-Abos (ICS) verwenden geheime Tokens in der URL (nur Hash in DB), da Kalender-Clients keine Cookies können (ADR-0005).
- CalDAV (
/caldav, ADR-0017) verwendet HTTP Basic mit App-Passwörtern (CalDavCredential: je Benutzer/Gerät, einmalig angezeigt, nur SHA-256 in der DB, widerrufbar) über ein eigenes, nicht standardmäßiges Auth-SchemaCalDavBasic: das Konto-Passwort wird nie akzeptiert, App-Passwörter öffnen kein/api, Cookies kein/caldav. Basic nur über HTTPS (CalDav:RequireHttps), Fehlversuche werden gedrosselt (429); Kalenderrechte werden pro Anfrage serverseitig geprüft.
6. API-Übersicht (Ziel, ✅ = umgesetzt)
| Bereich | Endpunkte |
|---|---|
| Auth ✅ | POST /api/auth/login, POST /api/auth/pin-login (Feature-Flag), POST /api/auth/logout, GET /api/auth/me, PUT /api/auth/me/birth-date (eigenes Geburtsdatum, jeder angemeldete Benutzer), POST /api/auth/change-password |
| Admin – Benutzer ✅ | GET/POST /api/admin/users, GET/PUT/DELETE /api/admin/users/{id}, POST …/{id}/reset-password, POST …/{id}/unlock, PUT …/{id}/pin, PUT/DELETE …/{id}/avatar |
| Admin – Einstellungen ✅ | GET /api/settings (alle), PUT /api/admin/settings, GET /api/admin/audit?page=&pageSize= |
| Benutzerverzeichnis ✅ | GET /api/users, GET /api/users/{id}/avatar – BirthDate im Verzeichnis nur für die eigene Person, Admins und Familie (BirthdayRules.CanView, serverseitig) |
| Einkauf ✅ | GET/POST /api/shopping/lists, PUT/DELETE /api/shopping/lists/{id}, GET/POST /api/shopping/lists/{id}/items, PATCH/DELETE /api/shopping/items/{id}, POST /api/shopping/lists/{id}/clear-done, GET /api/shopping/suggestions?q=&take= |
| Essen ✅ | GET /api/meals?from=&to=, PUT /api/meals (Upsert je Tag + Mahlzeit, Platz im Body), DELETE /api/meals/{id}, POST /api/meals/{id}/move, POST /api/meals/{id}/to-shopping, GET /api/meals/dishes?q=&take=, GET/POST /api/meals/wishes, POST …/wishes/{id}/accept, POST …/wishes/{id}/reject, DELETE …/wishes/{id} |
| Aufgaben ✅ | GET/POST /api/tasks (Anlegen idempotent mit Client-Id), GET/PUT/DELETE /api/tasks/{id} (Detail inkl. Verlauf, Ändern mit Version), POST /api/tasks/{id}/status (Zielstatus), PUT /api/tasks/{id}/checklist/{itemId}, POST /api/tasks/{id}/approve, POST /api/tasks/{id}/reject, GET /api/tasks/points, POST /api/tasks/points/adjust |
| Push ✅ | GET /api/push/config, POST /api/push/subscriptions, POST /api/push/unsubscribe, POST /api/push/test |
| Kalender ✅ | GET /api/calendars, GET /api/calendar/events?from=&to=&includeTasks=, GET /api/calendar/upcoming (heute + morgen), POST /api/calendar/events, PUT/DELETE /api/calendar/events/{id}?scope=All|Occurrence&occurrence=, POST /api/calendar/import?calendarId= |
| Kalender-Abos ✅ | GET/POST /api/calendar/feeds, POST /api/calendar/feeds/{id}/regenerate, DELETE /api/calendar/feeds/{id}; Admin: GET/DELETE /api/admin/calendar-feeds |
| Admin – Kalender ✅ | GET/POST /api/admin/calendars, PUT/DELETE /api/admin/calendars/{id} |
| ICS-Feed | GET /ics/{token}.ics (öffentlich, Token-gesichert, ETag/Last-Modified) |
| CalDAV ✅ | /.well-known/caldav → 301, /caldav/** (OPTIONS, PROPFIND, PROPPATCH, REPORT calendar-query/calendar-multiget/sync-collection, GET, PUT, DELETE; Basic mit App-Passwort), GET /api/caldav/info, GET/POST /api/caldav/credentials, DELETE /api/caldav/credentials/{id} (ADR-0017) |
| Chat ✅ | GET /api/chat/channels, POST /api/chat/direct, GET /api/chat/channels/{id}/messages?before=&take=, POST …/messages, POST …/images (Multipart), POST …/read, DELETE /api/chat/messages/{id}, PUT /api/chat/messages/{id}/reactions, GET /api/chat/attachments/{id} (ADR-0019); Erweiterungen (ADR-0022): GET /api/chat/features, POST /api/chat/ai (eigener KI-Kanal, get-or-create), GET /api/chat/gifs?q=, GET /api/chat/gifs/{id}/preview, POST …/channels/{id}/gifs; Konfiguration Giphy__ApiKey (leer = keine GIF-Suche) |
| KI ✅ | Admin: GET/POST /api/admin/ai/providers, PUT/DELETE …/{id}, POST …/{id}/test, GET /api/admin/ai/usage, KI-Protokoll (12b): GET /api/admin/ai/exchanges?purpose=&success=&userId=&from=&to=&page=&pageSize=, GET …/exchanges/people, GET/DELETE …/exchanges/{id}, DELETE …/exchanges (alle); Eltern: GET /api/ai/status (inkl. LearningWebSearchAvailable für „Online recherchieren“; ADR-0020) |
| Lernen ✅ (10a) | Eltern (LearningManage): GET/POST /api/learning/sets, GET/PUT/DELETE …/sets/{id}, POST …/sets/{id}/assign, POST …/unassign, POST …/sets/{id}/items, PUT/DELETE …/items/{itemId}, POST …/items/order, GET /api/learning/review, GET /api/learning/attempts/{id}, PUT …/attempts/{id}/answers/{itemId}/review, POST …/attempts/{id}/confirm, POST …/attempts/{id}/reopen; Kinder (LearningSolve): GET /api/learning/my, POST …/my/{setId}/attempts, GET …/my/attempts/{id}, PUT …/my/attempts/{id}/answers/{itemId}, POST …/my/attempts/{id}/submit; Fotos (10c): POST/DELETE /api/learning/my/attempts/{id}/items/{itemId}/photo (Kind), GET /api/learning/attempts/{id}/items/{itemId}/photo (Eltern nach Abgabe, Kind; sonst 404), POST /api/learning/attempts/{id}/ai-grade (LearningManage + AiUse, KI-Vorschlag für Fotos und unklare Zahlenantworten); Lesetexte (Phase 16): Passage in Blatt-Anfragen/-DTOs und in Versuchs-DTOs (Kind, Prüfung), CreateAiDraftRequest.Kind (Standard/Leseverständnis/Textaufgaben); Automatik (11c, alle Routen LearningManage + AiUse): GET/POST /api/learning/schedules, PUT/DELETE …/schedules/{id}, POST …/schedules/{id}/run (ADR-0021) |
| Kameras ✅ (Phase 18) | Nutzersicht (HomeUse, je Kamera freigegeben, sonst 404): GET /api/home/cameras (Name, Startseiten-Kennzeichen), GET /api/home/cameras/{id}/stream (MJPEG, von der Api weitergereicht; 429 bei zu vielen Zuschauern, 502 mit fester Meldung); Dashboard-Kacheln in GET /api/dashboard (Cameras); Verwaltung (HomeManage): GET/POST /api/admin/home/cameras, PUT/DELETE …/{id}, PUT …/{id}/access, POST …/cameras/test (ADR-0024) |
| Haus ✅ (Phase 17) | Haus-Seite (HomeUse, je Gerät nach Freigabe): GET /api/home/overview (Räume, sichtbare Geräte ohne Adresse/Zugangsdaten, Pläne mit sichtbaren Pins), GET /api/home/states (parallel, kurzer Server-Cache; ein offline Gerät bricht nichts ab), GET /api/home/devices/{id}/state, POST /api/home/devices/{id}/command (an/aus/umschalten, öffnen/schließen/stopp; protokolliert), GET /api/home/plans/{id}/image (nur für Admin oder wer ein platziertes Gerät sehen darf); Hausverwaltung (HomeManage): GET/POST /api/admin/home/devices, PUT/DELETE …/{id}, POST …/{id}/test, POST …/devices/test (Entwurf), PUT …/{id}/access, GET/POST /api/admin/home/rooms, PUT/DELETE …/{id}, GET/POST /api/admin/home/plans (Multipart), PUT/DELETE …/{id}, PUT …/{id}/image, PUT …/{id}/placements, GET /api/admin/home/log (ADR-0023) |
| Dashboard ✅ | GET /api/dashboard (alle Kacheln der Rolle in einem Request, ADR-0018; inkl. Info-Kachel Notes), POST /api/dashboard/notes, PUT/DELETE /api/dashboard/notes/{id} (DashboardNotesEdit, Titel ≤ 80, Text ≤ 1000 Zeichen, höchstens 20 Notizen) |
| Hubs | /hubs/live (Live-Updates: Einkauf ✅, Essenplan ✅, Aufgaben ✅, Termine ✅, Chat ✅ inkl. Hub-Methode Typing, Lernen ✅ (LearningChanged ohne Inhalte); das Dashboard lädt bei diesen Ereignissen neu) |
| Betrieb | /health, /alive |
7. Frontend
- Blazor Web App, Interactive WebAssembly global, Prerendering aus (ADR-0002). Der Web-Host rendert nur die HTML-Hülle.
- MudBlazor als Komponentenbibliothek (lokal ausgeliefert, kein CDN), deutsches UI. Lokalisierung vorbereitet: Kultur
de-DE,AddLocalization()registriert, alle Globalisierungsdaten geladen; Texte stehen derzeit direkt in den Komponenten und werden bei Bedarf inResources/*.resxüberführt. - Ordner
Pages/erfordert per_Imports.razordie PolicyPasswordChanged;Public/enthält Login, Passwortwechsel und 404. - Layout: MudLayout mit AppBar, Drawer (Desktop) bzw. Bottom-Navigation (Smartphone), Dark Mode (System-Default, umschaltbar, in
localStorage). - Eigener
AuthenticationStateProvider(ApiAuthenticationStateProvider) holt/api/auth/meund liefert Claims inkl. Rollen. - PWA:
manifest.webmanifest, Icons 72–512 px + maskable, eigener Service Worker (App-Shell-Cache, Network-first für Navigation, keine Caches für/api//hubs), Offline-Banner. Hintergrund-Sync für die Einkaufsliste über IndexedDB-Outbox ab Phase 3.
8. Echtzeit
SignalR-Hub /hubs/live in der Api, vom Web-Host per YARP (WebSockets) durchgereicht, Cookie-Auth. Gruppen: shopping und meals (alle Rollen),
family (ohne Gäste), user:{id}. Der Chat nutzt keine Kanal-Gruppen, sondern sendet an die user:{id} der jeweils aktuell Berechtigten (ADR-0019). Slices senden nach dem Speichern über IHubContext<LiveHub>
Ereignisse mit dem vollständigen DTO (z. B. ShoppingItemChanged), die Clients aktualisieren gezielt ihre Ansicht.
Aufgaben gehen wegen privater Aufgaben nur an user:{id} der Benutzer, die sie sehen dürfen (TaskChanged/TaskRemoved, ADR-0014).
Lern-Ereignisse (LearningChanged(setId, kind)) enthalten keine Inhalte und gehen nur an die Zielperson und alle Eltern (user:{id}, LearningNotifier, ADR-0021); die Clients laden über die geschützten Endpunkte nach.
Der Client (LiveConnection) verbindet sich mit unbegrenzten Wiederholungen neu und lädt danach den aktuellen Stand.
8a. Offline
Offline-fähige Bereiche halten Cache und Outbox im localStorage und senden idempotente Operationen nach (ADR-0011):
die Einkaufsliste vollständig, Aufgaben für Anlegen und Status (ADR-0014). Der Service Worker liefert die App-Shell offline aus;
Daten-Requests (/api) laufen nie über seinen Cache.
8b. Benachrichtigungen (Web Push)
Erinnerungen an Aufgaben per Web Push (VAPID, RFC 8291/8292, ADR-0014). Schlüssel als Aspire-Parameter vapid-public-key/
vapid-private-key/vapid-subject bzw. VAPID_* in deploy/.env (Konfiguration WebPush__*), erzeugt mit
dotnet run tools/generate-vapid-keys.cs; ohne Schlüssel ist Push aus. Geräte melden sich unter „Benachrichtigungen“ an,
der TaskReminderService der Api versendet fällige Erinnerungen, der Service Worker zeigt sie an.
9. Betrieb / Deployment
- Lokal:
aspire runbzw.dotnet run --project src/FamilyPlanner.AppHost. - Produktion:
deploy/docker-compose.yml(SQL Server + Volume, migrations, api, web, optional Caddy für HTTPS). Images werden mit dem gemeinsamenDockerfile(Build-ArgumentPROJECT) gebaut; alle Container laufen als Non-Root (app-User der Microsoft-Images, SQL Server alsmssql). - Backups:
deploy/scripts/backup.sh(SQL-Backup ins Volumesql-backup+ Kopie nachdeploy/backups/),deploy/scripts/restore.sh. aspire publishmitAddDockerComposeEnvironment("compose")erzeugt alternativ eine Compose-Datei aus dem App-Modell.- Konfiguration ausschließlich per Umgebungsvariablen/
.env(siehedeploy/.env.example), lokal User Secrets / Aspire-Parameter. - Health Checks:
/alive(Liveness),/health(Readiness inkl. DB bei der Api) – auch in Produktion gemappt (nur Status-Text, keine Details). Der Web-Host beantwortet/healthselbst und leitet es nicht an die Api weiter. - Data Protection Keys werden in der Datenbank gespeichert (Api), damit Cookies Container-Neustarts überleben.
- Kameras (Phase 18): Auch die Kamera-Streams holt die Api (nicht der Browser) und reicht sie weiter;
localhostin der Kamera-Adresse ist im Container der Container selbst – dort die LAN-IP oderhost.docker.internaleintragen. Ein Reverse Proxy davor darf die Antwort nicht puffern (ADR-0024). - Haus (Phase 17): Die Api spricht die Smart-Home-Geräte (Shelly) direkt im Heimnetz an – der Api-Container muss sie also erreichen (Bridge-Netzwerk mit Routing ins LAN genügt meist; sonst
network_mode: hostfür die Api oder ein Routing/Macvlan-Netz, siehe ADR-0023). Die Adressprüfung erlaubt nur private Adressen/Loopback.
10. Querschnitt
- Nullable,
TreatWarningsAsErrors, zentrales Paketmanagement (Directory.Packages.props),.editorconfig. - Validierung: FluentValidation in jedem Slice.
- Logging/Tracing/Metriken: OpenTelemetry (OTLP → Aspire-Dashboard).
- Zeitzone:
TimeProvider+TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin")(IANA-ID, funktioniert auf Linux und Windows mit ICU).