frontend: ?etherpad= — Echtzeit-Zusammenarbeit über ein geliehenes Pad

Werkbaum hat kein Backend, und das eigentlich Schwere an gemeinsamem
Bearbeiten ist das Zusammenführen gleichzeitiger Änderungen — im Plan als
`[!] Merging simultaneous edits` markiert. Etherpad hat das gelöst. Also
geliehen statt nachgebaut: das Pad ist die Schreibfläche, Werkbaum die
Ansicht.

Nachgemessen an pad.hostsharing.net, bevor irgendwas gebaut wurde:
- der Klartext-Export sendet `Access-Control-Allow-Origin: *`,
- das kanonische Beispiel (SPEC §10) kommt byte-identisch zurück,
- der HTML-Export zeigt kein Listen-Markup — Etherpad deutet `-` nicht zur
  Aufzählung um und behält die führenden Leerzeichen.
Das war das Risiko, das die Idee hätte erledigen können. Nicht geprüft ist
das Tippen im Etherpad-Editor selbst (Tab-Einrückung, mögliches
Auto-Bullet); dafür braucht es einen echten Browser.

Eigener Parameter statt `?sourceUrl=`: Die URL, die ein Mensch in der Hand
hat, ist die Pad-URL — `/export/txt` hängt Werkbaum selbst an. Vor allem
aber lizenziert der eigene Parameter das andere Verhalten, sodass D23
unangetastet bleibt: `sourceUrl` heißt weiter „statische Datei, einmal pro
Laden geholt", bestehende Links bekommen kein Polling. Name und id sind die
vollständige Pad-URL (Pad-Namen sind nur pro Instanz eindeutig).

Textfeld schreibgeschützt, Knopf öffnet das Pad: ohne das verschwände
getippter Text beim nächsten Abruf. Schrift bleibt Tinte statt grau — hier
wird gelesen, der Plantext ist der Hauptinhalt.

Drei Riegel im Takt, jeder aus einem echten Fehler:
- `padBusy` — im Netzwerk-Mitschnitt stapelten sich die Abrufe, weil die
  Gegenseite langsamer war als der Takt; eine spät eintreffende alte
  Antwort hätte neueren Text überschrieben,
- Abbruch nach 10 s — sonst bliebe der Riegel bei hängender Gegenseite für
  immer zu,
- `visibilityState` + `visibilitychange` — nicht im Hintergrund abrufen,
  aber bei Rückkehr sofort.
Der Stabilitätstakt übernimmt erst beim zweiten gleichen Abruf, sonst sieht
man die anderen mitten im Tippen.

Die Normalisierung der Pad-Adresse liegt headless in `remote.js`, damit sie
testbar ist (23 neue Tests, 60 -> 83). Im Vorschau-Browser meldet
`visibilityState` „hidden" und HMR lädt bei jeder Quelländerung neu — ein
Reload sieht wie eine geglückte Übernahme aus; nachgewiesen wurde die
Übernahme deshalb mit einem Marker auf `window`, der einen Reload nicht
überlebt.

