claude sync: 2026-07-02 10:59

This commit is contained in:
marcus.hinz
2026-07-02 10:59:36 +02:00
parent 3bb3beaa65
commit 41e74e9946
28 changed files with 1088943 additions and 83 deletions
+1
View File
@@ -28,3 +28,4 @@
- [Projekt: Bestätigen-Bug (FSA-Loop)](project_bestaetigen_bug.md) — Edge/Windows beim Kollegen; morgen Policy + Konsolen-reason prüfen
- [Projekt: Prod-Ist-Stand](project_prod_ist_stand.md) — Prod (ocp-01) LIVE: backend 0.38.4 Running, Secrets inkl. SAP gemountet; nur PR→main mergen offen; Quota 3CPU knapp
- [SAP-Config](sap-config.md) — echter Zugriff via Basic Auth (sy08804394), Key-Predicate-URL über Proxy; Env→SealedSecret/Overlay-Mapping im ops-Repo; Adapter-Patch offen
- [Projekt: Optimistische Sperre](project_optimistic_locking.md) — VERSION_CONFLICT (409) in persistCustomer gegen Last-Write-Wins; greift nur bei version!=null (Re-Send bleibt idempotent); lastUpdateTs ist server-autoritativ
@@ -0,0 +1,25 @@
---
name: project-optimistic-locking
description: "Server-seitige optimistische Sperre für Customer-Writes (VERSION_CONFLICT) — Fix gegen Last-Write-Wins bei zwei Sales am selben Kunden, eingebaut 2026-06-29."
metadata:
node_type: memory
type: project
originSessionId: 31351046-6fed-46e2-b117-bcc9d8064774
---
Vor Go-live (Donnerstag 2026-07-02) das Concurrency-Verhalten der Customer-Writes gefixt.
**Ausgangslage (charakterisiert):** `@VersionColumn` war im Schreibpfad wirkungslos — `persistCustomer` machte `repo.merge(existing, scalars)`, das die `version` aus dem Client-Payload übernahm. Folge: Last-Write-Wins, `version` lief sogar rückwärts (2→1), stiller Datenverlust bei parallelen Editoren. Jeder Sales hat prinzipiell alle Kunden des Landes → der Fall ist real.
**Fix (rein BE, kein FE-Eingriff):**
- `libs/shared/src/lib/errors/error-catalog.ts`: neuer Code `VERSION_CONFLICT` (HTTP 409).
- `libs/customer/customer-be/src/lib/customer-core.service.ts`: `enforceVersion()` in `persistCustomer` vor dem Merge — `incoming.version !== existing.version``DomainException('VERSION_CONFLICT', …, forcedState: existing)`. Danach zählt `@VersionColumn` wieder hoch.
- **Kernregel:** Sperre greift nur, wenn `incoming.version != null`. Ein nie synchronisierter Neuanlage-Stand (version undefined), der nach Crash erneut gesendet wird, ist ein idempotenter Re-Send — sonst wäre at-least-once / Crash-Recovery (S8b) kaputtgegangen. Ein bereits synchronisierter Datensatz trägt clientseitig immer eine version (reconcile schreibt den Server-Stand lokal zurück).
**Verhalten:** stale Edit → 409 → FE rollt über den vorhandenen `forcedState`-Mechanismus (wie SAP-Readonly) auf den Server-Stand zurück, Fehler sichtbar. Verliererseite verliert ihre Änderung (mit Meldung); ein Merge-Dialog wäre eine spätere fachliche Erweiterung.
**Wichtig für künftige Fixes:** `version` ist als „wer ist neuer"-Vergleichsbasis untauglich (client-steuerbar). `lastUpdateTs` ist server-autoritativ (`@BeforeUpdate``Date.now()`) — das ist die verlässliche Basis. Belegt in den Tests.
**Tests:** `customer-core.concurrency.spec.ts` (5, BE-direkt gegen In-Memory-SQLite) + `sync-loop.integration.spec.ts` S10 (End-to-End Multi-Sales). Gesamt 8 Projekte grün. Siehe [[project-sync-testing]] für den Test-Harness.
**Lokaler Testlauf:** node 22 via fnm nötig (`eval "$(/opt/homebrew/bin/fnm env)" && fnm use 22`), better-sqlite3 baut nicht gegen node 26. node_modules waren nicht installiert → `npm ci`. Repo: `~/projects/fahrzeugeinrichtung-plato-backend`.