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
+14
View File
@@ -188,6 +188,14 @@ deaktiviert (`AllowOverride None`), oder es läuft nginx statt Apache (dort gilt
grundsätzlich nicht) — dann hilft nur eine serverseitige Konfiguration durch den Hoster/Support
(z.B. bei nginx ein `fastcgi_param HTTP_AUTHORIZATION $http_authorization;`).
## Update für bereits deployte Installationen (Fehlzeiten-Statusvorschlag, `untis-status.php`)
Kein neues DB-Schema nötig (nutzt dieselben `users`/`tokens`/`transactions`-Tabellen und dieselbe
`ai_backend_call_and_charge`-Abrechnung wie `plan.php`/`explain.php`/`gbu.php`). Einfach die neue
Datei `untis-status.php` hochladen. Bewusst ohne jeden Personenbezug im Payload (Nutzer-Vorgabe zum
Untis-Hub) — die Zeilen enthalten nur eine rein technische Positions-Id sowie Fehlminuten/
Bearbeitet-Kennzeichen/ENr-Klammerung und den Freitext-Grund, nie Name, Klasse oder Datum.
## Fehlerbehebung: KI-Antwort endet mitten im JSON
Eine im Rettungsdialog am Ende abgeschnittene Modellantwort entsteht typischerweise nicht durch
@@ -211,6 +219,12 @@ werden muss.
- Ob Prompt Caching tatsächlich greift (`cache_read_input_tokens` > 0 bei einer zweiten Anfrage
innerhalb von 5 Minuten) — der `FakeProvider` simuliert kein Caching, das lässt sich nur gegen
die echte Anthropic-API beobachten (z.B. per Blick in die `transactions`-Tabelle).
- **`untis-status.php` (Fehlzeiten-Statusvorschlag):** dieselbe Einschränkung wie oben (echtes JSON
im erwarteten Schema, korrekte Kostenberechnung) gilt hier ebenso, zusätzlich ungeprüft: ob das
Modell die vorgegebene, feste Statusliste zuverlässig einhält statt eigene Werte zu erfinden
(der Desktop-Client verwirft einen nicht in dieser Liste enthaltenen Wert defensiv, siehe
`WebUntisLessonAbsenceRow.SelectableStatuses`), sowie ob die Id-Rückgabetreue in der Praxis so
zuverlässig ist wie in den Tests angenommen (dort nur mit statischen Antworten simuliert).
- **`substance.php`/`stoffliste.php` (Chemikalien-Recherche):** braucht keinen echten API-Key und
keine Netzwerkverbindung mehr, ist also grundsätzlich einfacher zu verifizieren als der Rest
dieser Liste — trotzdem NICHT in einer echten PHP-Umgebung gelaufen (kein PHP auf der
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
$config = require __DIR__ . '/config.php';
$pdo = ai_backend_db($config);
$user = ai_backend_authenticate($pdo);
if ((float) $user['balance_usd'] <= 0) {
ai_backend_fail(402, 'Kein Guthaben mehr vorhanden.');
}
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body) || !isset($body['rows']) || !is_array($body['rows']) || count($body['rows']) === 0) {
ai_backend_fail(400, 'Ungültige Anfrage.');
}
// Datensparsamkeit (Nutzer-Feedback zum Untis-Hub): dieser Endpunkt bekommt bewusst NIE Namen,
// Klassen oder Datumsangaben - nur eine je Zeile rein technische, für die KI bedeutungslose "id"
// (für die Rückzuordnung im Desktop-Client) sowie die für die Statuseinschätzung tatsächlich
// relevanten Felder aus dem WebUntis-Fehlzeitendatensatz. Mehrere Zeilen eines Abgleichslaufs
// werden in einer Anfrage gebündelt statt je Zeile eine eigene Anfrage zu stellen (Kosten/Latenz).
$systemPrompt = <<<PROMPT
Du hilfst einer Lehrkraft beim Abgleich von WebUntis-Fehlzeiten mit dem internen Anwesenheitsstatus
ihrer Schulverwaltungs-App. Du bekommst eine Liste anonymisierter Fehlzeiten-Zeilen (KEINE Namen,
KEINE Klassen, KEINE Daten) und sollst je Zeile den passendsten internen Status vorschlagen.
## Eingabeschema
{
"rows": [
{
"id": "<rein technische Kennung ohne inhaltliche Bedeutung, exakt so zurückzugeben>",
"reasonText": "<Freitext-Grund aus WebUntis, kann leer sein>",
"absentMinutes": <Zahl, gemeldete Fehlminuten der Unterrichtsstunde>,
"handledOn": <true, wenn der Eintrag laut WebUntis bereits vom Klassenlehrer bearbeitet wurde>,
"externKeyInParentheses": <true/false/null - schulinterne Konvention: Klammerung der
Entschuldigungsnummer bedeutet unentschuldigt, ohne Klammern entschuldigt; null, wenn keine
Nummer hinterlegt ist>,
"currentGuess": "<bereits regelbasiert ermittelter Status, siehe Schlüsselliste unten>"
}
]
}
## Möglicher Zielstatus (exakt einer dieser Schlüssel je Zeile)
- "Present": eigentlich anwesend (z.B. wenn der Freitext klar nahelegt, dass die Fehlzeit ein Irrtum war)
- "Late": Verspätung, kein vollständiges Fehlen der Stunde
- "LeftDuringClass": während des Unterrichts nach Hause entlassen/gegangen
- "ExcusePending": krank gemeldet, Entschuldigung noch offen/unbearbeitet
- "Excused": krank, entschuldigt
- "Unexcused": unentschuldigt
## Aufgabe
"currentGuess" kommt aus einer festen Regel über die strukturierten Felder und ist im Zweifel schon
korrekt - weiche davon nur ab, wenn "reasonText" eindeutig eine andere Einordnung nahelegt (z.B. ein
Text, der klar auf eine Klassenfahrt, einen Arzttermin mit bereits vorliegendem Attest, oder einen
erkennbaren Fehleintrag hindeutet). Bei Unsicherheit "currentGuess" unverändert übernehmen, nicht raten.
## Antwortformat
Antworte AUSSCHLIESSLICH mit gültigem JSON (kein Freitext davor/danach), mit GENAU einem Eintrag je
Eingabe-"id" (dieselbe Anzahl, dieselben "id"-Werte unverändert, keine erfundenen oder ausgelassenen
ids):
{ "suggestions": [ { "id": "<unverändert aus der Eingabe>", "status": "<einer der Schlüssel oben>" } ] }
PROMPT;
$userContent = json_encode($body);
$result = ai_backend_call_and_charge($pdo, $config, $user, $systemPrompt, $userContent);
$parsed = ai_backend_decode_json_response($result['content']);
if (!is_array($parsed) || !isset($parsed['suggestions']) || !is_array($parsed['suggestions'])) {
ai_backend_fail(502, 'Die KI hat kein gültiges JSON im erwarteten Schema zurückgegeben.');
}
echo json_encode($parsed);