SPEC §9 zuerst, dann D31, dann Code. Der Plan bekommt den Knoten nach D30
mit `[x]`, nicht `[^]`.
This commit is contained in:
mhoennig
2026-07-30 12:04:22 +02:00
parent cfbbfab54b
commit 3310cab7be
11 changed files with 478 additions and 31 deletions
+105
View File
@@ -979,3 +979,108 @@ einem absichtlich ausgeweiteten `sed` in einem Wegwerf-Worktree.
bleibt eine bewusste Handlung. `deploy-prod.sh` warnt stattdessen, wenn HEAD
noch nicht auf `origin` liegt: der Footer-Versionslink zeigt sonst auf einen
Commit, den GitHub nicht kennt.
## D31 — Echtzeit-Zusammenarbeit über ein Etherpad, eigener Parameter `?etherpad=`
Werkbaum hat kein Backend (D13 ist Plan, nicht Bestand), und der Plan setzt
Zusammenarbeit als `[?] Live editing, several people at once (XL)` an — mit
`[!] Merging simultaneous edits (L)` als der eigentlichen Arbeit. Genau diese
Arbeit ist in Etherpad seit Jahren getan. Also wird sie geliehen statt
nachgebaut: **Das Pad ist die Schreibfläche, Werkbaum die Ansicht.**
**Das Fundament war schon da.** Etherpad liefert pro Pad einen Klartext-Export
(`/p/<pad>/export/txt`), und `?sourceUrl=` (D23) lädt jede Textdatei über
http(s), ohne Endung oder `Content-Type` zu prüfen (D24). Nachgemessen an
`pad.hostsharing.net`:
- `Access-Control-Allow-Origin: *` und `Content-Type: text/plain; charset=utf-8`
— die eigentliche Hürde aus D23 (CORS) fällt also weg;
- das kanonische Beispiel aus SPEC §10 kommt **byte-identisch** zurück
(führende Leerzeichen, `-`/`+`/`|`, Statusboxen, `%%`, UTF-8);
- der HTML-Export zeigt **kein** Listen-Markup: Etherpad speichert die
Einrückung als echte Leerzeichen und deutet das `-` nicht zur Aufzählung um.
Das war das Risiko, das die Idee hätte erledigen können.
(Gemessen wurde Import → Speicher → Export. Das *Tippen* im Etherpad-Editor —
Tab-Einrückung, mögliches Auto-Bullet — ist damit **nicht** geprüft; dafür
braucht es einen echten Browser. Vgl. die Lehre aus D25: synthetische Ereignisse
beweisen nur die eigene Logik.)
**Eigener Parameter statt `?sourceUrl=`.** Drei Gründe, der erste ist der
schwächste:
1. Die URL, die ein Mensch in der Hand hat, ist die **Pad**-URL — die aus der
Adresszeile. `/export/txt` ist eine Implementierungseinzelheit und gehört
nicht in die Schnittstelle; Werkbaum hängt sie selbst an.
2. Der Parameter **lizenziert anderes Verhalten**. `sourceUrl` heißt „statische
Datei, einmal pro Laden geholt" — das ist D23 wörtlich und bleibt
unangetastet. `etherpad` heißt „lebendes Pad", und daran hängt das
regelmäßige Abrufen. Ohne die Trennung müsste D23 seine Semantik ändern und
bestehende Links bekämen ungefragt Polling.
3. Die **Pad**-URL ist mehr wert als die Export-URL: nur mit ihr sind der
„im Pad bearbeiten"-Knopf und ein späteres Einbetten (siehe unten) ohne
weiteren Parameter erreichbar, und Identität/Name des Dokuments werden aus
ihr gebildet — derselbe Pad ergibt so genau **ein** Dokument, auch wenn
jemand versehentlich die Export-URL einträgt (sie wird normalisiert).
**Der Dokumentname ist die vollständige Pad-URL**, nicht der bloße Pad-Name.
Kurz wäre schöner (`mein-plan` statt 45 Zeichen in einer schmalen Titelzeile),
aber Pad-Namen sind nur **pro Instanz** eindeutig, nicht global: zwei Hosts mit
je einem Pad `plan` ergäben zwei gleichnamige Dokumente im Wähler, ohne
Möglichkeit sie zu unterscheiden. Damit gilt dieselbe Regel wie in D23 — und der
Ellipsen-Schnitt in der Titelzeile samt vollständiger URL im Tooltip ist dafür
schon eingerichtet.
Der Name `?etherpad=` statt des neutraleren `?pad=`: Das Anhängen von
`/export/txt` **ist** produktspezifisch. Das ehrlich zu benennen ist besser, als
Allgemeinheit vorzutäuschen, die beim nächsten Werkzeug (HedgeDoc, CryptPad —
andere Export-Pfade) doch einen Typ-Diskriminator bräuchte.
**Das Textfeld ist schreibgeschützt.** Ohne das verschwände getippter Text beim
nächsten Abruf — Datenverlust, und zwar überraschend, weil nichts darauf
hindeutet. Der Schutz ist zugleich die ehrliche Aussage: Werkbaum kann
gleichzeitige Änderungen nicht zusammenführen, das Pad kann es. Deshalb
erscheint in der Editor-Titelzeile ein Knopf, der das Pad im neuen Tab öffnet
(nur bei solchen Dokumenten sichtbar).
**Stabilitäts-Verzögerung statt rohem Polling.** Beim Abrufen sieht man die
anderen mitten im Tippen; eine halb geschriebene Zeile ist eine kaputte Zeile,
das Diagramm zuckt und die Warnungen flackern. Übernommen wird ein neuer Text
deshalb erst, wenn **zwei** Abrufe hintereinander denselben liefern. Das kostet
im Mittel einen Takt Verzögerung und macht die Ansicht ruhig. Bei `?sourceUrl=`
gibt es das Problem nicht (man lädt bewusst neu) — es ist also spezifisch für
diesen Parameter, wie das Polling selbst.
Getaktet wird mit 2,5 s und **nicht**, solange der Tab im Hintergrund liegt: Der
Export rendert bei jedem Abruf das ganze Pad, und die Gegenseite ist fremde
Infrastruktur. Ein Fehlschlag beim Abrufen bleibt **stumm** und lässt den
letzten Stand stehen (nur der erste Ladeversuch warnt) — sonst flutete ein
Netzaussetzer den Warnbereich.
**Verhältnis zu D20 („keine externen Requests").** Unverändert wie bei D23: Die
App lädt von sich aus nichts; der Request entsteht nur, weil der Nutzer eine
URL angibt, und geht nur an genau diesen Host (`credentials:'omit'`, nur
`http`/`https`). Neu und ausdrücklich zu benennen ist die andere Richtung:
**Der Plantext liegt jetzt auf fremder Infrastruktur**, und ein Pad ist für
jeden lesbar, der die Adresse kennt. Das ist bei einem Projektplan etwas
anderes als bei einer Schriftart. Es bleibt die Entscheidung dessen, der den
Link baut — Werkbaum legt von sich aus kein Pad an.
**Verworfene und aufgeschobene Alternativen:**
- **`?sourceUrl=` um Polling erweitern** — hätte bestehenden Links ungefragt
wiederholte Requests verpasst und D23 seine klare Semantik gekostet.
- **Das Pad als `<iframe>` einbetten** (aufgeschoben, nicht verworfen): Bringt
Cursor, Namen, Farben und Chat mit. Kostet aber **beide** Richtungen von D25
— in einem cross-origin-iframe gibt es keinen DOM-Zugriff, also kein
Alt+Klick → Zeile und keine Cursor-Zeile → Knoten. Bei 75 Knoten ist das die
Orientierung. Der Knopf „im Pad bearbeiten" ist die kleine Lösung desselben
Bedürfnisses; das Einbetten bleibt möglich, weil die Pad-URL vorliegt.
- **Echter Etherpad-Client** (socket.io + Easysync-Changesets): Rettet D25 und
erlaubt Schreiben aus Werkbaum heraus, kostet aber zwei **Laufzeit**-
Abhängigkeiten und damit „eine self-contained Datei ohne
Laufzeit-Abhängigkeiten" (D11/D19/D20) — und die Diff-Hälfte müsste man
selbst schreiben. Das ist der XL-Knoten aus dem Plan, nur mit fremdem
Protokoll statt fremdem CRDT. Wenn, dann zusammen mit dem eigenen Backend
(D13) und dann besser mit einem Text-CRDT, wie der Plan es vorsieht.
- **Etherpads HTTP-API** (`/api/1/getText?apikey=…`) — der API-Schlüssel ist ein
Administrationsschlüssel für **alle** Pads der Instanz und hat in einer
Client-Anwendung nichts zu suchen. Der Export-Endpunkt braucht ihn nicht.
+33 -1
View File
@@ -276,6 +276,37 @@ Scheitert das Laden (häufigster Fall: das Ziel sendet keinen
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
### Gemeinsam an einem Pad arbeiten (`?etherpad=`)
Für Zusammenarbeit in Echtzeit nimmt der Editor die Adresse eines
**Etherpad-Pads** — die Adresse, die im Browser steht, ohne Export-Pfad:
`…?etherpad=https://pad.example.org/p/mein-plan`. Werkbaum hängt den
Klartext-Export (`/export/txt`) selbst an; ein versehentlich mitgegebener
Export- oder `/timeslider`-Pfad wird abgeschnitten.
- **Das Pad ist die Schreibfläche, Werkbaum die Ansicht.** Alle bearbeiten den
Notationstext im Pad, jeder Betrachter sieht das Diagramm mitwachsen. Das
Zusammenführen gleichzeitiger Änderungen macht Etherpad; Werkbaum tut es
nicht.
- Deshalb ist das Textfeld für ein solches Dokument **schreibgeschützt** — ein
Knopf in der Editor-Titelzeile öffnet das Pad im neuen Tab. Ohne den Schutz
verschwände getippter Text beim nächsten Abruf.
- Geholt wird **regelmäßig** (Voreinstellung 2,5 s; nicht, solange der Tab im
Hintergrund liegt). Übernommen wird eine Änderung erst, wenn zwei Abrufe
hintereinander **denselben** neuen Text liefern — sonst sähe man die anderen
mitten im Tippen, und halb geschriebene Zeilen ließen Diagramm und Warnungen
flackern.
- **Name ist die vollständige Pad-URL** (nicht der bloße Pad-Name — zwei Pads
gleichen Namens auf verschiedenen Hosts wären sonst nicht zu unterscheiden),
wie bei `?sourceUrl=`. Identität und Name leiten sich von der **Pad**-Adresse
ab, nicht von der Export-Adresse — derselbe Pad ergibt damit genau ein
Dokument, gleich in welcher Schreibweise der Link kam.
- `?sourceUrl=` bleibt unverändert: statische Datei, einmal pro Laden geholt.
Der eigene Parameter trägt gerade den Unterschied.
- Fehler (CORS, 404, Netz) melden sich wie bei `?sourceUrl=`; ein einzelner
Aussetzer beim Abrufen bleibt stumm und lässt den letzten Stand stehen.
Siehe D31.
### Legende im Editor-Panel
Neben dem Textfeld steht eine aufklappbare **Legende** (Notation in Kurzform,
abschließend eine Bedienungs-Zeile). Sie ist **scrollbar**, wenn ihr Inhalt
@@ -287,7 +318,8 @@ Ausrichtungen getrennt erhalten. Die Legende belegt höchstens 85 % des Panels,
damit das Textfeld nie ganz verschwindet. Siehe D26.
### Was ist neu? (Dokumente von außen)
Bei Dokumenten, die von außen kommen (mitgeliefert oder per `?sourceUrl=`), wird
Bei Dokumenten, die von außen kommen (mitgeliefert, per `?sourceUrl=` oder
`?etherpad=`), wird
gezeigt, was sich seit dem letzten Besuch getan hat. **„Neu" heißt: neu in
Produktion** — ein Knoten trägt jetzt `[^]` und tat es in der zuletzt gesehenen
Fassung nicht. Solche Knoten bekommen einen **gelben Strahlenkranz** nach außen
+5 -1
View File
@@ -61,7 +61,11 @@
- [?] Accounts and permissions (L)
| [?] Single user, one token (S)
| [?] Log in with OIDC (L)
- [?] Working together on one plan (XL)
- [~] Working together on one plan (XL)
- [x] Watch a shared Etherpad — it merges, we render (S)
- [x] Load the pad's plain-text export (XS)
- [x] Keep up while the others type (XS)
+ [?] Embed the pad beside the diagram (M) %% costs the jump to the line
- [ ] Git as the shared store (L)
| [ ] A file in a repository, changed by pull request (S) %% works today, no code
| [?] The backend commits every change (L)