feat: Untis-Hub-Kadenzkopplung, anpassbarer Statusvorschlag + KI-/MCP-Unterstuetzung beim Fehlzeitenabgleich (Nutzer-Feedback)
CI / build-and-test (push) Canceled after 0s

- UntisHubService.RecordRun: ein abgeschlossener Langzeit-Fehlzeitenabgleich schliesst die
  kurzfristige Kadenz derselben Gruppe automatisch mit ab (nicht umgekehrt).
- Fehlzeitenabgleich-Dialog: neue "Uebernahme als"-ComboBox statt starrem Zielstatus, vorbelegt
  mit dem berechneten Vorschlag, aber frei aenderbar.
- Neuer ai-backend-Endpunkt untis-status.php + AiPlanningService.RequestUntisStatusSuggestionsAsync:
  gebuendelter, anonymisierter KI-Statusvorschlag (nur Positions-Id + Rohsignale, nie Name/Klasse/
  Datum), mit hartem Id-Mengen-Abgleich gegen Verwechslung.
- Neue MCP-Tools (UntisComparisonTools): get_untis_hub_status, get_untis_absence_rows/
  apply_untis_absence_status (anonymer Weg ueber ENr-Zuordnung) sowie
  get_named_untis_absence_pattern als bewusste, eng begrenzte Ausnahme (Name+Fehlzeiten fuer
  explizit angegebene Schueler-IDs, mit Bestaetigung ohne Sitzungsfreigabe - dafuer
  IMcpConfirmationService.ConfirmAsync um allowSessionTrust erweitert).
- MapStatus/ENr-Zuordnung aus dem ViewModel in das neue, geteilte UntisLessonAbsenceHelper
  gezogen, damit Dialog und MCP-Tool nie unterschiedliche Statusvorschlaege berechnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 22:55:33 +02:00
