feat(frontend): gemeinsam am Server-Dokument arbeiten (?live=, Schritt 6)

Der Editor fuehrt ein Dokument des Backends: laden, nach 1,5 s Ruhe das Diff
schicken, ueber einen offenen Abruf fremde Aenderungen einspielen — ohne
Neuladen, mit mitwandernder Schreibmarke. Dazu CORS im Backend; ohne das
blockiert der Browser jeden Aufruf.

Zwei Fehler hat erst der Live-Test gegen das laufende Backend gefunden, beide
an der Naht zwischen Modul und Verdrahtung (D54-Nachtrag 3):

Der Konflikt entstand nie. Mit laufendem Feed zieht die Schattenkopie staendig
nach, die eigene Basis ist also nie veraltet — der Server haette nie 409
geantwortet, und die fremde Zeile waere stillschweigend ueberschrieben worden.
Der Client prueft die Ueberschneidung jetzt selbst gegen den ungesendeten
Text; den kennt der Server nicht.

Die Nummer begann nach jedem Neuladen wieder bei 1, waehrend die Kennung
blieb — der Server hielt die erste echte Aenderung fuer eine Wiederholung und
tat nichts. Beides liegt jetzt im sessionStorage: je Tab, ueberlebt Neuladen.
Je Tab ist zugleich die richtige Aussage, zwei Tabs sind zwei Schreiber.

Nachgemessen im Browser: fremde Aenderung erscheint ohne Neuladen, Getipptes
erreicht den Server, beide Konflikt-Knoepfe tun was sie sagen, und die
Schreibmarke steht nach zwei fremd eingefuegten Zeilen darueber unveraendert
bei Zeile+2, Spalte 8. 518 Frontend-Tests, 135 im Backend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-26 17:48:17 +02:00
co-authored by Claude Opus 5
parent 5881be1a2c
commit 08965157a1
12 changed files with 666 additions and 7 deletions
+10 -6
View File
@@ -1,9 +1,10 @@
# Live-Editing über HTTP (Variante „Simpel")
Status: **Konzept entschieden** (D76), **Schritte 15 der Umsetzungsreihenfolge
gebaut** (Zeilen-Diff, zweistufige Historie, `PATCH /content`, Änderungsfeed,
Master-Passwort); der Client steht aus, ebenso das Umbenennen per
`PATCH /title` und damit das Ereignis `RENAMED`. Die offenen
Status: **umgesetzt** — alle sechs Schritte der Umsetzungsreihenfolge stehen
(Zeilen-Diff, zweistufige Historie, `PATCH /content`, Änderungsfeed,
Master-Passwort, Client). Offen bleiben das Umbenennen per `PATCH /title`
(und damit das Ereignis `RENAMED`), ein Eingabefeld für den Anzeigenamen und
die Präsenz-Anzeige. Die offenen
Punkte des ersten Entwurfs sind beantwortet; die Begründungen stehen in
`docs/DECISIONS.md` unter D76 und werden hier nicht wiederholt, sondern nur
verwiesen. Was beim Bauen zusätzlich zu entscheiden war, steht dort in
@@ -24,7 +25,9 @@ Nachtrag 4.
deshalb ausschließlich auf **physischen Zeilen** und braucht keine IDs.
- Bei echtem Gleichzeitig-Konflikt: Update ablehnen, **der Client
entscheidet** (rebase, neu laden, verwerfen). Das Dokument darf nie
kaputtgehen.
kaputtgehen. **Der Client erkennt den Konflikt zusätzlich selbst**, wenn eine
fremde Änderung die Zeilen trifft, an denen gerade getippt wird — der Server
kann das nicht sehen, er kennt den ungesendeten Text nicht (D76-Nachtrag 7).
### Zugriff und Identität
@@ -433,7 +436,8 @@ verworfen.
4. ~~`GET /changes` mit Long Polling, Volltext-Fall und Ereignistypen
(Spec + Cucumber)~~ — gebaut, `ChangeNotifier` + `LiveEditingService`
5. ~~Master-Passwort für `GET /documents` (Spring Security)~~ — gebaut
6. Client-Anpassung (Feed-Schleife, lokales Anwenden, Konfliktdialog)
6. ~~Client-Anpassung (Feed-Schleife, lokales Anwenden, Konfliktdialog)~~ —
gebaut, `frontend/src/live.js` + `app.js`; Befunde in D76-Nachtrag 7
**Vor Schritt 4** steht die Vermessung der Zielumgebung (siehe „Betrieb") —
sie bestimmt den `wait`-Wert und im Extremfall, ob Long Polling dort