CI / build-and-test (push) Canceled after 0s
Verspätungen sind i.d.R. nicht entschuldigungsfähig - "Offene Entschuldigungen" erinnert dafür nicht mehr, stattdessen eskaliert die Mustererkennung ab 5 Verspätungen im Schuljahr zu einem Elterngespräch/Brief-Hinweis mit Wiedervorlage-Option. Die Jahresfehlquote nutzte bislang den 1. August als fest verdrahteten Schuljahresbeginn und zählte damit noch laufende Sommerferien als Schultage mit, was die Quote kurz nach Schuljahresbeginn stark verfälschte (~57% statt ~100% bei durchgehend fehlenden Schülern). Nutzt jetzt den echten WebUntis-Ferienkalender (GetHolidaysAsync, gecacht in WebUntisSettingsService). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
156 lines
5.7 KiB
C#
156 lines
5.7 KiB
C#
using LehrerApp.Sync.Crypto;
|
|
using System.Text.Json;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
internal class WebUntisSettingsConfig
|
|
{
|
|
public bool Enabled { get; set; }
|
|
public string? EncryptedIcalUrl { get; set; }
|
|
public DateTime? LastSyncAt { get; set; }
|
|
public string LastSyncStatus { get; set; } = "";
|
|
public string? EncryptedApiCredentials { get; set; }
|
|
public int? TeacherUntisId { get; set; }
|
|
public int? HomeroomClassUntisId { get; set; }
|
|
public string? HomeroomClassName { get; set; }
|
|
public List<CachedUntisHoliday>? CachedHolidays { get; set; }
|
|
public DateTime? HolidaysFetchedAt { get; set; }
|
|
}
|
|
|
|
public sealed record WebUntisCredentials(string School, string Host, string Username, string Password);
|
|
|
|
/// <summary>Ferienzeitraum aus WebUntis' <c>getHolidays</c>-Bericht, hier auf die für die
|
|
/// Fehlquoten-Berechnung ("Klassenlehrer"-Feature) relevanten Felder reduziert. Kein Geheimnis
|
|
/// (anders als iCal-URL/API-Zugangsdaten in dieser Datei), deshalb unverschlüsselt gecacht.</summary>
|
|
public sealed record CachedUntisHoliday(string Name, DateOnly Start, DateOnly End);
|
|
|
|
/// <summary>
|
|
/// Einstellungen für den WebUntis-iCal-Abgleich (Nutzer-Feedback, siehe TODO.md). Liegt wie
|
|
/// AiSettingsService/SyncSettingsService in LehrerApp.Desktop statt LehrerApp.Core, da die
|
|
/// Verschlüsselung über <see cref="SyncCrypto"/> aus LehrerApp.Sync läuft — Core bleibt bewusst
|
|
/// frei von Abhängigkeiten außerhalb von .NET selbst (siehe CLAUDE.md).
|
|
///
|
|
/// Die iCal-URL trägt ein eingebettetes Auth-Token und wird deshalb wie ein Passwort behandelt:
|
|
/// nie im Klartext persistiert, nur AES-256-GCM-verschlüsselt (gleicher Mechanismus wie beim
|
|
/// KI-Backend-Token) mit einem eigenen, dateirechte-geschützten Schlüssel.
|
|
/// </summary>
|
|
public class WebUntisSettingsService
|
|
{
|
|
private readonly string _configPath;
|
|
private readonly string _keyPath;
|
|
private readonly byte[] _urlKey;
|
|
private WebUntisSettingsConfig _config;
|
|
|
|
public bool Enabled => _config.Enabled;
|
|
public bool IsConfigured => _config.EncryptedIcalUrl is not null;
|
|
public bool ApiIsConfigured => _config.EncryptedApiCredentials is not null;
|
|
public int? TeacherUntisId => _config.TeacherUntisId;
|
|
public int? HomeroomClassUntisId => _config.HomeroomClassUntisId;
|
|
public string? HomeroomClassName => _config.HomeroomClassName;
|
|
public DateTime? LastSyncAt => _config.LastSyncAt;
|
|
public string LastSyncStatus => _config.LastSyncStatus;
|
|
|
|
public WebUntisSettingsService(string appDataPath)
|
|
{
|
|
_configPath = Path.Combine(appDataPath, "webuntis-settings.json");
|
|
_keyPath = Path.Combine(appDataPath, "webuntis-url.key");
|
|
_urlKey = SyncCrypto.LoadKey(_keyPath) ?? GenerateAndSaveKey();
|
|
_config = Load();
|
|
}
|
|
|
|
public void SetEnabled(bool enabled)
|
|
{
|
|
_config.Enabled = enabled;
|
|
Save();
|
|
}
|
|
|
|
public void SetIcalUrl(string url)
|
|
{
|
|
_config.EncryptedIcalUrl = SyncCrypto.EncryptObject(url, _urlKey);
|
|
Save();
|
|
}
|
|
|
|
public string? GetIcalUrl() =>
|
|
_config.EncryptedIcalUrl is null ? null : SyncCrypto.DecryptObject<string>(_config.EncryptedIcalUrl, _urlKey);
|
|
|
|
public void ClearIcalUrl()
|
|
{
|
|
_config.EncryptedIcalUrl = null;
|
|
_config.Enabled = false;
|
|
Save();
|
|
}
|
|
|
|
public void SetApiCredentials(WebUntisCredentials credentials)
|
|
{
|
|
_config.EncryptedApiCredentials = SyncCrypto.EncryptObject(credentials, _urlKey);
|
|
Save();
|
|
}
|
|
|
|
public WebUntisCredentials? GetApiCredentials() => _config.EncryptedApiCredentials is null
|
|
? null
|
|
: SyncCrypto.DecryptObject<WebUntisCredentials>(_config.EncryptedApiCredentials, _urlKey);
|
|
|
|
public void ClearApiCredentials()
|
|
{
|
|
_config.EncryptedApiCredentials = null;
|
|
_config.TeacherUntisId = null;
|
|
_config.HomeroomClassUntisId = null;
|
|
_config.HomeroomClassName = null;
|
|
Save();
|
|
}
|
|
|
|
public void SetTeacherUntisId(int? teacherUntisId)
|
|
{
|
|
_config.TeacherUntisId = teacherUntisId;
|
|
Save();
|
|
}
|
|
|
|
// Die Klasse, deren Klassenlehrer/-in man ist - unabhängig von jeder LearningGroup (siehe
|
|
// TODO.md "Klassenlehrer"-Feature): man ist es für die ganze Klasse, nicht für einen einzelnen
|
|
// Unterricht/Kurs. HomeroomClassName ist wie TeacherUntisId unverschlüsselt, da kein Geheimnis.
|
|
public void SetHomeroomClass(int? untisId, string? name)
|
|
{
|
|
_config.HomeroomClassUntisId = untisId;
|
|
_config.HomeroomClassName = name;
|
|
Save();
|
|
}
|
|
|
|
public void SetLastSync(DateTime at, string status)
|
|
{
|
|
_config.LastSyncAt = at;
|
|
_config.LastSyncStatus = status;
|
|
Save();
|
|
}
|
|
|
|
public DateTime? HolidaysFetchedAt => _config.HolidaysFetchedAt;
|
|
public IReadOnlyList<CachedUntisHoliday>? GetCachedHolidays() => _config.CachedHolidays;
|
|
|
|
public void SetCachedHolidays(IReadOnlyList<CachedUntisHoliday> holidays, DateTime at)
|
|
{
|
|
_config.CachedHolidays = holidays.ToList();
|
|
_config.HolidaysFetchedAt = at;
|
|
Save();
|
|
}
|
|
|
|
private byte[] GenerateAndSaveKey()
|
|
{
|
|
var key = SyncCrypto.GenerateKey();
|
|
SyncCrypto.SaveKey(key, _keyPath);
|
|
return key;
|
|
}
|
|
|
|
private void Save() => File.WriteAllText(_configPath, JsonSerializer.Serialize(_config));
|
|
|
|
private WebUntisSettingsConfig Load()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(_configPath))
|
|
return JsonSerializer.Deserialize<WebUntisSettingsConfig>(File.ReadAllText(_configPath))
|
|
?? new WebUntisSettingsConfig();
|
|
}
|
|
catch { /* beschädigte Konfiguration -> Standardwert */ }
|
|
return new WebUntisSettingsConfig();
|
|
}
|
|
}
|