Codepflege: CLAUDE.md und UI-Konsolidierung (Kapitel 13.4)
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>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# 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" />`).
|
||||
Reference in New Issue
Block a user