From 3310cab7bef732980312f83f8acb6c33cc964eaa Mon Sep 17 00:00:00 2001 From: mhoennig Date: Thu, 30 Jul 2026 12:04:22 +0200 Subject: [PATCH] =?UTF-8?q?frontend:=20`=3Fetherpad=3D`=20=E2=80=94=20Echt?= =?UTF-8?q?zeit-Zusammenarbeit=20=C3=BCber=20ein=20geliehenes=20Pad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `[^]`. --- README.de.md | 26 ++++ README.md | 26 ++++ docs/DECISIONS.md | 105 ++++++++++++++ docs/SPEC.md | 34 ++++- docs/examples/example-werkbaum.werkbaum | 6 +- frontend/CLAUDE.md | 20 +++ frontend/index.html | 3 + frontend/src/app.js | 183 ++++++++++++++++++++---- frontend/src/remote.js | 28 ++++ frontend/src/style.css | 9 ++ frontend/tests/remote.test.js | 69 +++++++++ 11 files changed, 478 insertions(+), 31 deletions(-) create mode 100644 frontend/src/remote.js create mode 100644 frontend/tests/remote.test.js diff --git a/README.de.md b/README.de.md index 92374e4..8e4aaa7 100644 --- a/README.de.md +++ b/README.de.md @@ -91,6 +91,32 @@ GitLab-Raw-Links tun das; ein beliebiger Webserver oft nicht. Scheitert das Laden, bleibt der bisherige Stand stehen und eine Warnung nennt die Ursache. Zugelassen sind nur `http`/`https`. +### Zusammen an einem Plan arbeiten (Etherpad) + +Für Zusammenarbeit in Echtzeit braucht Werkbaum kein eigenes Backend — es leiht +sich ein **Etherpad**. Angegeben wird die Pad-Adresse, wie sie im Browser steht +(ohne Export-Pfad, den hängt Werkbaum selbst an): + +``` +https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/mein-plan +``` + +Alle bearbeiten den Notationstext **im Pad**, jeder Betrachter sieht das Diagramm +mitwachsen (Abruf alle 2,5 s; nicht, während der Tab im Hintergrund liegt). Das +Zusammenführen gleichzeitiger Änderungen macht Etherpad — genau darum geht es. + +Weil das Pad die Schreibfläche ist, ist das Textfeld hier **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. + +Getestet gegen Etherpad: Der Klartext-Export sendet +`Access-Control-Allow-Origin: *`, und die Notation kommt **byte-identisch** +zurück — führende Leerzeichen, `-`/`+`/`|`, Statusboxen und `%%` überleben +Etherpads Speichermodell, das `-` wird nicht zur Aufzählung umgedeutet. + +**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist +für jeden lesbar, der die Adresse kennt. Siehe `docs/DECISIONS.md` D31. + ### Lokal ausführen Die Editor-Quelle liegt jetzt als ES-Module unter `frontend/src/`, gebündelt mit diff --git a/README.md b/README.md index 2efb37f..73d90b4 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,32 @@ http(s) regardless of extension or content type. do; an arbitrary web server often does not. If loading fails the previous content stays and a warning explains why. Only `http`/`https` are allowed. +### Working on one plan together (Etherpad) + +For real-time collaboration Werkbaum needs no backend of its own — it borrows an +**Etherpad**. Pass the pad address as it appears in your browser (no export path, +Werkbaum appends that itself): + +``` +https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/my-plan +``` + +Everyone edits the notation text **in the pad**, and every viewer watches the +diagram grow (fetched every 2.5 s; not while the tab is in the background). +Merging simultaneous edits is Etherpad's job — that is the whole point. + +Because the pad is the writing surface, the text area here is **read-only**; a +button in the editor title bar opens the pad in a new tab. Without that +protection, anything you typed would vanish on the next fetch. + +Verified against Etherpad: the plain-text export sends +`Access-Control-Allow-Origin: *`, and the notation comes back **byte-identical** — +leading spaces, `-`/`+`/`|`, status boxes and `%%` survive Etherpad's storage +model, and `-` is not turned into a bullet list. + +**Be aware:** your plan text then lives on third-party infrastructure, and a pad +is readable by anyone who knows its address. See `docs/DECISIONS.md` D31. + ### Running it locally The editor source now lives as ES modules under `frontend/src/`, bundled by diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1456a6f..1d004e5 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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//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 `