co-authored by Claude Sonnet 5
parent d979a84e7b
commit bfe214ecf2
25 changed files with 900 additions and 62 deletions
+63
View File
@@ -2023,6 +2023,69 @@ WebUntis auffallen").
Der Fehlzeitenabgleich-Button in der einzelnen Lerngruppe (`GroupDetailView`) blieb unverändert,
da kontextgebunden.
**Nachtrag zum Untis-Hub (September 2026):** Nutzer-Feedback aus der ersten echten Nutzung — fast
alle Zeilen liefen anfangs noch nie, dabei fiel auf, dass ein soeben abgeschlossener Langzeit-
Fehlzeitenabgleich die kurzfristige Kadenz derselben Gruppe unverändert als fällig stehen ließ,
obwohl deren Zeitfenster (seit Schuljahresbeginn) das kurzfristige (letzte 14 Tage) als Teilmenge
vollständig enthält.
- `UntisHubService.RecordRun` schließt bei `FehlzeitenLang` jetzt automatisch auch
`FehlzeitenKurz` derselben Gruppe mit demselben Zeitstempel ab (eigener `LastResultSummary`-Text
"durch Abgleich seit Schuljahresbeginn mit erledigt"). Nicht symmetrisch: ein Kurz-Lauf deckt das
lange Fenster nicht ab und lässt `FehlzeitenLang` deshalb unangetastet.
- Im Fehlzeitenabgleich je Unterricht ([WebUntisLessonAbsenceComparisonViewModel.cs](LehrerApp.Desktop/ViewModels/Groups/WebUntisLessonAbsenceComparisonViewModel.cs))
war der aus dem WebUntis-Datensatz abgeleitete Zielstatus (`MapStatus`) bisher nur als Text
sichtbar und wurde 1:1 übernommen oder gar nicht — Nutzer-Feedback: der Status-String aus WebUntis
ließ oft nicht erkennen, wohin die automatische Übernahme führt, eine Zeile wurde deshalb öfter
abgelehnt und der Status danach manuell außerhalb des Dialogs nachgetragen. Spalte "Übernahme als"
ist jetzt eine ComboBox (`WebUntisLessonAbsenceRow.StatusOptions`/`SelectedStatusName`, gleiches
String-Wrapper-Muster wie `GradeCategoryDisplay`), vorbelegt mit dem berechneten Zielstatus, aber
vor dem Übernehmen frei auf einen von sechs plausiblen Status änderbar (die WebUntis hier nie
meldet — "Geschwänzt", "Suspendiert" usw. — bleiben bewusst außen vor).
- KI-gestützter Statusvorschlag über das bestehende ai-backend (bisher nur für die
Unterrichtsplanung genutzt) ergänzt: neuer Endpunkt `ai-backend/untis-status.php` (gleiches
Auth-/Abrechnungsmuster wie `explain.php`) und `AiPlanningService.RequestUntisStatusSuggestionsAsync`.
Bewusst ohne jeden Personenbezug im Payload (Nutzer-Vorgabe: "keimfrei", solange kein Name
mitgeschickt wird) — jede Zeile bekommt nur eine rein technische Positions-Id sowie die
strukturierten Rohsignale (Fehlminuten, "bereits bearbeitet"-Kennzeichen, ENr-Klammerung) und den
Freitext-Grund, nie Name/Klasse/Datum. Alle fraglichen Zeilen eines Abgleichslaufs gehen in
**einer** gebündelten Anfrage raus statt je Zeile einzeln (Kosten/Latenz-Überlegung aus der
Nutzerdiskussion — der Fix-Overhead pro Anfrage dominiert sonst die kurze Nutzlast). Gegen
Verwechslung (Nutzer-Sorge: "die KI könnte was vermischen") wird nicht auf Reihenfolge vertraut,
sondern hart auf Id-Mengengleichheit geprüft — weicht die zurückgegebene Id-Menge auch nur
minimal ab (fehlend/zusätzlich/doppelt), wird die gesamte Antwort verworfen, siehe die vier neuen
Tests in `AiPlanningServiceTests.cs`. Der Vorschlag setzt nur die "Übernahme als"-ComboBox vor
(neuer Button "KI-Vorschlag für Übernahme laden" im Fehlzeitenabgleich), übernimmt nie
eigenständig etwas — "Markierte übernehmen" bleibt der einzige schreibende Schritt.
- MCP-Zugriff von außen auf die Untis-Abgleiche umgesetzt, zweigleisig wie vom Nutzer entschieden —
neue Klasse [UntisComparisonTools.cs](LehrerApp.Desktop/Services/Mcp/Tools/UntisComparisonTools.cs):
- `get_untis_hub_status` (Read): reine Delegation an `UntisHubService.GetRows()`, kein eigener
WebUntis-Zugriff.
- `get_untis_absence_rows`/`apply_untis_absence_status` (bevorzugter, anonymer Weg): eine rein
technische `row-id` (In-Memory, pro Prozesslaufzeit) ordnet zurück, nie ein Schülername verlässt
die App. Nur Zeilen, die WebUntis über die externe Schülernummer (ENr) eindeutig einem
Kursmitglied zuordnet, werden überhaupt gelistet — die namensbasierte Fallback-Zuordnung des
interaktiven Dialogs bleibt dafür bewusst außen vor (eine autonome KI-Schreibaktion ohne Mensch,
der eine Namensauflösung visuell gegenprüft, wäre sonst riskanter als im Dialog). Das schreibende
Tool bestätigt wie jedes andere Write-Tool, die Bestätigungsmeldung nennt aber bewusst nur Datum/
Lerngruppe/Zielstatus, nie den Namen.
- `get_named_untis_absence_pattern` (bewusste, eng begrenzte Ausnahme von der sonst in
`McpToolScope.cs` beschriebenen Regel): verknüpft Name UND Fehlzeiten für explizit angegebene
Schüler-IDs, z.B. für einen Fehlmuster-Vergleich zwischen zwei Schülern — Datensparsamkeit über
den Aufrufparameter (nur die angefragten IDs, nicht der ganze Kurs) statt einer Auswahl-UI, um
keinen weiteren Dialogtyp einzuführen. Erfordert JEDES MAL eine gesonderte Bestätigung ohne
Sitzungsfreigabe (neuer Parameter `allowSessionTrust` auf `IMcpConfirmationService.ConfirmAsync`,
Standard weiterhin `true` für alle bestehenden Write-Tools) — `AvaloniaMcpConfirmationService`
prüft und setzt die Sitzungsfreigabe dafür gar nicht erst, `McpConfirmDialog` blendet die beiden
"nicht mehr nachfragen"-Kontrollkästchen für diesen Fall aus.
- `MapStatus`/die ENr-Zuordnung wurden aus `WebUntisLessonAbsenceComparisonViewModel` in das neue,
framework-freie `UntisLessonAbsenceHelper` (LehrerApp.Desktop/Services/) verschoben, damit Dialog
und MCP-Tool niemals unterschiedliche Statusvorschläge für dieselbe Fehlzeile berechnen.
- Bewusst ohne Tests für `GetUntisAbsenceRows`/`GetNamedUntisAbsencePattern` gelassen (gleiche
Begründung wie bei den drei bestehenden Abgleichs-ViewModels: brauchen einen echten WebUntis-
JSON-RPC-Zugriff) — `GetUntisHubStatus` und die Validierungspfade von `ApplyUntisAbsenceStatus`
(unbekannter/nicht zulässiger Status, unbekannte/abgelaufene row-id, jeweils ohne Bestätigung
erreicht) sind in `UntisComparisonToolsTests.cs` abgedeckt.
### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.