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
+28
View File
@@ -0,0 +1,28 @@
/* Werkbaum — Adressen für Dokumente von außen (headless, D31).
Hier steht nur reine Logik, damit sie testbar ist; das Holen selbst und der
ganze DOM-Kram bleiben in app.js. */
/* Pad-Adresse normalisieren (D31). Eingabe ist die Adresse aus der Browser-
Adresszeile — ohne Export-Pfad, den hängt Werkbaum selbst an. Abgeschnitten
werden ein versehentlich mitgegebener Export- oder /timeslider-Pfad, Query,
Fragment und Schrägstriche am Ende: derselbe Pad soll genau ein Dokument
ergeben, gleich in welcher Schreibweise der Link kam.
Verlangt wird `/p/<name>` am Ende (auch unter einem Unterpfad montiert) — das
ist die Prüfung, ob überhaupt eine Pad-Adresse vorliegt. Erlaubt sind nur
http(s), wie bei ?sourceUrl= (D23).
Rückgabe {pad, text} oder null. `pad` ist zugleich Name und Identität des
Dokuments — die **vollständige** URL, nicht der bloße Pad-Name: Pad-Namen sind
nur pro Instanz eindeutig, zwei Hosts mit je einem Pad `plan` wären sonst im
Wähler nicht zu unterscheiden. */
export function padUrls(raw, base){
let u;
try{ u = base ? new URL(raw, base) : new URL(raw); }catch(_){ return null; }
if(u.protocol !== 'http:' && u.protocol !== 'https:') return null;
const path = u.pathname.replace(/\/+$/, '').replace(/\/(export\/[^/]+|timeslider)$/, '');
if(!/\/p\/[^/]+$/.test(path)) return null;
u.pathname = path; u.search = ''; u.hash = '';
const pad = u.href;
return {pad, text: pad + '/export/txt'};
}