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
@@ -40,15 +40,18 @@ public sealed class AvaloniaMcpConfirmationService(AppLogger logger) : IMcpConfi
private bool _trustAll;
public async Task<bool> ConfirmAsync(string title, string message, CancellationToken ct,
[CallerMemberName] string operationKey = "")
[CallerMemberName] string operationKey = "", bool allowSessionTrust = true)
{
bool alreadyTrusted;
lock (_trustLock) alreadyTrusted = _trustAll || _trustedOperations.Contains(operationKey);
if (alreadyTrusted)
if (allowSessionTrust)
{
logger.Info($"MCP: „{title}“ automatisch bestätigt (Sitzungsfreigabe für " +
$"{(_trustAll ? "alle Aktionen" : operationKey)}).");
return true;
bool alreadyTrusted;
lock (_trustLock) alreadyTrusted = _trustAll || _trustedOperations.Contains(operationKey);
if (alreadyTrusted)
{
logger.Info($"MCP: „{title}“ automatisch bestätigt (Sitzungsfreigabe für " +
$"{(_trustAll ? "alle Aktionen" : operationKey)}).");
return true;
}
}
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
@@ -65,7 +68,11 @@ public sealed class AvaloniaMcpConfirmationService(AppLogger logger) : IMcpConfi
var dialog = new McpConfirmDialog
{
DataContext = new McpConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
DataContext = new McpConfirmDialogInfo
{
Title = title, Message = message, ConfirmText = "Übernehmen",
AllowSessionTrust = allowSessionTrust,
},
};
var dialogTask = dialog.ShowDialog<McpConfirmDialogResult>(owner);
var timeoutTask = Task.Delay(Timeout, ct);
@@ -78,7 +85,7 @@ public sealed class AvaloniaMcpConfirmationService(AppLogger logger) : IMcpConfi
return await dialogTask;
});
if (result.Approved)
if (result.Approved && allowSessionTrust)
{
lock (_trustLock)
{