feat(live): vier Netze gegen stillen Verlust — Zwei-Fenster-Modal, Wachhund, Rettungs-Sicherungen, lokale Stände für Server-Dokumente (D89)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
42d37daf63
commit
408e23f25b
@@ -19,6 +19,11 @@ reverse.
|
||||
|
||||
## 2026-08-27
|
||||
|
||||
- A second Werkbaum window now raises a modal dialog in both windows — it closes by itself as soon as the other window is gone
|
||||
- A standing warning appears when changes have not reached the server for 30 seconds — whatever the reason: an open conflict bar, a dead session, the network
|
||||
- Unsent text is never discarded silently any more: it is rescued into the local backups before a reload, a conflict decision or a server full-text replaces it, and closing with unsent changes asks first
|
||||
- Local backups are collected for shared documents again and appear in the clock menu below the server milestones
|
||||
- Fix: a failed first load of a shared document left a half-initialised session behind — typing went nowhere, silently
|
||||
- A `pull-doc` script fetches a shared document from the backend into a file — with `--git-commit` it also commits it, dated, and only when something changed, so a cron gives the plan a git history
|
||||
- A benefit rating per node recorded as planned: a 0-9 digit inside the size token (`M/9`), and a third path mode that picks alternatives by benefit over cost — spelled out in SPEC §11 before it is built
|
||||
- A people bar below the diagram shows each `@person` with their share of the open work on the cheapest path — plus an entry for what is assigned to nobody
|
||||
|
||||
@@ -7597,3 +7597,112 @@ dann `--git-commit` → Commit, Worktree sauber; sauber und gleich →
|
||||
Haupttext gelten fort; neu geprüft außerdem: flaglos außerhalb jedes
|
||||
Worktrees schreibt anstandslos, `--git-commit` dort lehnt mit klarer
|
||||
Meldung ab.
|
||||
|
||||
## D89 — Zwei Stunden verlorene Arbeit: vier Netze gegen den stillen Live-Verlust
|
||||
Der Vorfall (2026-08-27 vormittags, gemeldet vom Nutzer): Zwei Stunden Arbeit
|
||||
an einem geteilten Dokument (`?live=`, PWA) waren weg — die Server-Historie
|
||||
endet 08:55:58, danach kam **kein einziger Patch** mehr an, obwohl bis ~11 Uhr
|
||||
getippt wurde; beim Zurückkommen nach einer Pause stand wieder der Stand von
|
||||
vor 9 Uhr. Alle Verstecke wurden geprüft und waren leer: Die Server-Historie
|
||||
(Meilensteine enden bei v22), die lokalen 10-Minuten-Stände (für
|
||||
Server-Dokumente seit D86, deployt am selben Morgen 08:35, **nicht mehr
|
||||
gesammelt** — die Sitzung lief bereits auf dem neuen Stand), und
|
||||
Dokument-Schlüssel wie Spiegel im localStorage (beim Wiederöffnen mit dem
|
||||
Server-Stand überschrieben). Die Arbeit war nicht zu retten.
|
||||
|
||||
**Der Auslöser ist nicht bewiesen — die Bauform des Schadens schon.** Vorher
|
||||
war ein zweites Werkbaum-Fenster offen (geschlossen, PWA neu gestartet), am
|
||||
selben Morgen liefen Backend- und Frontend-Deploys; welcher Kandidat die
|
||||
Sitzung stumm stellte, gibt der Client nach dem Neuladen nicht mehr her. Die
|
||||
Code-Durchsicht fand aber **mehrere** Zustände, in denen der Editor Tippen
|
||||
annimmt, ohne zu senden und ohne laut zu werden — und genau das ist der
|
||||
eigentliche Fehler, unabhängig vom Auslöser:
|
||||
|
||||
- Ein **offenes Konflikt-Band** lässt Senden und Feed ruhen (gewollt, D76-N7) —
|
||||
aber man kann darunter stundenlang weitertippen, alles bleibt nur lokal.
|
||||
- **`stopLive()` auf ein Feed-404** beendete die Sitzung wortlos.
|
||||
- **`startLive()` ließ bei gescheitertem Erstabruf `liveState` halb
|
||||
initialisiert stehen** (Version 0, leere Schattenkopie, kein Feed) — jede
|
||||
weitere Eingabe eine stumme Sackgasse.
|
||||
- Und **D86 hatte das letzte Netz entfernt**: „Fremde übernehmen" und jedes
|
||||
Neuladen verwarfen ungesendeten Text ersatzlos; der Code-Kommentar „er steht
|
||||
in den früheren Ständen (D54)" stimmte seit D86 nicht mehr.
|
||||
|
||||
**Vier Netze, gegen die Bauform statt gegen den einen Auslöser:**
|
||||
|
||||
**1. Zwei Fenster ⇒ modaler Dialog, in beiden, bis eines zu ist**
|
||||
(Nutzer-Vorgabe; verschärft D84, dessen zeilenlose Warnung als Protokoll
|
||||
bleibt). Herzschlag über einen BroadcastChannel: jedes Fenster meldet sich
|
||||
sekündlich, ein `hello` wird sofort beantwortet (Erkennung < 1 s), `bye` bei
|
||||
pagehide schließt den Dialog im anderen Fenster **sofort und ohne Klick** —
|
||||
die zweite Nutzer-Vorgabe („wenn das zweite Fenster geschlossen wird, muss
|
||||
die App wieder funktionieren"). Zwei Messbefunde beim Bauen, beide erst im
|
||||
Browser sichtbar: **(a)** Der Totes-Fenster-Timeout darf nicht bei wenigen
|
||||
Sekunden liegen — Chrome drosselt die Timer verborgener Fenster nach fünf
|
||||
Minuten auf **einen Tick je Minute** (dieselbe Umgebungsgrenze wie in D79
|
||||
gemessen); mit 3 s Timeout flackerte der Dialog im sichtbaren Fenster. Jetzt
|
||||
75 s — er fängt nur hart gestorbene Fenster, das saubere Schließen meldet
|
||||
sich selbst ab. **(b)** Die Notluke „Trotzdem fortfahren" gilt **je fremder
|
||||
Fenster-Id**, nicht je Sitzung: Die erste Fassung setzte ein Flag zurück,
|
||||
sobald eine Herzschlag-Lücke beobachtet wurde — ein gedrosseltes Fenster
|
||||
verpasst die Lücke, und ein NEUES zweites Fenster bekam nie wieder einen
|
||||
Dialog. Identität statt Timing.
|
||||
|
||||
**2. Lokale Sicherungen wieder auch für Server-Dokumente** (kehrt D86
|
||||
teilweise um — dieser Vorfall ist der Beweis, dass der Rückbau zu weit ging).
|
||||
Der 10-Minuten-Takt sammelt wieder; die Kamera bleibt Server-Meilenstein
|
||||
(D86). Sichtbar im Uhr-Menü als eigener Abschnitt **„Lokale Sicherungen
|
||||
(dieses Fenster)"** unter den Server-Meilensteinen — beschriftet, weil sie
|
||||
fremde Änderungen enthalten können und nur dieses Fenster sie kennt (die
|
||||
D86-Einwände, beantwortet statt ignoriert). Auch wenn der Server nicht
|
||||
antwortet, zeigt das Menü sie — gerade dann zählen sie.
|
||||
|
||||
**3. Nichts verwirft ungesendeten Text stumm.** `rescueSnapshot()` legt ihn
|
||||
in die lokalen Sicherungen, **bevor** er weicht: in `adoptLive()` (die
|
||||
Stelle, an der der Vormittag verloren ging — jedes Wiederöffnen, Neuladen
|
||||
und jeder Rollback läuft hier durch), in „Fremde übernehmen" und im
|
||||
Volltext-Ersatz des Feeds; dedupliziert gegen den letzten Stand, gedeckelt
|
||||
wie alle Stände (D54). Dazu `beforeunload` (Nachfrage des Browsers bei
|
||||
Ungesendetem) und als Gürtel zum Hosenträger eine Rettungs-Sicherung bei
|
||||
`pagehide` — auch wer die Nachfrage wegklickt, verliert nichts mehr.
|
||||
|
||||
**4. Ein Wachhund macht jede stumme Blockade laut.** Alle 5 s: Weicht der
|
||||
Editor länger als 30 s von der Schattenkopie ab, steht die zeilenlose
|
||||
Warnung `liveUnsent` („seit {min} Minuten NICHT auf dem Server … nur in
|
||||
diesem Fenster"), **egal aus welchem Grund** — offenes Band, tote Sitzung,
|
||||
Netz; sie räumt sich beim nächsten gelungenen Abgleich selbst weg. Ist das
|
||||
aktive Dokument ein `live:`-Dokument ohne Sitzung, steht `liveEnded` — damit
|
||||
ist auch das Feed-404-stopLive nicht mehr wortlos, ohne dass die
|
||||
Abbruchstelle selbst Warnungen kennen muss. `startLive()` räumt bei
|
||||
gescheitertem Erstabruf `liveState` jetzt ganz weg (der Wachhund meldet die
|
||||
Lage), und ein gelungener Push räumt eine liegengebliebene
|
||||
`liveLoad`-Warnung mit ab — „nicht geladen" neben funktionierendem Senden
|
||||
wäre eine Lüge.
|
||||
|
||||
**Nachgemessen** im Browser gegen ein lokal laufendes Backend, das
|
||||
Vormittags-Szenario nachgestellt: Backend getötet, getippt, Backend neu,
|
||||
Seite neu geladen — der Editor zeigt den Server-Stand, aber der ungesendete
|
||||
Text liegt in den lokalen Sicherungen (genau **ein** Eintrag trotz doppelten
|
||||
Netzes aus pagehide und adoptLive), und das Uhr-Menü zeigt ihn unter den
|
||||
zwei Server-Meilensteinen. Wachhund: 40 s nach dem Tippen gegen ein totes
|
||||
Backend steht `liveUnsent` neben dem Push-Fehler; ein Tastendruck nach dem
|
||||
Neustart, und beide sind weg (Server auf v3, alles angekommen). Toter
|
||||
Erstabruf: `liveEnded` nach < 10 s, der Editor behält den lokalen Text.
|
||||
Modal: zweites Fenster ⇒ Dialog in beiden < 2 s; Notluke wirkt nur im
|
||||
eigenen Fenster; Schließen des zweiten ⇒ Dialog verschwindet von selbst;
|
||||
ein NEUES zweites Fenster nach früherer Notluke ⇒ Dialog wieder da. 538
|
||||
Tests unverändert grün; die Netze sind DOM/BroadcastChannel-Verdrahtung und
|
||||
damit Browser-geprüft statt unit-getestet (dieselbe Grenze wie D41/D54-N3).
|
||||
|
||||
**Werkzeuggrenze, wieder einschlägig:** Der Automations-Tab meldet sich
|
||||
dauerhaft als verborgen — Feed und Sichtbarkeits-Pfade liefen nur mit
|
||||
gestellter Sichtbarkeit (D76-N7), und die Intensiv-Drosselung verborgener
|
||||
Tabs hätte den ersten Modal-Test beinahe als „geht doch" durchgewinkt: Der
|
||||
Flacker- und der Kleb-Befund oben stammen genau daher.
|
||||
|
||||
**Was offen bleibt:** der Auslöser des 08:56-Abrisses. Kandidaten (offenes
|
||||
Band, Feed-404, halb initialisierte Sitzung, Zwei-Fenster-Überschreiben)
|
||||
sind jetzt alle laut oder abgestellt; tritt es erneut auf, benennt die
|
||||
Warnung den Zustand, und die Sicherungen halten den Text. Unabhängig davon
|
||||
kann `tools/pull-doc --git-commit` (D88) per Cron eine Git-Historie des
|
||||
Server-Dokuments führen — ein Netz außerhalb des Browsers.
|
||||
|
||||
@@ -844,6 +844,25 @@ anderen, ohne neu zu laden.
|
||||
Wähler auswählt, arbeitet darin wieder gemeinsam; wer es verlässt, hört auf,
|
||||
mitzuschreiben. Siehe D80.
|
||||
|
||||
- **Ungesendetes bleibt nie stumm.** Erreichen Änderungen den Server länger
|
||||
als ~30 Sekunden nicht — offenes Konflikt-Band, tote Sitzung, Netz —, steht
|
||||
eine Warnung, bis der Abgleich wieder gelingt; eine beendete Sitzung meldet
|
||||
sich ebenso. Beim Schließen oder Neuladen mit ungesendeten Änderungen fragt
|
||||
der Browser nach. Und **bevor** irgendetwas ungesendeten Text überschreibt
|
||||
(Neuladen des Dokuments, *Fremde übernehmen*, ein Volltext vom Server),
|
||||
wandert er in die **lokalen Sicherungen**: Für Server-Dokumente sammelt der
|
||||
10-Minuten-Takt sie wieder, und das Uhr-Menü zeigt sie als eigenen Abschnitt
|
||||
unter den Server-Meilensteinen. Siehe D89.
|
||||
|
||||
**Ein zweites Werkbaum-Fenster desselben Browsers** — Tab, Fenster oder
|
||||
PWA — bekommt einen **modalen Dialog**, in beiden Fenstern, bis eines
|
||||
geschlossen ist: Beide schreiben in dieselbe Dokument-Ablage, der zuletzt
|
||||
speichernde überschreibt den anderen. Erkannt per Herzschlag
|
||||
(BroadcastChannel); der Dialog schließt sich **von selbst**, sobald das
|
||||
andere Fenster zu ist — es ist nichts zu bestätigen. „Trotzdem fortfahren"
|
||||
ist die Notluke und gilt je Fenster und Vorfall. Siehe D89 (und D84 für die
|
||||
zeilenlose Warnung, die daneben bestehen bleibt).
|
||||
|
||||
Siehe D76 (Protokoll und Begründung) und
|
||||
`backend/docs/live-editing-proposal.md`.
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
- [?] #col.git.diff: Diff between two versions (S)
|
||||
- [?] #col.git.blame: Who changed this line (S)
|
||||
- [?] #col.git.branch: A branch per scenario (S)
|
||||
- [/] #col.live: Live editing, several people at once (XL) :#be.docs
|
||||
- [/] #col.live: Live editing, several people at once (XXL) :#be.docs
|
||||
- [^] #col.live.http: Transport over plain HTTP (M) :#be.docs %% no websocket needed
|
||||
- [^] #col.live.patch: Submit a change as a line diff (S)
|
||||
- [^] #col.live.feed: Long polling holds the other direction open (S)
|
||||
@@ -163,6 +163,7 @@
|
||||
- [^] #col.live.rename: Renaming reaches everyone (S) %% PATCH /title + RENAMED in the feed, see D85
|
||||
- [^] #col.live.history: Earlier states come from the server (M) %% milestones in the clock menu, rollback for everyone, see D86
|
||||
- [^] #col.live.name: A self-chosen display name (XS) %% fills "changed by", a claim, not a proof
|
||||
- [x] #col.live.safe: Unsent work is loud, rescued and never silently discarded (M) %% modal for a second window, watchdog, local backups — see D89
|
||||
- [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough
|
||||
- [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
|
||||
- [?] #mmd: Mermaid plugin (XL)
|
||||
@@ -983,6 +984,12 @@
|
||||
Asked once when joining or sharing, kept in the browser, sent with every
|
||||
patch. Without accounts it is a claim, not a proof.
|
||||
|
||||
#col.live.safe
|
||||
Born from a real loss: two hours of typing that never reached the server
|
||||
and vanished on reload. A second window raises a modal dialog until one is
|
||||
closed; a watchdog warns after 30 seconds of unsent changes; and unsent
|
||||
text is rescued into the local backups before anything overwrites it.
|
||||
|
||||
#col.live.cursors
|
||||
Seeing where the others are. A by-product of the merge layer rather than a
|
||||
feature in its own right.
|
||||
|
||||
Reference in New Issue
Block a user