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
+24
View File
@@ -1544,3 +1544,27 @@
.node:not(.folded) .fold{display:none!important}
@page{margin:12mm}
}
/* ---------- Konflikt beim gemeinsamen Bearbeiten (D76) ----------
Band oben, wie der Update-Hinweis: Es unterbricht nicht die Arbeit, steht
aber über allem, bis jemand entscheidet. In `--warn`, nicht in Petrol — es
ist keine Einladung, sondern eine Sache, die geklärt werden muss. */
.live-conflict{
position: fixed; inset: 0 0 auto 0; z-index: 1200;
display: flex; align-items: center; justify-content: space-between;
gap: 16px; padding: 10px 16px;
background: var(--warn, #B45309); color: #fff;
font: 500 14px/1.4 'IBM Plex Sans', system-ui, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
.live-conflict-actions{ display: flex; gap: 8px; flex: 0 0 auto; }
.live-conflict button{
font: 500 13px/1 'IBM Plex Sans', system-ui, sans-serif;
padding: 6px 10px; border-radius: 4px; cursor: pointer;
border: 1px solid #fff; background: transparent; color: #fff;
}
/* „Eigene durchsetzen" ist die Antwort, die nichts von der eigenen Arbeit
kostet - sie steht deshalb gefüllt da, ohne die andere zu verstecken. */
.live-conflict button.mine{ background: #fff; color: var(--warn, #B45309); border-color: #fff; }
.live-conflict button:focus-visible{ outline: 2px solid #fff; outline-offset: 2px; }
@media print{ .live-conflict{ display: none; } }