# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project LehrerApp is a German-language desktop application for teachers (Notenverwaltung, Anwesenheit, Hausaufgaben, Klausuren, Unterrichtsplanung, Mitarbeitsbewertung). The primary product is the Avalonia desktop client backed by a local LiteDB file; an optional ASP.NET Core API enables multi-device sync via an encrypted event queue. The living roadmap and feature spec is `TODO.md` — check it before starting new work and update it (with an implementation note, following the existing style) when a checklist item is finished. `docs/Datenmodell.md` documents the meaning of domain fields where naming alone is ambiguous (e.g. why `GroupMembership` has no `SchoolYear` of its own). ## Commands ```bash # Build everything dotnet build LehrerApp.sln # Run all tests dotnet test LehrerApp.sln # Run one test project dotnet test LehrerApp.Tests/LehrerApp.Tests.csproj # Run a single test (xUnit fully-qualified name filter) dotnet test --filter "FullyQualifiedName~GradingServiceTests.RoundToGrade_Kaufmaennisch" # Run the desktop app dotnet run --project LehrerApp.Desktop # Run the sync API locally (needs JWT_SECRET — copy .env.example to .env first) dotnet run --project LehrerApp.Api ``` The API can also run via `docker/docker-compose.yml` (reads `JWT_SECRET` from the environment). ## Architecture ### Project layout - **LehrerApp.Core** — domain models (`Models/`), repository interfaces (`Interfaces/IRepositories.cs`), and framework-free business services (`Services/`): `GradingService` (rounding, grading-key validation, report-grade calculation), `SchoolYearService`, `AppLogger`, `BackupService`, `AppLockService`. No dependency on LiteDB or Avalonia — this is what makes these services testable without a UI or a real database. - **LehrerApp.Data** — LiteDB implementation. `LiteDbContext` owns the single `LiteDatabase` connection and all collection accessors; `Repositories/AllRepositories.cs` implements every `I*Repository` interface from Core. `DatabaseEncryptionService` handles password-protecting the DB file. - **LehrerApp.Desktop** — the Avalonia MVVM client. `AppBootstrapper.BuildServices()` is the single DI composition root (repositories and `LiteDbContext` are singletons — one LiteDB file per user/process). `ViewModels/` and `Views/` are split into `Groups/`, `Students/`, `Settings/` (mirrors the domain, not a strict 1:1 with models). - **LehrerApp.Sync** — sync client library used by Desktop: `EventQueue` (local outbox), `ConflictResolver` (last-write-wins with timestamp tie-breaking), `SyncEngine` (push/pull orchestration, timer-driven), `SnapshotService`, `Crypto/SyncCrypto` (AES-256-GCM payload encryption — desktop events are encrypted at rest and in transit; Companion/WebApp events are plaintext, see `PlainSyncEvent` vs `SyncEvent`). - **LehrerApp.WebUntis** — direkter, serverunabhängiger WebUntis-Client für den Desktop. Hält die persönliche JSON-RPC-Sitzung lokal und parst den Schülerreport lokal; WebUntis-Zugangsdaten und personenbezogene Antworten dürfen nicht über `LehrerApp.Api` geleitet werden. - **LehrerApp.Api** — minimal ASP.NET Core server: JWT auth, an append-only `EventStore` plus `SnapshotStore`/`ReadableSnapshotStore` per device, mapped in `Endpoints/Endpoints.cs`. Sync is optional — Desktop only registers `SyncEngine`/`SnapshotService` in DI when a server URL is configured (`AppBootstrapper.LoadServerUrl`). - Each library has a matching `*.Tests` project (`LehrerApp.Tests` → Core, `LehrerApp.Data.Tests` → Data, `LehrerApp.Desktop.Tests` → Desktop, `LehrerApp.Sync.Tests` → Sync), all xUnit. `LehrerApp.WebUntis.Tests` covers the direct WebUntis client and report parser. ### MVVM conventions (Desktop) - ViewModels use CommunityToolkit.Mvvm source generators: `[ObservableProperty] private T _foo;` and `[RelayCommand]`. A generated `partial void On{Foo}Changed(...)` is emitted for every `[ObservableProperty]` — do not name a property/callback so it collides with that generated name (e.g. a field named `OnFooChanged` next to `[ObservableProperty] Foo` fails with `CS0102`). - Field-level validation: each invalid input gets its own `{Field}Error` string property (not one collective `ValidationMessage`), rendered directly under that field in XAML via `IsVisible="{Binding {Field}Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"`. `Save()` clears all `*Error` properties up front, accumulates failures into a local `valid` flag instead of returning on the first one, then does `if (!valid) return;` — so multiple errors can show at once. A message that only makes sense as one combined statement about several fields (e.g. a weighting scheme's three percentages summing to 100) legitimately stays a single message near those fields; that's a judgement call, not a bug. - Enum-backed UI: don't bind a `ComboBox` directly to a raw enum (Avalonia falls back to `ToString()`, i.e. English member names). Expose a `string[] Options` of German labels plus a `{Enum}Name` wrapper string property that the ComboBox binds to, converting to/from the actual enum property — see `NiveauDisplay`/`GradeCategoryDisplay` for the established pattern. - Dialogs are `Window` subclasses shown via `await dialog.ShowDialog(owner)`, with `Close(true)`/`Close(false)` on confirm/cancel and the result read from a `Result` property on the dialog's ViewModel. ### Avalonia XAML gotchas - Local attribute values always win over `Style` selectors for the same property. Conditional `Classes`-based styling only works if the base value also comes from a `Style` selector, not a local attribute on the element. - `[SomeAttached.Property] = value` is not valid inside a C# object initializer for an attached property — use `ToolTip.SetTip(control, value)` as a separate statement. - `x:DataType` on a `Window`/`UserControl` enables compiled bindings, which validate binding paths at build time — a clean build is meaningful evidence that new bindings are wired correctly, not just that the XML parses. ### LiteDB details - Only a property literally named `Id` is auto-recognized as the primary key. A model with a differently-named identity property (e.g. `EventId`) needs an explicit `[BsonId]`, or LiteDB silently assigns an unrelated `_id` and lookups/deletes by that property quietly do nothing. - `DateTime` round-trips through LiteDB BSON with `Kind` converted `Utc → Local` and the ticks shifted to preserve the same instant. Since `DateTime` comparison operators compare raw ticks and ignore `Kind`, comparing a round-tripped value against a freshly-created one is unsafe outside UTC+0 — always `.ToUniversalTime()` both sides first (see `ConflictResolver`). LiteDB's `LiteDatabase.Rebuild(new RebuildOptions { Password = ... })` for in-place re-encryption is broken in the pinned LiteDB version (5.0.21) — it throws even against an unencrypted source file. `DatabaseEncryptionService` instead copies every collection into a fresh database opened with the target password. - `LiteDbContext` tracks a schema version in a `meta` collection (`RunVersionedMigrations`) so migration steps run once, not on every startup. Add new migrations as another `if (version < N) { ...; version = N; }` block rather than making old steps re-check their own idempotency forever. - `new LiteDbContext(stream)` opens an in-memory database from a `MemoryStream` — used pervasively in tests for a fast, disk-free `LiteDbContext`. ### Central Package Management `Directory.Packages.props` pins all NuGet versions; individual `.csproj` files must reference packages without a `Version` attribute (``).