Files
Rbm.FamStack/docs/ARCHITECTURE.md
T
openclaw 70f1890641 feat: Phase 19 Haus - Vallox MV ventilation unit (profile, temperatures, profile switching)
- 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
2026-10-16 20:04:14 +02:00

29 KiB
Raw Blame History

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 /api und /hubs an 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: FamilyPlanner auf 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-Schema CalDavBasic: 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 in Resources/*.resx überführt.
  • Ordner Pages/ erfordert per _Imports.razor die Policy PasswordChanged; 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/me und 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 run bzw. 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 gemeinsamen Dockerfile (Build-Argument PROJECT) gebaut; alle Container laufen als Non-Root (app-User der Microsoft-Images, SQL Server als mssql).
  • Backups: deploy/scripts/backup.sh (SQL-Backup ins Volume sql-backup + Kopie nach deploy/backups/), deploy/scripts/restore.sh.
  • aspire publish mit AddDockerComposeEnvironment("compose") erzeugt alternativ eine Compose-Datei aus dem App-Modell.
  • Konfiguration ausschließlich per Umgebungsvariablen/.env (siehe deploy/.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 /health selbst 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; localhost in der Kamera-Adresse ist im Container der Container selbst – dort die LAN-IP oder host.docker.internal eintragen. 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: host fü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).