From 793561c87367c8a69ced5eb6dcc6c39c575b04f1 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Thu, 30 Jul 2026 12:59:14 +0200 Subject: [PATCH] =?UTF-8?q?frontend:=20Pad=20auf=20Knopfdruck=20holen=20st?= =?UTF-8?q?att=20zu=20takten=20=E2=80=94=20Etherpad=20drosselt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Hintergrund-Takt aus dem letzten Commit funktionierte in der Praxis nicht: im Netzwerk-Mitschnitt stapelten sich Anfragen und wurden abgebrochen ("cancelled"). Die Ursache ist nicht Langsamkeit, sondern ein Rate Limit — Etherpads `importExportRateLimiting` ist serienmäßig an und lässt 10 Abrufe je 90 s und IP zu; der Takt wollte 36. Jenseits davon antwortet die Gegenseite nicht mit 429, sondern hält die Verbindung ohne Antwort offen. Nachgemessen: 12:48:12 Abbruch nach 25 s (0 Bytes) Abbruch nach 25 s (0 Bytes) 12:50:23 HTTP 200 nach 0,436 s <- nach ~40 s Pause Zwei Minuten totgestellt, dann sofort in 0,4 s da. Gegen eine Drosselung kann ein Takt nicht gewinnen, er erzeugt sie. Also ein Knopf. Damit entfallen Stabilitätstakt, Sichtbarkeits-Wächter, Anti-Stapel-Riegel und Wiederanlauf. Der Knopf greift zudem gut mit "Was ist neu?" (D28) zusammen: drücken, und was seither in Produktion ging, leuchtet auf. Verhindern UND melden, beides: - Mindestabstand 10 s zwischen zwei Abrufen. Das ergibt höchstens 9 je 90 s und bleibt damit beweisbar unter Etherpads Budget — die Drosselung wird gar nicht erst ausgelöst. Nach einem Abbruch sind wir schon drüber: dann das ganze Fenster (90 s) abwarten statt weiter dagegen zu rennen. - Der Knopf bleibt in der Sperre **klickbar** (nur `aria-disabled`, nicht `disabled`): Ein Klick meldet den Grund und nennt die Grenze samt Restzeit. Die Meldung landet in `#warn`, das eine Live-Region ist, wird also angesagt. Ein `disabled` erklärt nichts. - Eigener Warnungstyp `sourceTimeout` für den Abbruch: `sourceLoad` zeigt auf CORS und schickte hier auf die falsche Fährte. - `setPadBusy()` dreht das Symbol während des Abrufs — das können bis zu 20 s sein, und ohne Zeichen wirkt der Knopf kaputt. Zwei Fehler des Vorgänger-Commits fielen dabei auf: - `padSource` und der Takt standen HINTER dem `await` des ersten Abrufs. Ein einziger Fehlschlag — bei dieser Gegenseite der Normalfall — ließ das Dokument tot liegen, ohne Knopf und ohne Wiederversuch, bis zum Neuladen der Seite. Genau das war die vom Nutzer gemeldete Fehlermeldung. - Kein Erfolg räumte die Warnung des ersten Versuchs weg; sie hätte gestanden, während der Text längst da war. Dazu: `deleteDoc()` beendet die Pad-Quelle, sonst legte ein späterer Abruf das gelöschte Dokument wieder an und aktivierte es auch gleich. Nebenbefunde, beide nachgemessen und in D31 festgehalten: - Das *Tippen* im Etherpad-Editor war der letzte offene Punkt. Der Nutzer hat ` - [ ] Layout` eingerückt eingetippt; der Export gibt es zeichengenau zurück (vier echte Leerzeichen, kein Tab, kein Listen-Markup). - Einbetten verweigert der Server nicht: kein `X-Frame-Options`, keine CSP mit `frame-ancestors`, und im Versuch baute das Pad in einem fremdstämmigen Rahmen seine Editorframes auf. Preis ist `SameSite=Lax` am Autoren-Cookie (jedes Laden ein neuer Autor; nur serverseitig zu beheben) und der Verlust beider Richtungen von D25, falls das Textpanel ersetzt wird. 86 Tests (85 -> 86); Schreibschutz zeigt den Text jetzt in Tinte statt grau — hier wird gelesen, der Plantext ist der Hauptinhalt. --- README.de.md | 13 +- README.md | 13 +- docs/DECISIONS.md | 109 ++++++++--- docs/SPEC.md | 19 +- docs/examples/example-werkbaum.werkbaum | 2 +- frontend/CLAUDE.md | 29 +-- frontend/index.html | 3 + frontend/src/app.js | 233 ++++++++++++++++++------ frontend/src/style.css | 15 +- frontend/src/warnings.js | 13 +- frontend/tests/warnings.test.js | 19 ++ 11 files changed, 357 insertions(+), 111 deletions(-) diff --git a/README.de.md b/README.de.md index 8e4aaa7..a261b99 100644 --- a/README.de.md +++ b/README.de.md @@ -101,9 +101,16 @@ sich ein **Etherpad**. Angegeben wird die Pad-Adresse, wie sie im Browser steht 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. +Alle bearbeiten den Notationstext **im Pad**; ein Neu-laden-Knopf neben dem +Pad-Knopf holt den aktuellen Stand. Das Zusammenführen gleichzeitiger Änderungen +macht Etherpad — genau darum geht es. + +Bewusst **kein** Abrufen im Hintergrund: Etherpad drosselt den Export +(serienmäßig 10 Abrufe je 90 s und IP), ein Takt gewinnt dagegen nicht — er +*erzeugt* die Drosselung. Nachgemessen: Nach Überschreiten des Budgets hält der +Server die Verbindung rund zwei Minuten ohne Antwort offen und liefert dann in +0,4 s. Der Knopf greift gut mit „Was ist neu?" zusammen: drücken, und was seither +in Produktion ging, leuchtet auf. 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 diff --git a/README.md b/README.md index 73d90b4..e636d86 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,16 @@ 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. +Everyone edits the notation text **in the pad**; a reload button next to the pad +button fetches the current state. Merging simultaneous edits is Etherpad's job — +that is the whole point. + +Deliberately **no background polling**: Etherpad rate-limits the export (10 +fetches per 90 s per IP by default), so a timer does not win against it — it +*causes* the throttling. Measured: after exceeding the budget the server holds +the connection open with no reply for about two minutes, then answers in 0.4 s. +The button pairs well with "what's new": press it, and whatever went into +production since your last look lights up. 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 diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1d004e5..e75c94f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -1000,10 +1000,14 @@ http(s), ohne Endung oder `Content-Type` zu prüfen (D24). Nachgemessen an 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.) +Das war zunächst nur Import → Speicher → Export, also **nicht** das Tippen — und +genau das war der offene Punkt (Tab-Einrückung, mögliches Auto-Bullet), im Geist +der Lehre aus D25: synthetische Ereignisse beweisen nur die eigene Logik. +**Nachgeholt an echter Eingabe:** Der Nutzer hat im Pad ` - [ ] Layout` +eingerückt eingetippt und eine Kommentarzeile geändert; der Export gibt beides +zeichengenau zurück (`> - [ ] Layout$` — vier echte Leerzeichen, kein Tab, +kein Listen-Markup). Der Etherpad-Editor fasst die Notation beim Tippen also +nicht an. **Eigener Parameter statt `?sourceUrl=`.** Drei Gründe, der erste ist der schwächste: @@ -1013,9 +1017,10 @@ schwächste: 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. + unangetastet. `etherpad` heißt „lebendes Pad", und daran hängen der + Schreibschutz, der „im Pad bearbeiten"-Knopf und der Neu-laden-Knopf. Ohne + die Trennung müsste D23 seine Semantik ändern und bestehende Links bekämen + Verhalten, um das niemand gebeten hat. 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 @@ -1042,19 +1047,51 @@ 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. +**Geholt wird auf Knopfdruck, nicht selbsttätig — Etherpad drosselt.** Die erste +Fassung holte alle 2,5 s im Hintergrund, mit einem Stabilitätstakt (erst +übernehmen, wenn zwei Abrufe denselben Text liefern) gegen das Mitlesen halb +getippter Zeilen. In der Praxis kam damit fast nichts an: im Netzwerk-Mitschnitt +stapelten sich Anfragen und wurden abgebrochen („cancelled"). -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. +Die Ursache ist nicht Langsamkeit, sondern ein **Rate Limit**: +`importExportRateLimiting` ist in Etherpad serienmäßig an und lässt **10 Abrufe +je 90 s und IP** zu — der Takt wollte 36. Danach antwortet die Gegenseite nicht +mit `429`, sondern **hält die Verbindung offen** (keine Kopfzeilen, keine +Antwort), bis der eigene Abbruch sie abreißt. Nachgemessen: + +``` +12:48:12 Abbruch nach 25 s (0 Bytes) + Abbruch nach 25 s (0 Bytes) +12:50:23 HTTP 200 nach 0,436 s <- nach ~40 s Pause +``` + +Zwei Minuten totgestellt, dann sofort in 0,4 s da. Gegen eine Drosselung kann ein +Takt nicht gewinnen — er *erzeugt* sie. Also: **ein Knopf**. Damit entfallen +Stabilitätstakt, Sichtbarkeits-Wächter, Anti-Stapel-Riegel und Wiederanlauf; was +bleibt, ist ein Abruf, wenn jemand ihn will. Das ist auch die ehrlichere Haltung +gegenüber fremder Infrastruktur, und es greift gut mit „Was ist neu?" (D28) +zusammen: drücken, und was seither in Produktion ging, leuchtet auf. + +Zwei Dinge, die der Knopf braucht und ein stiller Takt nicht: + +- **Rückmeldung während des Abrufs** (das Symbol dreht). Bei gedrosselter + Gegenseite sind das bis zu 20 s; ohne Zeichen wirkt der Knopf kaputt. +- **Eine Antwort im Fehlerfall.** Ein Hintergrund-Takt durfte stumm scheitern, + eine bewusste Handlung nicht. Der Abbruch bekommt dafür einen **eigenen** + Warnungstyp `sourceTimeout`: Die `sourceLoad`-Meldung zeigt auf CORS und + schickte hier auf die falsche Fährte — richtig ist „warte einen Moment". + +**Der erste Abruf darf scheitern, ohne alles zu verlieren.** In der ersten Fassung +standen `padSource` und der Takt *hinter* dem `await` des ersten Abrufs: Ein +einziger Fehlschlag — bei dieser Gegenseite der Normalfall — ließ das Dokument +tot liegen, ohne Knopf und ohne Wiederversuch, bis zum Neuladen der Seite. +Jetzt wird `padSource` **vor** dem Abruf gesetzt; der Knopf erscheint auch, wenn +es das Dokument noch nicht gibt, und legt es beim ersten Erfolg an. + +**Wer das Pad-Dokument löscht, meint es.** `deleteDoc()` beendet die Pad-Quelle. +Ohne das hätte der (damalige) Takt es wieder angelegt — und beim Anlegen auch +gleich aktiviert, den Nutzer also aus dem Dokument gerissen, in das er gewechselt +war. **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 @@ -1066,13 +1103,33 @@ 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 `