fix: Untis-Hub-Status ueber Geraete hinweg synchronisieren
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
UntisHubJobState (Faelligkeits-Zeitstempel des Untis-Hubs) lief bisher ausserhalb des Sync - jedes Geraet fuehrte seine eigene Buchhaltung, wodurch auf allen Instanzen dieselben Punkte offen blieben und ein bereits erledigter Abgleich anderswo erneut WebUntis-Traffic ausgeloest haette (Nutzer-Feedback). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,4 +57,24 @@ public sealed class ChangeHookTests
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
// Kein eigener Fall in ChangeHookMatrixTests: UntisHubJobStateRepository kennt kein Delete
|
||||
// (siehe IUntisHubJobStateRepository), passt also nicht in deren Save+Delete-Tabellenform.
|
||||
[Fact]
|
||||
public void UntisHubJobStateRepository_Save_LoestOnChangeAus()
|
||||
{
|
||||
using var db = NewInMemoryContext();
|
||||
var calls = new List<(string EntityType, string EntityId, string Operation, object? Payload)>();
|
||||
db.OnChange = (type, id, op, payload) => calls.Add((type, id, op, payload));
|
||||
var repo = new UntisHubJobStateRepository(db);
|
||||
var state = new UntisHubJobState { Kind = UntisHubJobKind.OffenePeriods, LastRunAt = DateTime.UtcNow };
|
||||
|
||||
repo.Save(state);
|
||||
|
||||
var call = Assert.Single(calls);
|
||||
Assert.Equal(nameof(UntisHubJobState), call.EntityType);
|
||||
Assert.Equal(state.Id.ToString(), call.EntityId);
|
||||
Assert.Equal("Save", call.Operation);
|
||||
Assert.Same(state, call.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,7 +907,11 @@ public class UntisHubJobStateRepository(LiteDbContext db) : IUntisHubJobStateRep
|
||||
|
||||
public List<UntisHubJobState> GetAll() => db.UntisHubJobStates.FindAll().ToList();
|
||||
|
||||
public void Save(UntisHubJobState state) => db.UntisHubJobStates.Upsert(state);
|
||||
public void Save(UntisHubJobState state)
|
||||
{
|
||||
db.UntisHubJobStates.Upsert(state);
|
||||
db.OnChange?.Invoke(nameof(UntisHubJobState), state.Id.ToString(), "Save", state);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository
|
||||
|
||||
@@ -72,8 +72,14 @@ public sealed class UntisHubService(
|
||||
public static List<UntisHubJobRow> BuildRows(
|
||||
IReadOnlyList<LearningGroup> eligibleGroups, IReadOnlyList<UntisHubJobState> states, DateTime utcNow)
|
||||
{
|
||||
// Nach Sync-Aktivierung von UntisHubJobState (siehe TODO.md) können zwei Geräte, die
|
||||
// denselben, noch nie gelaufenen Job unabhängig voneinander zum ersten Mal ausführen,
|
||||
// bevor sie sich gegenseitig gesehen haben, kurzzeitig zwei Datensätze für dasselbe
|
||||
// (Kind, GroupId) anlegen - hier den zuletzt gelaufenen wählen statt einen beliebigen.
|
||||
UntisHubJobState? State(UntisHubJobKind kind, Guid? groupId) =>
|
||||
states.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId);
|
||||
states.Where(s => s.Kind == kind && s.GroupId == groupId)
|
||||
.OrderByDescending(s => s.LastRunAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
var rows = new List<UntisHubJobRow>();
|
||||
foreach (var group in eligibleGroups)
|
||||
|
||||
@@ -38,6 +38,20 @@ public sealed class EventApplierTests
|
||||
Assert.Null(db.Students.FindById(student.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_UntisHubJobStateSave_SchreibtEntitaetDirektInDieCollection()
|
||||
{
|
||||
using var db = NewInMemoryContext();
|
||||
var applier = new EventApplier(db, Key);
|
||||
var state = new UntisHubJobState { Kind = UntisHubJobKind.OffenePeriods, LastRunAt = DateTime.UtcNow };
|
||||
|
||||
await applier.ApplyAsync(MakeEvent(nameof(UntisHubJobState), state.Id.ToString(), "Save", state));
|
||||
|
||||
var saved = db.UntisHubJobStates.FindById(state.Id);
|
||||
Assert.NotNull(saved);
|
||||
Assert.Equal(UntisHubJobKind.OffenePeriods, saved!.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyAsync_UnbekannterEntityType_TutNichtsUndWirftNicht()
|
||||
{
|
||||
|
||||
@@ -147,6 +147,7 @@ public class EventApplier(LiteDbContext db, byte[] syncKey, HttpClient? http = n
|
||||
Simple<SubstitutionEntry>(context => context.SubstitutionEntries);
|
||||
Simple<CompetencyDomain>(context => context.CompetencyDomains);
|
||||
Simple<Vorgang>(context => context.Vorgaenge);
|
||||
Simple<UntisHubJobState>(context => context.UntisHubJobStates);
|
||||
|
||||
// Kaskaden-Fälle: dieselben internen LiteDbContext-Hilfsmethoden wie die jeweiligen
|
||||
// Repositories, damit die Kaskade nur an einer Stelle im Code existiert.
|
||||
|
||||
@@ -2097,6 +2097,26 @@ vollständig enthält.
|
||||
Zeilen), damit ein KI-Client für eine dort gelistete Gruppe nicht zusätzlich `get_groups`
|
||||
aufrufen muss, nur um den Fehlzeitenabgleich für sie auszulösen.
|
||||
|
||||
**Nachtrag (Nutzer-Feedback, 2026-09-13):** `UntisHubJobState` (die Fälligkeits-Zeitstempel
|
||||
hinter dem Hub) lief bislang außerhalb des Sync — jedes Gerät führte seine eigene Buchhaltung.
|
||||
In der Praxis zeigte das auf allen Instanzen dieselben offenen Punkte, was dem Zweck des Hubs
|
||||
widerspricht (möglichst wenig WebUntis-Traffic: ein bereits auf Gerät A erledigter Abgleich soll
|
||||
auf Gerät B nicht erneut als fällig erscheinen und zu einem zweiten, unnötigen Abruf verleiten).
|
||||
Anders als die drei bewusst unsynchronisierten Report-Caches (`UntisAbsenceCacheRepository` &
|
||||
Co., siehe oben) enthält `UntisHubJobState` keine WebUntis-Rohdaten, sondern nur Zeitstempel +
|
||||
Kurztext — unkritisch für Sync.
|
||||
- `UntisHubJobStateRepository.Save` ruft jetzt `db.OnChange` wie die übrigen ~28 synchronisierten
|
||||
Repositories auf; `EventApplier` bekommt dafür einen zusätzlichen `Simple<UntisHubJobState>`-
|
||||
Eintrag. Kein `Delete` nötig (Interface kennt keins) — Zeilen werden nur überschrieben.
|
||||
- Geräte-Pairing (`SnapshotService`) war bereits unberührt, da es die komplette DB-Datei kopiert.
|
||||
- Race-Härtung: `UntisHubService.RecordRun` legt bei einem (Kind, GroupId), das lokal noch nie
|
||||
lief, eine neue `Id` an; laufen zwei Geräte offline denselben, noch nie ausgeführten Job
|
||||
unabhängig voneinander, entstehen dadurch kurzzeitig zwei Datensätze für dasselbe Paar (kein
|
||||
Unique-Index darauf). `UntisHubService.BuildRows` wählt deshalb jetzt den Datensatz mit dem
|
||||
jüngsten `LastRunAt` statt eines beliebigen — der verwaiste zweite Datensatz bleibt harmlos in
|
||||
der DB stehen (gleiches akzeptiertes v1-Verhalten wie bei anderen Entitäten ohne serverseitige
|
||||
Merge-Logik, siehe TODO 10.3).
|
||||
|
||||
### 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.
|
||||
|
||||
Reference in New Issue
Block a user