docs: phase 11c in roadmap, architecture and ADR-0021; unit tests for schedule api helpers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8978cebd17
commit
3c0f6b4b7c
4 files changed
+199
-6
No files matched your search
@@ -87,7 +87,7 @@ Alle Zeitpunkte werden als UTC (`DateTimeOffset`/`DateTime` UTC) gespeichert und
|
||||
| Kalender ✅ | `FamilyCalendar` (Name, Color, IsDefault, Soft Delete), `CalendarPermission` (Rolle **oder** Benutzer → None/Read/WriteOwn/Write), `CalendarEvent` (Title, Location, Notes, IsAllDay, 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) – ADR-0015/0016 |
|
||||
| KI ✅ | `AiProviderConfig` (Name, Type Ollama/Claude/OpenAiCompatible, BaseUrl, Model, ApiKeyProtected, TimeoutSeconds, IsActive, AllowImages, Purposes, DailyLimitPerUser, RowVersion, Soft Delete), `AiUsage` (Person, Anbieter, Zweck, Dauer, Tokens, Erfolg/Fehlerart – ohne Inhalt) – ADR-0020 |
|
||||
| Chat ✅ | `ChatChannel` (Family / Direct), `ChatMember` (LastReadAt), `ChatMessage` (Text, HasImage), `ChatAttachment`, `ChatReaction` |
|
||||
| Lernen ✅ (10a) | `LearningSet` (Titel, Fach Mathe/Deutsch, Klassenstufe 1–6, 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 vorgesehen, 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); `FamilySettings.LearningPhotoRetentionDays`; `PointsLedgerEntry.LearningSetId` – ADR-0021 |
|
||||
| Lernen ✅ (10a) | `LearningSet` (Titel, Fach Mathe/Deutsch, Klassenstufe 1–6, 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 vorgesehen, 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 |
|
||||
| Betrieb | `AuditLogEntry` ✅ (ActorName, Action, TargetType/Id/Name, Details, CreatedAt), `PushSubscription` ✅ (UserId, Endpoint + Hash, P256dh, Auth, LastSuccessAt, FailureCount) |
|
||||
|
||||
## 4. Rollen- und Berechtigungsmodell
|
||||
@@ -156,7 +156,7 @@ Das UI bekommt die wirksamen Berechtigungen über `/api/auth/me` und blendet anh
|
||||
| ICS-Feed | `GET /ics/{token}.ics` (öffentlich, Token-gesichert, ETag/Last-Modified) |
|
||||
| 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) |
|
||||
| KI ✅ | Admin: `GET/POST /api/admin/ai/providers`, `PUT/DELETE …/{id}`, `POST …/{id}/test`, `GET /api/admin/ai/usage`; Eltern: `GET /api/ai/status` (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`, Foto-KI-Prüfung) (ADR-0021) |
|
||||
| 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); Automatik (11c, alle Routen `LearningManage` + `AiUse`): `GET/POST /api/learning/schedules`, `PUT/DELETE …/schedules/{id}`, `POST …/schedules/{id}/run` (ADR-0021) |
|
||||
| Dashboard ✅ | `GET /api/dashboard` (alle Kacheln der Rolle in einem Request, ADR-0018) |
|
||||
| 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` |
|
||||
|
||||
+8
-3
@@ -132,6 +132,14 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te
|
||||
- [x] 10c: Papierlösung per Foto (Upload je Aufgabe/Blatt), Prüfung manuell durch Eltern, danach KI-Prüfung mit Sicherheitsangabe und Eltern-Bestätigung (`LearningAnswerType.Paper`, `LearningGrader.Ai` sind als Werte vorgesehen)
|
||||
- [x] Foto-Aufbewahrungsfrist (Standard 30 Tage nach Prüfung, einstellbar, 0 = sofort) – mit 10c
|
||||
|
||||
## Phase 11c – Lernen: Auswertung, Note, Automatik (ADR-0021)
|
||||
- [x] Tolerantere Antwort-Auswertung für Zahlen: Einheiten und Schreibweisen werden gelesen („1€ 30 Cent“ = „1,30“, „2 Euro 50“, „1,3 €“, „12 cm“, „1 m 30 cm“, „50 Cent“), Tausenderpunkte und Leerzeichen; umgerechnet wird nur bei Euro/Cent, m/cm, kg/g, l/ml (beide plausiblen Werte gelten). Ziffern ohne eindeutige Zahl („5 Äpfel und 3 Birnen“, „1 h 30 min“) bleiben offen statt falsch
|
||||
- [x] Unklare Zahlenantworten optional von der KI gegenprüfen lassen (derselbe Knopf und Endpunkt wie die Foto-Prüfung, Zweck `LearningImageGrading` ohne Rückfall-Anbieter, Datenschutzhinweis, Tageslimit); Ergebnis bleibt Vorschlag, Eltern bestätigen
|
||||
- [x] Kind sieht die bestätigte Note auch im Kopf des Lernzettels (Ergebnisansicht), nicht nur in der Liste
|
||||
- [x] Übungsblätter automatisch erzeugen: Zeitplan je Kind und Fach (Wochentage, Uhrzeit Europe/Berlin, Anzahl, Klassenstufe, Startschwierigkeit, Steigerung nach N Blättern), Entwurf zur Prüfung (Standard) oder direkt zuweisen (mit Warnung und Qualitätsgrenze), Hintergrunddienst `LearningScheduleService` (TimeProvider, idempotent, Sperre + eindeutiger Plantermin), „Jetzt ausführen“, Tageslimit/Fehler werden protokolliert und später erneut versucht, Audit ohne Inhalt; Migration `Phase11cLearningAuto`
|
||||
- [x] Oberfläche: Reiter „Automatisch“ in der Lernen-Seite (Liste, Dialog, Pause, Löschen, Jetzt ausführen); nur Eltern mit `LearningManage` + `AiUse`
|
||||
- [x] Tests: Unit (Zahlen mit Einheiten, Plantermine inkl. Sommerzeit, Schwierigkeit, Wartezeiten, Validator), bUnit (Reiter, Dialog, Pause, Löschen, Jetzt ausführen, Kopf-Note, KI-Knopf bei unklarer Antwort), Integration (Rechte, Jetzt ausführen, Qualitätsgrenze, Schwierigkeit, Tageslimit, Fehler, Audit, echter Lauf des Hintergrunddienstes)
|
||||
|
||||
## Phase 11 – Härtung
|
||||
- [ ] Content-Security-Policy (Basis-Header X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS sind seit Phase 1 aktiv)
|
||||
- [ ] Eigener SQL-Login mit minimalen Rechten statt `sa` für Api/MigrationService
|
||||
@@ -163,9 +171,6 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te
|
||||
- Chat: Emoji-Auswahl (Button neben „Bild senden“)
|
||||
- Chat: KI-Chat als zusätzlicher Kanal (z. B. „KI-Chat“ unter „Familienchat“): Chattext wird zum Prompt, die Antwort erscheint im Verlauf; Admin schaltet ihn ein/aus und legt fest, welche Anbieter/Modelle auswählbar sind (neuer `AiPurpose`, Tageslimit und Datenschutz-Opt-in aus ADR-0020; Kinder-Schutz: Rechte, Inhaltsfilter, Protokoll ohne Inhalt klären)
|
||||
- Essen: Wunschliste – Wünsche und bereits hinterlegte Gerichte landen in einer dauerhaften, durchsuchbaren Wunschliste und lassen sich jederzeit wieder in den Plan übernehmen
|
||||
- Lernen: tolerantere Antwort-Auswertung für Zahlen mit Einheiten und Schreibweisen (z. B. „1€ 30 Cent“ = „1,30“, „2 Euro 50“); unklare Fälle optional zur Gegenprüfung an die KI (Purpose und Opt-in aus ADR-0020), Ergebnis bleibt Vorschlag, Eltern bestätigen
|
||||
- Lernen: Kind sieht die Note auch auf dem Lernzettel selbst (Kopfbereich des Blatts/Ergebnisansicht), nicht nur in der Liste
|
||||
- Lernen: Übungsblätter automatisch erzeugen (Zeitplan je Kind, z. B. täglich/wöchentlich 10 neue Aufgaben, Schwierigkeit steigt schrittweise; Hintergrunddienst, nur Eltern konfigurieren, Tageslimit der KI beachten, Entwürfe bleiben prüfbar oder werden nach Eltern-Einstellung direkt zugewiesen)
|
||||
- Einkaufsliste: kleiner Hinweis „hinzugefügt von …“ unter dem Eintrag; Plausibilitätsprüfung für Kinder – offensichtlicher Unsinn wird nur nach Rückfrage gespeichert (Regelliste/Wörterbuch, optional KI)
|
||||
- Dashboard: allgemeine Kachel für Freitext-Infos (Eltern pflegen, z. B. „Wlan-Passwort“, „Ferien bis …“)
|
||||
- Oberfläche: Klick auf die eigenen Initialen öffnet ein Fenster/Menü (Passwort ändern, Meine Einstellungen, Abmelden)
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-0021: Lernaufgaben – manuell oder per KI, digital oder auf Papier
|
||||
|
||||
* Status: akzeptiert (Phase 10; Entscheidungen der Rückfragen am 2026-10-10 eingearbeitet) – **Phase 10a, 10b und 10c umgesetzt** (Abschnitte „Umsetzung Phase 10a“, „10b“ und „10c“)
|
||||
* Status: akzeptiert (Phase 10; Entscheidungen der Rückfragen am 2026-10-10 eingearbeitet) – **Phase 10a, 10b, 10c und 11c umgesetzt** (Abschnitte „Umsetzung Phase 10a“, „10b“, „10c“ und „11c“)
|
||||
* Datum: 2026-10-10
|
||||
|
||||
## Kontext
|
||||
@@ -196,6 +196,43 @@ Phase 10c liefert die Papierlösung per Foto, die manuelle Prüfung und die opti
|
||||
* Nicht bestätigte (z. B. liegengebliebene) Versuche behalten ihre Fotos unbegrenzt.
|
||||
* Die Prüfung der KI-Voraussetzung „Bilder-Opt-in fehlt“ ist wegen der Anbieter-Validierung (Cloud-Anbieter mit Foto-Zweck brauchen das Opt-in schon beim Speichern) im Endpunkt nur defensiv und per Unit-/`AiService`-Test abgedeckt, nicht per Integrationstest.
|
||||
|
||||
## Umsetzung Phase 11c (2026-10-10): Auswertung, Note, Automatik
|
||||
|
||||
Migration `Phase11cLearningAuto` (Tabellen `LearningSchedules`, `LearningScheduleRuns`, Spalte `LearningSets.ScheduleId`).
|
||||
|
||||
### Tolerantere Zahlen und KI-Gegenprüfung
|
||||
* `LearningNumbers.ParseLoose` (Domain) liest zusätzlich Einheiten und Schreibweisen: Einheitenwörter (€, Euro, Cent, cm, m, km, kg, g, l, ml, min, h, %, ° …) und Füllwörter („ca.“, „etwa“) werden nur **entfernt**. Umgerechnet wird ausschließlich bei „Euro + Cent“ („1€ 30 Cent“, „2 Euro 50“ = 1,30/2,50),
|
||||
„Cent“ allein („50 Cent“ = 0,50 **oder** 50) und „m + cm“, „kg + g“, „l + ml“ (beide plausiblen Werte gelten, wie bei „1.234“). Zeiten („1 h 30 min“), unbekannte Wörter, mehrere unzusammenhängende Zahlen und Brüche bleiben unklar.
|
||||
* Antworten mit Ziffern, aber ohne eindeutige Zahl, sind nicht mehr „falsch“, sondern `Open` („Antwort nicht eindeutig lesbar – bitte prüfen“); ohne jede Ziffer bleibt „Keine Zahl erkannt“ = falsch. Die Auto-Bewertung ist damit nie strenger als vorher, nur toleranter.
|
||||
* **KI-Gegenprüfung** unklarer Zahlenantworten läuft über denselben Endpunkt und Knopf wie die Foto-Prüfung (`POST …/attempts/{id}/ai-grade`, `LearningManage` + `AiUse`, Datenschutzbestätigung): Zweck `LearningImageGrading` – also nur mit ausdrücklich zugeordnetem Anbieter, **kein Rückfall** auf den Standard-Anbieter –,
|
||||
Tageslimit, gleiches strenges Schema (`LearningAiGrading`), Sicherheit unter 70 % bleibt offen. Der Prompt (`LearningAnswerCheckPrompt`) enthält nur Fach, Stufe, Aufgabe, Musterlösung und den Antworttext (als Daten abgegrenzt), keine Namen, kein Bild. Ergebnis ist ein Vorschlag (`Grader = Ai`), das Kind sieht ihn erst nach der Bestätigung.
|
||||
Folge: Die Funktion braucht einen Anbieter mit diesem Zweck (der Knopf hängt an `CanGradeImages`); ein reiner Standard-Anbieter bietet sie nicht an.
|
||||
|
||||
### Note im Kopf des Zettels
|
||||
Die bestätigte Note (Prüfungen) steht zusätzlich als Chip neben dem Titel der Ergebnisansicht des Kindes (bisher nur im Ergebnis-Kasten und in der Liste). Übungen haben weiterhin keine Note.
|
||||
|
||||
### Automatische Übungsblätter
|
||||
* **Zeitplan** (`LearningSchedule`, Soft Delete, `RowVersion`): Kind (bloße Id), Ersteller `OwnerUserId`, Fach, Klasse, Anzahl 1–30, Startschwierigkeit 1–3 und `RaiseEveryRuns` (nach N erzeugten Blättern eine Stufe mehr, höchstens 3; 0 = gleichbleibend), optional Wunsch (≤ 300 Zeichen),
|
||||
`Weekdays` (Bitmaske; „täglich“ = alle sieben, „wöchentlich“ = ein Tag), `TimeOfDay` in der **Familienzeitzone** (Sommerzeit-fest über `FamilyTime`), `AutoAssign` (Standard **aus** = Entwurf), `IsActive`. Die Schwierigkeit geht über den vorhandenen Verlauf `Easy/Medium/Hard` in den 10b-Prompt. Kein Freitext im System-Prompt.
|
||||
* **Rechte:** alle Routen (auch Lesen) `LearningManage` **und** `AiUse` – Kinder/Gäste 403, anonym 401. Wer zuletzt ändert, wird `OwnerUserId`; die KI läuft im Namen des Erstellers, **sein** Tageslimit gilt, und zur Laufzeit wird erneut geprüft, dass er aktiv ist und beide Rechte hat und das Kind noch ein aktives Kind ist – sonst wird der Zeitplan pausiert.
|
||||
* **Hintergrunddienst** `LearningScheduleService` (`PeriodicTimer` über `TimeProvider`, 60 s, sofort beim Start) ruft `LearningScheduleRunner.SweepAsync`: fällig ist nur der **jüngste** vergangene Plantermin nach `HandledThroughAt` – nach einem Ausfall also genau ein Blatt, kein Rückstau; beim Anlegen, Wiedereinschalten und Ändern von Zeit/Tagen wird `HandledThroughAt = jetzt` gesetzt,
|
||||
damit nichts nachträglich feuert. Höchstens 10 Läufe je Durchlauf, nacheinander; ist das Tageslimit eines Erstellers erreicht, werden seine übrigen Pläne im Durchlauf übersprungen.
|
||||
* **Idempotenz:** Sperre (`LockedUntil`, 10 min) atomar per `ExecuteUpdate` mit `RowVersion`; Blatt, Lauf-Eintrag und `HandledThroughAt` werden in **einem** `SaveChanges` geschrieben; zusätzlich lässt ein eindeutiger gefilterter Index `(ScheduleId, SlotUtc)` je Plantermin nur ein erzeugtes Blatt zu. Der Kontext bleibt bis nach dem KI-Aufruf ohne Vormerkungen (`AiService` schreibt sein Nutzungsprotokoll über denselben Kontext).
|
||||
Die Erzeugung teilt sich die Pipeline mit dem 10b-Endpunkt (`LearningAiDraftPipeline`: Prompt, Parser, Nachrechnen).
|
||||
* **Fehler und Limit:** Tageslimit = Aufschub (`Deferred`, nächster Versuch ab lokaler Mitternacht, kein Fehlerzähler); sonst Wartezeit 15 min, 30 min, 1 h, 2 h, 4 h, nach fünf Fehlversuchen wird der Termin aufgegeben (`HandledThroughAt`, Audit-Eintrag). Meldungen sind feste Texte je Fehlerart, nie Anbieter- oder Aufgabentext; `LastStatus/LastMessage` am Zeitplan, jeder Lauf als `LearningScheduleRun` (die jüngsten 20 bleiben).
|
||||
* **Schutz:** Bei mehr als 5 unerledigten Blättern desselben Zeitplans (Entwurf/zugewiesen ohne Versuch) wird kein neues erzeugt (Status „Übersprungen“, kein KI-Aufruf); höchstens 30 Zeitpläne.
|
||||
* **Entwurf oder direkt:** Standard ist ein Entwurf (`Source = Ai`, Herkunft wie 10b) – dann gilt „nichts geht ungeprüft an Kinder“ unverändert. **„Direkt zuweisen“ weicht davon bewusst ab** (ausdrückliche Eltern-Einstellung je Zeitplan, Warnung im Dialog); Qualitätsgrenze: bleiben weniger als die Hälfte der gewünschten Aufgaben nach der Prüfung übrig, wird trotzdem nur ein Entwurf gespeichert (`CreatedAsDraft`).
|
||||
Der Titel bekommt das Datum („Rechnen (13.10.)“). Kinder-DTOs enthalten weder Zeitplan noch Herkunft (Test).
|
||||
* **„Jetzt ausführen“** (`POST …/schedules/{id}/run`) läuft im Namen des Auslösenden, verbraucht keinen Plantermin, ändert die geplanten Läufe nicht und zählt für die Schwierigkeitssteigerung mit (ein erzeugtes Blatt = ein Zählschritt). Tageslimit → 429, Fehler → Status der KI-Fehlerart, Sperre → 409.
|
||||
* **Audit** (`learning.schedule_created/_updated/_deleted/_run/_paused`): Fach, Klasse, Stufe, Anzahl, Modus, Zeiten – nie Wunsch, Aufgaben, Antworten oder Anbietertexte; nicht jeder Wiederholversuch, nur erzeugte Blätter, aufgegebene Termine und Pausen.
|
||||
* **Oberfläche:** Reiter „Automatisch“ in `ParentOverview` (`ScheduleTab`, `ScheduleDialog`): Liste mit nächstem Lauf, letztem Ergebnis, Pause, Ändern, Löschen (Bestätigung), „Jetzt ausführen“ (Bestätigung, Wartezustand); Datenschutzhinweis bei Cloud-Anbietern; ohne eingerichtete KI sind Anlegen und Ausführen ausgeblendet. Alle Meldungen werden als Text angezeigt.
|
||||
|
||||
### Abweichungen und bewusste Vereinfachungen
|
||||
* Die Schwierigkeit steigt in drei Stufen (leicht/mittel/schwer) über den vorhandenen Prompt-Verlauf, nicht feiner; der Modus ist immer Übung, es gibt kein Fälligkeitsdatum und keine Punkte außer den 10 Punkten des 10b-Entwurfs.
|
||||
* Der Fehlerzähler gehört zum Zeitplan, nicht zum einzelnen Termin: Löst ein neuer Termin einen noch wiederholenden ab, erbt er den Zähler.
|
||||
* Der Hintergrunddienst hat keinen eigenen Unit-Test mit gesteuerter Uhr (der Runner braucht SQL Server und `ExecuteUpdate`); er ist über Integrationstests inkl. eines echten Laufs (Termin in der nächsten Minute) abgedeckt, die Zeitlogik über reine Unit-Tests.
|
||||
* Zeitpläne erscheinen nicht im Live-Kanal; die Liste lädt beim Öffnen und nach Aktionen.
|
||||
|
||||
## Bewusst nicht Teil des ersten Schnitts
|
||||
Offline-Lösen, Sprachausgabe/Diktat, Lernstatistiken über Wochen, Adaptive Schwierigkeit, Formeleditor (Aufgaben bleiben Text), Auswertung mehrerer Kinder im Vergleich,
|
||||
Überwachung gegen Schummeln (die Emulation ist ein Übungsrahmen, keine Fernprüfung).
|
||||
@@ -205,6 +242,7 @@ Offline-Lösen, Sprachausgabe/Diktat, Lernstatistiken über Wochen, Adaptive Sch
|
||||
* Phase 10a: Lernaufgaben manuell + digital lösen + automatische Bewertung + Elternprüfung.
|
||||
* Phase 10b: KI-Erzeugung (Entwurf-Assistent).
|
||||
* Phase 10c: Papierlösung mit Foto (manuelle Prüfung), danach KI-Prüfung.
|
||||
* Phase 11c: tolerantere Zahlen, Note im Zettelkopf, automatische Übungsblätter.
|
||||
|
||||
## Offene Fragen
|
||||
Notenschlüssel für Deutsch-Diktate/Freitext (Standard-Schulnoten reichen für Punkte-basierte Wertung zunächst).
|
||||
@@ -0,0 +1,150 @@
|
||||
using FamilyPlanner.Api.Features.Ai;
|
||||
using FamilyPlanner.Api.Features.Learning;
|
||||
using FamilyPlanner.Domain.Common;
|
||||
using FamilyPlanner.Domain.Learning;
|
||||
using FamilyPlanner.Shared.Learning;
|
||||
|
||||
namespace FamilyPlanner.UnitTests.Learning;
|
||||
|
||||
/// <summary>Validator, Anzeige-Zustand und feste Texte der Lern-Automatik (Phase 11c).</summary>
|
||||
public sealed class LearningScheduleApiTests
|
||||
{
|
||||
private static readonly TimeZoneInfo Zone = FamilyTime.Berlin;
|
||||
private static readonly SaveLearningScheduleRequestValidator Validator = new();
|
||||
|
||||
private static SaveLearningScheduleRequest Valid() =>
|
||||
new(Guid.NewGuid(), LearningSubjects.Math, 3, 10, 1, 4, null, LearningWeekdays.Weekdays, new TimeOnly(15, 0));
|
||||
|
||||
private static DateTimeOffset Utc(int day, int hour, int minute = 0) => new(2026, 10, day, hour, minute, 0, TimeSpan.Zero);
|
||||
|
||||
// ---------- Validator ----------
|
||||
|
||||
[Fact]
|
||||
public void A_complete_request_is_valid() => Assert.True(Validator.Validate(Valid()).IsValid);
|
||||
|
||||
[Theory]
|
||||
[InlineData("Sport")]
|
||||
[InlineData("")]
|
||||
public void Unknown_subjects_are_rejected(string subject) =>
|
||||
Assert.Contains(nameof(SaveLearningScheduleRequest.Subject), Validator.Validate(Valid() with { Subject = subject }).Errors.Select(e => e.PropertyName));
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(7)]
|
||||
public void The_grade_level_must_be_1_to_6(int grade) => Assert.False(Validator.Validate(Valid() with { GradeLevel = grade }).IsValid);
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false)]
|
||||
[InlineData(1, true)]
|
||||
[InlineData(30, true)]
|
||||
[InlineData(31, false)]
|
||||
public void The_task_count_is_limited(int count, bool valid) => Assert.Equal(valid, Validator.Validate(Valid() with { ItemCount = count }).IsValid);
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, false)]
|
||||
[InlineData(1, true)]
|
||||
[InlineData(3, true)]
|
||||
[InlineData(4, false)]
|
||||
public void The_start_difficulty_is_1_to_3(int level, bool valid) => Assert.Equal(valid, Validator.Validate(Valid() with { StartDifficulty = level }).IsValid);
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1, false)]
|
||||
[InlineData(0, true)]
|
||||
[InlineData(52, true)]
|
||||
[InlineData(53, false)]
|
||||
public void The_raise_interval_is_limited(int every, bool valid) => Assert.Equal(valid, Validator.Validate(Valid() with { RaiseEveryRuns = every }).IsValid);
|
||||
|
||||
[Fact]
|
||||
public void At_least_one_known_weekday_is_required()
|
||||
{
|
||||
Assert.False(Validator.Validate(Valid() with { Weekdays = LearningWeekdays.None }).IsValid);
|
||||
Assert.False(Validator.Validate(Valid() with { Weekdays = (LearningWeekdays)255 }).IsValid);
|
||||
Assert.True(Validator.Validate(Valid() with { Weekdays = LearningWeekdays.Saturday }).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_time_has_no_seconds_and_the_extra_wish_is_limited()
|
||||
{
|
||||
Assert.False(Validator.Validate(Valid() with { TimeOfDay = new TimeOnly(7, 0, 30) }).IsValid);
|
||||
Assert.True(Validator.Validate(Valid() with { Extra = new string('a', 300) }).IsValid);
|
||||
Assert.False(Validator.Validate(Valid() with { Extra = new string('a', 301) }).IsValid);
|
||||
Assert.False(Validator.Validate(Valid() with { TargetUserId = Guid.Empty }).IsValid);
|
||||
}
|
||||
|
||||
// ---------- Anzeige-Zustand ----------
|
||||
|
||||
private static LearningSchedule Plan(bool active = true, DateTimeOffset? handled = null, DateTimeOffset? retry = null) => new()
|
||||
{
|
||||
TargetUserId = Guid.NewGuid(),
|
||||
Weekdays = LearningWeekdays.All,
|
||||
TimeOfDay = new TimeOnly(15, 0),
|
||||
IsActive = active,
|
||||
HandledThroughAt = handled ?? Utc(12, 6),
|
||||
RetryAfter = retry,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void The_next_run_is_the_next_slot_once_todays_slot_is_handled()
|
||||
{
|
||||
// 12.10. 16:00 Berlin = 14:00 UTC; der heutige Termin (15:00 Berlin = 13:00 UTC) ist erledigt
|
||||
var dto = LearningScheduleEndpoints.ToDto(Plan(handled: Utc(12, 13)), "Mia", Utc(12, 14), Zone);
|
||||
|
||||
Assert.Equal(Utc(13, 13), dto.NextRunAt);
|
||||
Assert.Equal("Mia", dto.ChildName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_due_slot_shows_now_or_the_retry_time()
|
||||
{
|
||||
var now = Utc(12, 14);
|
||||
|
||||
Assert.Equal(now, LearningScheduleEndpoints.ToDto(Plan(handled: Utc(11, 13)), "Mia", now, Zone).NextRunAt);
|
||||
Assert.Equal(now.AddMinutes(30), LearningScheduleEndpoints.ToDto(Plan(handled: Utc(11, 13), retry: now.AddMinutes(30)), "Mia", now, Zone).NextRunAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_paused_schedule_has_no_next_run() =>
|
||||
Assert.Null(LearningScheduleEndpoints.ToDto(Plan(active: false), "Mia", Utc(12, 14), Zone).NextRunAt);
|
||||
|
||||
[Fact]
|
||||
public void The_current_difficulty_follows_the_run_count()
|
||||
{
|
||||
var plan = Plan();
|
||||
plan.StartDifficulty = 1;
|
||||
plan.RaiseEveryRuns = 2;
|
||||
plan.RunCount = 4;
|
||||
|
||||
Assert.Equal(3, LearningScheduleEndpoints.ToDto(plan, "Mia", Utc(12, 14), Zone).CurrentDifficulty);
|
||||
}
|
||||
|
||||
// ---------- Texte und Hilfen des Runners ----------
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, LearningDifficultyCurve.Easy)]
|
||||
[InlineData(2, LearningDifficultyCurve.Medium)]
|
||||
[InlineData(3, LearningDifficultyCurve.Hard)]
|
||||
public void The_difficulty_level_maps_to_the_draft_curve(int level, LearningDifficultyCurve curve) =>
|
||||
Assert.Equal(curve, LearningScheduleRunner.CurveFor(level));
|
||||
|
||||
[Fact]
|
||||
public void The_title_gets_the_local_date_and_stays_within_the_limit()
|
||||
{
|
||||
var now = Utc(12, 22, 30); // 13.10. 00:30 Berlin
|
||||
|
||||
Assert.Equal("Rechnen (13.10.)", LearningScheduleRunner.TitleWithDate(" Rechnen ", now, Zone));
|
||||
var long_ = LearningScheduleRunner.TitleWithDate(new string('x', 200), now, Zone);
|
||||
Assert.Equal(LearningSet.TitleMaxLength, long_.Length);
|
||||
Assert.EndsWith(" (13.10.)", long_);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Failure_texts_are_fixed_and_never_contain_provider_text()
|
||||
{
|
||||
foreach (var kind in Enum.GetValues<AiErrorKind>())
|
||||
{
|
||||
var text = LearningScheduleRunner.FailureText(kind);
|
||||
Assert.False(string.IsNullOrWhiteSpace(text));
|
||||
Assert.True(text.Length <= LearningSchedule.MessageMaxLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user