CLAUDE.md mit Projektkonventionen für künftige Claude-Code-Sitzungen. Wiederkehrende UI-Muster konsolidiert: neue PageHeader-Control für Titel/Untertitel in den Listen- und Detailansichten, globale Styles für Dialog-Titel und Leerlisten-Hinweise statt inline wiederholter Werte. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
127 lines
7.4 KiB
Markdown
127 lines
7.4 KiB
Markdown
# 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.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.
|
|
|
|
### 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<bool>(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 (`<PackageReference Include="Foo" />`).
|