diff --git a/README.de.md b/README.de.md index 0f3c705..fbd8f46 100644 --- a/README.de.md +++ b/README.de.md @@ -88,60 +88,46 @@ 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) +### Zusammen an einem Plan arbeiten (`?live=`) -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): +Liegt der Plan auf einem Werkbaum-Backend, bearbeiten ihn alle **im Editor +selbst** und sehen die Änderungen der anderen, ohne neu zu laden: ``` -https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/mein-plan +https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/ ``` -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. +Angelegt wird so ein Dokument über den Eintrag **„Auf den Server legen"** im +Dokumenten-Menü: Er lädt den aktiven Plan hoch, schaltet dorthin um und schreibt +den Link in die Adresszeile und in die Zwischenablage. Diesen Link weitergeben — +die Adresse ist die Einladung, und wer sie kennt, kommt hinein. -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. +**Das Textfeld bleibt beschreibbar.** Nach 1,5 s Ruhe schickt der Editor die +Änderung als Zeilen-Diff; ein offener Abruf hält die Gegenrichtung bereit und +spielt fremde Änderungen ein. **Die Schreibmarke wandert mit** — fügt jemand +oberhalb Zeilen ein, bleibt sie an ihrer Stelle im Text. -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. +Der **Name** ist der Titel des Dokuments (alle sehen denselben), die vollständige +Adresse steht im Tooltip. -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. +**Überschneiden sich zwei Änderungen wirklich** — dieselben Zeilen —, fragt ein +Band oben, wessen Fassung gelten soll: *Fremde übernehmen* oder *Eigene +durchsetzen*. Alles andere führt der Server selbst zusammen, ohne zu fragen. +Verloren geht nichts: Der verworfene Stand bleibt in den früheren Ständen, jede +Version in der Historie des Servers. -Das Pad lässt sich auch **im Editor-Panel einbetten**: Ein Wähler in der -Titelzeile schaltet reihum zwischen *Pad und Text* (durch einen Splitter frei -geteilt), *nur Pad* und *nur Text*. Der schmal gezogene Textspiegel behält seinen -Zweck — der Sprung zwischen Diagramm und Text arbeitet auf ihm, und in *nur Pad* -holt ein Sprung ihn selbst zurück. Der Rahmen wird nur geladen, solange er -sichtbar ist, denn ein geladenes Pad verbindet sich und zeigt dich in dessen -Anwesenden-Liste. +**Ein gemeinsamer Zeigefinger:** `!!!` auf einer Zeile hebt diesen Knoten hervor +und holt ihn ins Bild — bei **allen**, die das Dokument ansehen. Das kann ein +Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!` bleibt also ein +gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie löscht. -**Der gemeinsame Zeigefinger:** Schreib `!!!` in eine Zeile, und dieser Knoten -wird hervorgehoben und ins Bild geholt — bei **allen**, die auf das Pad schauen. -Das kann ein Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!` -bleibt also ein gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie -löscht. +Das Backend einrichten: siehe [backend/README.md](backend/README.md) und +`docs/DECISIONS.md` D76. -**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist -für jeden lesbar, der die Adresse kennt. Im eingebetteten Rahmen wird Etherpads -Autoren-Cookie (`SameSite=Lax`) nicht mitgesendet — man gilt bei jedem Laden als -neuer Autor, was nur serverseitig zu beheben ist (`cookie.sameSite: "None"`). - -Aus demselben Grund ist der eingebettete Rahmen vor allem zum **Mitlesen** gut: -Sollte das Bearbeiten darin einmal aufhören zu funktionieren, den Ansichts-Wähler -einmal durchschalten (das lädt den Rahmen neu) oder über „im Pad bearbeiten" im -eigenen Tab weiterarbeiten, wo das Cookie gilt. Siehe `docs/DECISIONS.md` D31 -und D32. +> Dafür lieh sich Werkbaum früher ein **Etherpad** (`?etherpad=`). Das ist +> ausgebaut — das Backend kann dasselbe besser und im Editor selbst. Ein alter +> `?etherpad=`-Link zeigt heute einen Hinweis hierher, statt still nichts zu +> tun; siehe `docs/DECISIONS.md` D78. ### Lokal ausführen diff --git a/README.md b/README.md index dfa4792..c9fc084 100644 --- a/README.md +++ b/README.md @@ -83,57 +83,46 @@ 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) +### Working on one plan together (`?live=`) -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): +If the plan lives on a Werkbaum backend, everyone edits it **in the editor +itself** and sees the others' changes without reloading: ``` -https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/my-plan +https://werkbaum.javagil.de/?live=https://werkbaum.javagil.de/api/v1/documents/ ``` -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. +To create such a document, open the document menu and pick **"Put on the +server"**: it uploads the active plan, switches to it, and puts the link in the +address bar and on your clipboard. Share that link — the address is the +invitation, and knowing it is what grants access. -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. +The text area stays **writable**. After 1.5 s of quiet the editor sends the +change as a line diff; an open request holds the other direction ready and plays +foreign changes in. **The caret travels with them** — if someone inserts lines +above you, it stays where it was in the text. -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. +The document's **title** is its name (everyone sees the same one), the full +address sits in the tooltip. -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. - -The pad can also be **embedded** in the editor panel: a selector in the title bar -cycles through *pad and text* (split by a draggable divider), *pad only* and *text -only*. The narrow text mirror keeps its purpose — the jump between diagram and -text works on it, and in *pad only* a jump brings it back by itself. The frame is -only loaded while it is visible, because a loaded pad connects and shows you in -the pad's list of people. +When two changes really overlap — the same lines — a bar at the top asks whose +version should win: *take theirs* or *keep mine*. Everything else the server +merges without asking. Nothing is lost either way: the discarded state stays in +the earlier states, and every version is in the server's history. **A shared pointer:** write `!!!` on a line and that node is highlighted and -scrolled into view — for **everyone** looking at the pad, which is something a -cursor cannot do. Recognised only as a standalone token, so `Careful!!!` stays an -ordinary label. It stays in the text until someone deletes it. +scrolled into view — for **everyone** looking at the document, which is +something a cursor cannot do. Recognised only as a standalone token, so +`Careful!!!` stays an ordinary label. It stays in the text until someone +deletes it. -**Be aware:** your plan text then lives on third-party infrastructure, and a pad -is readable by anyone who knows its address. In an embedded frame Etherpad's -author cookie (`SameSite=Lax`) is not sent, so you count as a new author on every -load — fixable only on the server (`cookie.sameSite: "None"`). +Setting up the backend: see [backend/README.md](backend/README.md) and +`docs/DECISIONS.md` D76. -For the same reason the **embedded frame is best for reading along**: if editing -in it ever stops working, cycle the view selector once (that reloads the frame), -or use the "edit in the pad" button and work in the pad's own tab, where the -cookie does apply. See `docs/DECISIONS.md` D31 and D32. +> Werkbaum used to borrow an **Etherpad** for this (`?etherpad=`). That is +> removed — the backend does the same job better and in the editor itself. An +> old `?etherpad=` link now shows a note pointing here rather than silently +> doing nothing; see `docs/DECISIONS.md` D78. ### Running it locally diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4072703..f7d477a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,7 @@ reverse. ## 2026-08-26 +- The Etherpad integration is gone: collaboration now runs through a Werkbaum backend, and an old `?etherpad=` link says so instead of doing nothing - Plans can live on a Werkbaum backend now: open `?live=` and everyone edits the same text, seeing each other's changes without reloading - When two people change the same lines, a bar asks whose version should win — everything else the server merges by itself - The caret stays where you put it when someone else inserts lines above you diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 3834d22..112e959 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -6660,3 +6660,87 @@ Version), `frontend info` (1.1.162, Commit = HEAD), `backend status`, `backend restart`, `backend backup` samt lokalem Wiederanlauf, `frontend preview` (nichts geschrieben, HEAD unverändert) und die Fehlerpfade (Exit-Code 2 bei unbekanntem Ziel und fehlender Aktion). + +## D78 — Die Etherpad-Anbindung ist ausgebaut +D31 lieh sich für die Echtzeit-Zusammenarbeit ein **Etherpad**: Das Pad war die +Schreibfläche, Werkbaum die Ansicht. Das war die richtige Entscheidung für den +Zeitpunkt — die schwere Arbeit (gleichzeitige Änderungen zusammenführen) war +dort seit Jahren getan, und Werkbaum hatte kein Backend. **Jetzt hat es eins** +(D76), und das kann dasselbe besser und im Editor selbst. Also raus damit, +statt zwei Wege zur selben Sache zu pflegen. + +**D31 bleibt stehen** — Entscheidungen werden nicht gelöscht, sondern +fortgeschrieben. Was dort gemessen wurde, gilt weiter und ist der Grund, warum +die Anbindung nie gut wurde: Etherpad **drosselt den Export** (serienmäßig 10 +Abrufe je 90 s und IP), ein Hintergrund-Takt erzeugt die Drosselung, statt sie +zu umgehen; das Autoren-Cookie ist `SameSite=Lax` und kommt im eingebetteten +Rahmen nicht an, also ist man bei jedem Laden ein neuer Autor; und ein eigener +Socket zum Pad wurde von der Gegenseite abgelehnt (D31-Nachtrag, Code 1006). +Übrig blieb ein Neu-laden-Knopf und ein schreibgeschütztes Textfeld — die +Rückmeldung „funktioniert sowieso nicht gut" ist die ehrliche Zusammenfassung. + +**Was `?live=` besser macht**, in derselben Reihenfolge: Es wird **im Editor +geschrieben** statt in einem fremden Rahmen; die Gegenrichtung ist ein offener +Abruf statt eines Takts gegen ein fremdes Limit; der Konflikt wird benannt +statt versteckt; und der Plantext liegt auf **eigener** Infrastruktur statt auf +einem Pad, das jeder lesen kann, der die Adresse kennt. + +**Ein alter Link meldet sich, statt still nichts zu tun.** `?etherpad=` bleibt +als **erkannter** Parameter stehen und ergibt die zeilenlose Warnung `padGone`, +die auf `?live=` zeigt — in allen neun Sprachen. Der Parameter war geteilt: Wer +ihn irgendwo stehen hat (in einer Mail, einem Wiki, einem Lesezeichen), bekäme +sonst ein leeres Werkbaum ohne Erklärung. Genau der stille Fehler, den dieses +Projekt sonst überall ablehnt (SPEC §4, D59). Es ist bewusst **kein** +Rest-Feature: Geholt wird nichts, der Pad-Host sieht keine Anfrage mehr +(nachgemessen). + +**Vorhandene Pad-Dokumente bleiben liegen — und werden dabei besser.** Sie sind +im localStorage gewöhnliche Dokumente (`{id, name, text, source}`); ohne den +Schreibschutz sind sie ab jetzt **bearbeitbar**, sammeln frühere Stände (D54) +und lassen sich falten. Ihr Text ist der zuletzt geholte. Nichts wird gelöscht, +niemand verliert seinen Plan. + +**Der Schreibschutz verschwindet ganz, nicht nur seine Ursache.** `src.readOnly` +wurde ausschließlich von Pad-Dokumenten gesetzt; danach hätten sieben Wächter +in Falten, Kurz-IDs, Autovervollständigung und Ständen auf eine Bedingung +geprüft, die nie mehr wahr wird — mit Kommentaren, die auf D31 zeigen. Tote +Wächter mit veralteter Begründung sind schlechter als keine; ein künftiger +Lesemodus braucht ohnehin seine eigene Entscheidung. Mit ihm fällt +`updateSnapBtn()` weg: Der Knopf ist jetzt immer da. + +**Eine Layout-Ebene weniger.** `#srcArea` gab es nur, damit Pad-Rahmen und +Textspiegel sich einen Bereich teilen konnten, ohne die Legenden-Aufteilung +(D26) anzufassen. Ohne Rahmen ist es ein Kasten mit einem Kind. Nachgemessen +nach dem Entfernen: `.editor-body` trägt jetzt direkt `srcWrap` · `hintGutter` · +`agenda`, und die drei kacheln die Breite exakt (954 + 10 + 300 px) — der +Legenden-Splitter arbeitet unverändert. + +**Mit ausgebaut**, weil sie nur der Anbindung dienten: `remote.js` samt seinen +Tests (die Pad-Adressen normalisieren), der Ansichts-Wähler und sein Splitter +(`--pcol`/`--prow`), der Neu-laden-Knopf samt Drosselungs-Zähler, die Warnungen +`sourceTimeout` und `padRateLimit`, elf i18n-Schlüssel × 9 Sprachen und der +Timeout-Parameter von `fetchRemote()`. **`?sourceUrl=` bleibt unangetastet** +(D23) — beide teilten sich einen Fetch-Pfad, und das war die eine Stelle, an der +beim Schneiden nichts verrutschen durfte. + +**Der Plan sagt es auch.** Im mitgelieferten `werkbaum.werkbaum` wird aus dem +`#col.pad`-Zweig (14 Knoten samt der ganzen „Update by itself"-Gruppe, die nur +Etherpads Grenze umgehen wollte) **ein** verworfener Knoten `[-] #col.pad` mit +Begründung — dieselbe Form, in der dort schon `#bld.ghpages` und `#not.store` +stehen. Der eine Knoten, der weiterlebt, wandert heraus: `#col.pad.point` wird +`#col.point`, denn der gemeinsame Zeigefinger `!!!` gilt jedem geteilten +Dokument, nicht nur einem Pad. Danach 196 Knoten, 0 Warnungen. + +**Nebengewinn: Der README bekommt endlich den Abschnitt zu `?live=`.** Den gab +es nie — D76 ist gebaut und dokumentiert (SPEC §9, DECISIONS), aber die +Einstiegs-Beschreibung stand weiter beim Pad. Das Ausbauen hätte sonst ein Loch +hinterlassen; jetzt steht dort in beiden Sprachen, wie man ein Server-Dokument +anlegt, teilt und was bei einem Konflikt passiert. + +**Nachgemessen** im Browser: `?etherpad=…` zeigt die Warnung, und im +Netzwerk-Mitschnitt geht **keine** Anfrage an den Pad-Host; `?sourceUrl=` lädt +unverändert und das Dokument ist beschreibbar (früher: schreibgeschützt); das +Textfeld liegt mit dem Zahlenstreifen bündig (1151 + 20 px), die Zeilennummern +sitzen auf ihren Höhen, Pfad und Stationen werden gezeichnet; der +Legenden-Splitter teilt wie zuvor. 501 Tests (die 24 Pad-Adress-Tests sind mit +`remote.js` gegangen, `padGone` ist dazugekommen). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 88c6f3b..51c912d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -112,7 +112,7 @@ Tastendruck ein Commit); und wer bei einem Backend eigentlich was darf Zu klären ist das Zusammenspiel mit der Update-Anzeige (D45): Sitzt ein Worker vor der Seite, entscheidet **er**, was ausgeliefert wird — „der laufende Build gegen den, den der Server schickt" ist dann nicht mehr - dasselbe. Dokumente aus `?sourceUrl=`/`?etherpad=` bleiben naturgemäß + dasselbe. Dokumente aus `?sourceUrl=`/`?live=` bleiben naturgemäß online. Im Plan: `#bld.pwa`. - Schlagworte `&tag` mit einer Filter-/Hervorheben-Linse im Diagramm: Der Baum ist genau eine Zerlegung — ein Tag benennt eine Menge von Knoten quer dazu diff --git a/docs/SPEC.md b/docs/SPEC.md index 1d5c152..9ef2dbe 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -155,7 +155,7 @@ ab, auch außerhalb seines eigenen Teilbaums. **Fokusmarke `!!!`** — „schau hier hin": Der Knoten wird im Diagramm hervorgehoben und ins Bild geholt (§9). Gedacht für das gemeinsame Arbeiten an -einem Pad (§9, `?etherpad=`): Weil dort niemand den Cursor der anderen sieht, +einem Dokument (§9, `?live=`): Weil dort niemand den Cursor der anderen sieht, ist eine Marke **im Text** der einzige Weg, auf eine Stelle zu zeigen — und sie hat etwas, das ein Cursor nicht hat: **alle** sehen dieselbe Stelle. @@ -770,59 +770,18 @@ 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. -- Das Pad kann **im Editor-Panel eingebettet** werden. Ein Wähler in der - Titelzeile schaltet reihum zwischen drei Ansichten: **Pad und Text** (beide, - durch einen eigenen **Splitter** frei geteilt — Doppelklick setzt zurück), - **nur Pad** und **nur Text**. Die Aufteilung wird für nebeneinander und - gestapelt getrennt gehalten und bleibt erhalten. - - Der schmal gezogene Textspiegel behält seinen Zweck: Der Sprung zwischen - Diagramm und Text (§9) arbeitet auf ihm. In „nur Pad" ist er ausgeblendet — - ein Sprung holt ihn dann selbst zurück, so wie er ein zugeklapptes - Editor-Panel aufklappt. - - Der Rahmen wird **nur geladen, wenn er sichtbar ist**. Ein geladenes Pad - verbindet sich und zeigt dich in dessen Anwesenden-Liste; „nur Text" ist - damit die Ansicht, die nichts von dir verrät. -- Geholt wird **auf Knopfdruck**, nicht selbsttätig: Ein Neu-laden-Knopf neben - dem Pad-Knopf holt den aktuellen Stand. Etherpad **drosselt** den Export - (serienmäßig 10 Abrufe je 90 s und IP); ein Hintergrund-Takt läuft dagegen an - und bekommt am Ende gar nichts mehr. Läuft ein Abruf, dreht das Symbol — bei - gedrosselter Gegenseite kann das bis zum Abbruch (20 s) dauern. -- Der Knopf greift gut mit „Was ist neu?" (§9) zusammen: drücken, und was seither - in Produktion ging, leuchtet auf. -- **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 **Abbruch** - wegen Zeitablauf bekommt eine eigene Meldung, die die Drosselung benennt — - die `?sourceUrl=`-Meldung zeigt auf CORS und schickte hier auf die falsche - Fährte. Scheitert schon der erste Abruf, bleibt der Neu-laden-Knopf sichtbar - und holt es nach; ein Neuladen der Seite ist nicht nötig. - -Siehe D31. +### Kein Etherpad mehr (`?etherpad=`, entfallen) +Der Editor konnte den Notationstext aus einem **Etherpad-Pad** beziehen: Alle +schrieben im Pad, Werkbaum zeigte das Diagramm dazu. Das ist **ausgebaut** — +`?live=` (unten) kann dasselbe besser, und zwar im Editor selbst. Ein alter +`?etherpad=`-Link ergibt heute eine **Warnung**, die auf `?live=` zeigt, statt +still nichts zu tun. Siehe D78; die frühere Ausführung steht in D31. ### Gemeinsam an einem Server-Dokument arbeiten (`?live=`) Liegt der Plan auf einem Werkbaum-Backend, nimmt der Editor dessen Dokument-Adresse: `…?live=https://example.org/api/v1/documents/`. -Anders als beim Pad (`?etherpad=`) wird hier **im Editor selbst geschrieben**, -und alle sehen die Änderungen der anderen, ohne neu zu laden. +Geschrieben wird **im Editor selbst**, und alle sehen die Änderungen der +anderen, ohne neu zu laden. - **Das Textfeld bleibt beschreibbar.** Nach kurzer Ruhe (1,5 s) schickt der Editor die Änderung als Zeilen-Diff; ein offener Abruf hält die Gegenrichtung @@ -852,8 +811,7 @@ 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, per `?sourceUrl=` oder -`?etherpad=`), wird +Bei Dokumenten, die von außen kommen (mitgeliefert oder per `?sourceUrl=`), 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 @@ -1018,8 +976,7 @@ eigene ID der Zeile. weiter: Sie fängt keine Taste ab, solange sie nichts anzeigt, und Tab rückt bei geschlossener Liste unverändert ein. - **Kein Vorschlag** bei bloßem `#` (das *definiert* eine ID), im Kommentar - (`%%`), im Beschreibungsteil hinter `---` und in schreibgeschützten - Pad-Dokumenten (§9). + (`%%`) und im Beschreibungsteil hinter `---`. - Der Parser sieht nie etwas davon; `llms.md` (§13) bleibt unberührt. Für Screenreader meldet eine höfliche Live-Region die Trefferzahl und den gewählten Eintrag; das Popup selbst ist `aria-hidden`, normales Tippen @@ -1099,13 +1056,12 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom). angefasst, solange die übrigen Marken den Zustand noch richtig beschreiben — ein von Hand gesetztes `<` bleibt also stehen. Trifft es nicht mehr zu, werden alle Marken neu gesetzt und das `<` dabei aufgelöst. -- Wo der Text **nicht beschreibbar** ist — bei einem Pad-Dokument (§9) —, gilt - der Eingriff **je Knoten** (Identität = Label-Pfad, wie bei „Was ist neu?“) - und **nur für die Sitzung**; ein Dokumentwechsel setzt ihn zurück. Dasselbe - gilt für einen Zustand, der sich in Marken gar nicht ausdrücken lässt (etwa - weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird - lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes - sagt als das Bild. +- Lässt sich ein Zustand in Marken **gar nicht ausdrücken** (etwa weil eine + Fokusmarke `!!!` ihren Knoten immer wieder hervorholt), wird lieber nichts + geschrieben, als einen Text zu hinterlassen, der etwas anderes sagt als das + Bild. Der Eingriff gilt dann **je Knoten** (Identität = Label-Pfad, wie bei + „Was ist neu?“) und **nur für die Sitzung**; ein Dokumentwechsel setzt ihn + zurück. - **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Durchschalter**: Jeder Druck stellt die **nächste** von vier Voreinstellungen her, reihum — 1. **ab Größe M abwärts zugeklappt**: jeder Knoten mit Kindern, dessen diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index 87abb3b..eea182d 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -131,30 +131,10 @@ | [?] #be.auth.token: Single user, one token (S) | [?] #be.auth.oidc: Log in with OIDC (L) - [~] #col: Collaborating on a Diagram (XXL) - " The pad merges concurrent edits, Werkbaum renders the plan. - - [^] #col.pad: Watch a shared Etherpad — it merges, we render (L) - - [^] #col.pad.export: Load the pad's plain-text export (XS) - - [^] #col.pad.reload: Reload on demand (XS) %% a timer only trips Etherpad's rate limit - - [^] #col.pad.embed: Embed the pad, keep the text mirror beside it (M) - - [^] #col.pad.views: Three views: pad and text, pad only, text only (S) - - [^] #col.pad.split: Splitter between pad and mirror (XS) - - [?] #col.pad.cookie: Editing in the frame stops after a while (S) %% SameSite=Lax, see D31 - | [?] #col.pad.samesite: Ask the admin for cookie.sameSite = None (XS) - | [ ] #col.pad.owntab: Live with it: edit in the pad's own tab (XS) - - [^] #col.pad.point: A shared pointer: !!! focuses a node for everyone (XS) - - [?] > #col.auto: Update by itself, within about 2 s (M) - | [?] #col.own: Our own Etherpad beside Werkbaum (L) %% then every setting below is ours - - [ ] #col.own.db: Node and a real database in userland (S) %% the flatfile store is not for production - - [ ] #col.own.ram: Book the RAM quota for an own daemon (XS) - - [ ] #col.own.proxy: Reverse proxy in .htaccess, websockets included (S) %% Mattermost does it, wiki shows how - - [ ] #col.own.unit: systemd user unit so it survives a reboot (XS) - - [!] #col.own.patch: Keeping a public service patched (S) %% indefinitely, that is the real cost - | [?] #col.auto.limit: Admin raises the export rate limit (XS) %% default is 10 per 90 s - | [?] #col.auto.plugin: Admin installs a plugin that pushes the text (S) %% postMessage to the page - | [?] #col.auto.relay: Our own backend holds one socket and fans out (XXL) :#be.scaffold - | [ ] #col.auto.poll: Poll inside the budget, at best every 9 s (S) %% legal, but not 2 s - | [-] #col.auto.socket: Our own socket straight to the pad (L) %% measured: upgrade refused - | [-] #col.auto.html: Read the pad's HTML instead of the export (M) %% measured: no CORS header + " Reading together is solved, changing together too — typing together at the + " same instant is the expensive one. + - [^] #col.point: A shared pointer: !!! focuses a node for everyone (XS) + - [-] #col.pad: Borrow an Etherpad — it merges, we render (L) %% built, then removed, see D78 - [ ] #col.git: Git as the shared store (L) | [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code | [?] #col.git.auto: The backend commits every change (L) :#be.scaffold @@ -714,8 +694,8 @@ #bld.pwa.sw A worker lets the editor start without a network. It is deliberately dumb: navigations go network-first, and the cache only keeps the last version it - has seen of the one self-contained file for the offline case. Texts pulled from a URL - or a pad pass through untouched and stay online by their nature. + has seen of the one self-contained file for the offline case. Texts pulled from + a URL or a server pass through untouched and stay online by their nature. #bld.pwa.update The feared migration turned out unnecessary: because the worker only @@ -838,96 +818,17 @@ machinery. Reading together is solved, changing together mostly is, and typing together at the same instant is the expensive one. +#col.point + Nobody sees anyone else's caret, so a mark in the text is the only way to + point at a place — and it has something a caret has not: everyone sees the + same spot. + #col.pad - Let an Etherpad do the hard part: it merges concurrent edits, and Werkbaum - only renders the result. Borrowed rather than rebuilt. - -#col.pad.export - Every pad offers its content as plain text, with the header that lets a - foreign page read it. That one endpoint is the entire integration. - -#col.pad.reload - Fetching happens on a button press, not on a timer. The export is rate - limited, and a timer does nothing but trip it. - -#col.pad.embed - The pad in a frame inside the editor, with our own text mirror beside it. - The mirror keeps the jumps between diagram and text working, because a - foreign frame gives up no access to its content. - -#col.pad.views - Pad and text, pad only, text only. Text only is the view that tells the pad - nothing about you — a loaded pad announces you in its list of people. - -#col.pad.split - A splitter of its own between frame and mirror, remembered separately for - side by side and stacked. - -#col.pad.cookie - In a foreign frame the pad's author cookie is not sent, so a session can - turn read-only after a while. Not fixable from this side. - -#col.pad.samesite - One server setting resolves it. Presumes somebody who can change that - setting. - -#col.pad.owntab - Or live with it: read along in the frame, and write in the pad's own tab - where the cookie works. - -#col.pad.point - Nobody sees anybody else's cursor across two applications, so a mark in the - text is the only way to point at something. It has one thing a cursor - hasn't: everyone sees the same spot. - -#col.auto - The view should follow the pad by itself, within a couple of seconds. Every - route below was measured rather than guessed at. - -#col.own - Running the pad ourselves makes every setting below it ours. It also makes - every one of them our job. - -#col.own.db - The default file store is not meant for production, so a real database and a - runtime to host it are the price of admission. - -#col.own.ram - A daemon of one's own needs memory booked for it. - -#col.own.proxy - Reaching it needs a reverse proxy that passes websockets through as well. - -#col.own.unit - And something that starts it again after a reboot. - -#col.own.patch - The real cost is not the setup but keeping a public service patched, - indefinitely. - -#col.auto.limit - The cheapest effective change: raise the export limit on the pad server. Ten - fetches per ninety seconds is the default, and the reason a timer fails. - -#col.auto.plugin - A pad plugin can push the text to the embedding page. Live, no polling, no - limit touched — and nothing to install on our side. - -#col.auto.relay - Our own backend holds one connection to the pad and fans out to every - viewer. Solves it for everybody at once, and costs a backend. - -#col.auto.poll - Staying inside the budget means one fetch every nine seconds at best, shared - by everyone behind the same address. Legal, but not two seconds. - -#col.auto.socket - Measured and rejected: the pad server refuses the websocket upgrade when it - comes from another origin. - -#col.auto.html - Measured and rejected: the pad page carries the full text, but sends no - header that would let a foreign page read it. + Removed. An Etherpad did the hard part for a while: it merged concurrent + edits and Werkbaum only rendered the result, borrowed rather than rebuilt. + The borrowing is what it cost — a rate-limited export, no writing from here, + and an author cookie that a foreign frame never gets. The backend does it + better and in our own editor, so this went out again. #col.git Keep the plan in a repository. Because it is text, diff, blame, review and diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index f5538f2..1c2e5f6 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -76,7 +76,7 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der (verworfene einblenden, Pfad an/aus) als **Parameter** — keine Globals; nur `cheapPathOn` lebt als UI-State in `app.js`. Tests: `tests/*.test.js`. - **Was entscheidbar ist, gehört in ein eigenes Modul** — auch bei Features, die - wie reine UI aussehen: `remote.js` (Pad-URLs normalisieren, D31), + wie reine UI aussehen: `live.js` (Server-Dokumente: Adressen, Zeilen-Diff, Cursor-Rechnung, wann eine Feed-Antwort angewendet werden darf — D76), `warnings.js` (Warnung → Text), `snapshots.js` (frühere Stände: wann entsteht @@ -201,47 +201,18 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der Endung `.werkbaum` ist reine Konvention (D24, SPEC §12). Beispieldateien zum Ausprobieren: `docs/examples/*.werkbaum` (nacheinander geöffnet ergeben sie mehrere Dokumente im Wähler). -- `?etherpad=` (D31): `remoteSource()` liefert **einen** Beschreiber für beide - Eingänge (`?sourceUrl=` und `?etherpad=`), `loadRemoteSource()` holt und legt - das Dokument an — ein Fetch-Pfad, ein Warnkanal. Die Normalisierung der - Pad-Adresse steht headless in **`remote.js`** (`padUrls`, Tests in - `tests/remote.test.js`): Export-/Timeslider-Pfad, Query, Fragment und - Schrägstriche fallen weg, verlangt wird `/p/` am Ende. Name und id sind - die **vollständige** Pad-URL (Pad-Namen sind nur pro Instanz eindeutig). - `padSource` muss **vor** dem ersten Abruf und vor `loadActiveIntoEditor()` - gesetzt werden: Es trägt den Schreibschutz, und scheitert der erste Abruf - (Normalfall bei dieser Gegenseite), bleibt so der Neu-laden-Knopf erreichbar - statt das Dokument tot liegen zu lassen. Geholt wird **nur auf Knopfdruck** - (`refreshPad`) — **kein Hintergrund-Takt wieder einbauen**: Etherpads - `importExportRateLimiting` lässt serienmäßig 10 Abrufe je 90 s und IP zu, und - jenseits davon hält die Gegenseite die Verbindung ohne Antwort offen (kein - `429`), bis der eigene Abbruch sie abreißt — gemessen: zwei Minuten tot, danach - 0,4 s. Ein Takt gewinnt dagegen nicht, er erzeugt die Drosselung. Deshalb auch - `setPadBusy()` (Symbol dreht, bis zu 20 s) und ein **eigener** Warnungstyp - `sourceTimeout` für den Abbruch — `sourceLoad` zeigt auf CORS und schickte hier - auf die falsche Fährte. `deleteDoc()` ruft `stopPad()`, sonst legte ein späterer - Abruf das gelöschte Dokument wieder an und aktivierte es auch gleich. - Beim Prüfen im Vorschau-Browser: HMR lädt bei jeder Quelländerung neu, und ein - Reload holt den Text beim Erstabruf — sieht wie eine geglückte Übernahme aus. - Marker auf `window` setzen und hinterher prüfen, sonst beweist der Test nichts. -- Pad einbetten, drei Ansichten (D31): `padView` ('both'|'pad'|'text') + - `applyPadView()`. Der Rahmen bekommt **nur sichtbar** ein `src`, sonst - `about:blank` — ein geladenes Pad verbindet sich per Socket und zeigt dich in - dessen Anwesenden-Liste. `PAD_VIEWS`/`padView` stehen **oben** bei - `cheapPathOn`, nicht beim übrigen Pad-Code: `saveUI()` liest sie und kann schon - aus `applySplit()` heraus laufen, weiter unten stünden sie noch in der - temporalen Todeszone. Der Splitter Pad|Spiegel (`#padGutter`, `--pcol`/`--prow`) - ist eine Kopie des Legenden-Splitters (D26) samt getrennter Persistenz. - `revealEditor()` schaltet 'pad' → 'both', sonst zeigte der Sprung (D25) ins - Nichts. Das Markup: `#srcArea` umschließt Rahmen + Splitter + `#src`, damit die - Legenden-Aufteilung unberührt bleibt — `.editor-body textarea` ist ein - Nachfahren-Selektor und greift weiter. Nebenwirkung, die als Behelf taugt: - Einmal durch die Ansichten schalten **lädt den Rahmen neu** (über - `about:blank`) — das ist der Ausweg, wenn ein eingebettetes Pad nicht mehr - beschreibbar ist. Nicht in den Neu-laden-Knopf legen: der zieht nach dem Tippen - Spiegel und Diagramm nach, und ein Rahmen-Reload dabei kostete jedes Mal die - Schreibmarke im Pad. **Umhängen im DOM lädt jeden ` - - +
-
diff --git a/frontend/public/llms.md b/frontend/public/llms.md index fb29dc7..bc5a2cf 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -10,7 +10,8 @@ Files are UTF-8 with LF line endings; the extension `.werkbaum` is a convention (plain `.txt` works too). The editor at https://werkbaum.javagil.de renders the notation as a diagram; `?sourceUrl=` loads any CORS-readable http(s) text file, and -`?etherpad=` follows a live Etherpad. +`?live=` opens a document on a Werkbaum backend that several +people edit at once. ## Line format diff --git a/frontend/src/app.js b/frontend/src/app.js index ba1ec99..b9bb66d 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -3,7 +3,6 @@ import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js'; import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { formatWarning, warningText } from './warnings.js'; -import { padUrls } from './remote.js'; import * as live from './live.js'; import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js'; import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel } @@ -69,18 +68,14 @@ let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */ Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */ const FOLD_CYCLE = ['small', 'path', 'closed', 'open']; let foldCycleNext = 0, foldCycleApplied = -1; -/* Ansicht bei einem Pad-Dokument (D31): beide | nur Pad | nur Spiegel. Hier oben - deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen - kann — weiter unten stünde sie dann noch in der temporalen Todeszone. */ -const PAD_VIEWS = ['both', 'pad', 'text']; -let padView = 'both'; /* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag): dort ist immer - genau einer zu sehen. Aus demselben Grund hier oben deklariert wie `padView` - — saveUI() liest ihn und läuft schon aus applySplit() heraus. */ + genau einer zu sehen. Hier oben deklariert, weil saveUI() ihn liest und schon + aus applySplit() heraus laufen kann — weiter unten stünde er dann noch in der + temporalen Todeszone. */ let mobilePane = 'diagram'; /* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde, - sonst wäre nach dem Laden ungefragt die Wurzel markiert. Hier oben deklariert - wie `padView`: der Zeilennummern-Streifen (D33) liest sie, und der hängt an + sonst wäre nach dem Laden ungefragt die Wurzel markiert. Aus demselben Grund + hier oben: der Zeilennummern-Streifen (D33) liest sie, und der hängt an render(). */ let caretLine = null, currentNodeEl = null; /* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */ @@ -242,7 +237,7 @@ function render(){ /* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt — aber nur, wenn sich die Marke **geändert** hat. Sonst zöge jeder Neubau des Baums den Blick zurück und man käme nicht weg. Verglichen wird das Label des - Knotens, nicht die Zeilennummer: Umsortieren im Pad soll nicht als neue Marke + Knotens, nicht die Zeilennummer: Umsortieren soll nicht als neue Marke gelten. Die Hervorhebung selbst macht die Klasse `focusmark` aus dem Renderer, hier geht es nur ums Scrollen. */ let lastFocusMark = null; @@ -1074,14 +1069,6 @@ function scrollEditorToOffset(offset){ /* Ist das Editor-Panel zugeklappt, muss der Sprung es erst öffnen. */ function revealEditor(){ - /* In der Ansicht „nur Pad" (D31) ist der Spiegel ausgeblendet — dann gibt es - keine Zeile zum Markieren. Der Sprung holt ihn deshalb zurück, genau wie er - ein zugeklapptes Editor-Panel aufklappt (D25). */ - if(padView === 'pad' && padSource && activeId === padSource.id){ - padView = 'both'; - applyPadView(); - saveUI(); - } if(isMobile()){ /* Auf Mobil ist der Text womöglich gar nicht vorn — dann holt der Sprung ihn nach vorn, so wie er auf dem Desktop ein zugeklapptes Panel aufklappt @@ -1133,7 +1120,7 @@ function nodeFromEvent(e){ Umklappen im Diagramm ändert den Notationstext. Der Text ist damit auch für die Faltung die eine Quelle der Wahrheit (D14) — die Sitzungs-Überlagerung `foldOverrides` bleibt nur für Dokumente, in die nicht geschrieben werden - kann (Pad, schreibgeschützt nach D31). */ + kann. */ /* Der Sollzustand als Menge von Label-Pfad-Schlüsseln: heutiger Anfangszustand aus dem Text, überlagert von den Eingriffen — dieselbe Rechnung wie in @@ -1222,7 +1209,6 @@ function withEditorWritable(fn){ /* Schreibt den Faltzustand in den Text. Liefert false, wenn nicht geschrieben werden konnte — dann bleibt die Sitzungs-Überlagerung stehen. */ function writeFoldToText(line, collapsed){ - if(src.readOnly) return false; /* Pad-Dokument (D31) */ const roots = parse(src.value).roots; if(!roots.length) return false; const want = desiredFoldKeys(roots); @@ -1273,8 +1259,8 @@ function applyFoldPreset(mode){ if(!n.children.length) return; /* nur faltbare Knoten */ foldOverrides.set(key, want.has(n)); }); - if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear(); - else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */ + if(writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear(); + else render(); /* nicht ausdrückbar: Überlagerung trägt */ } /* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text @@ -1620,7 +1606,7 @@ function resolveShortId(line){ writeShortId(line); } function writeShortId(line){ - if(src.readOnly || line == null) return; + if(line == null) return; /* **Nicht** sofort schreiben: Beide Wege hierher hängen am `input`-Ereignis — der Doppelpunkt unmittelbar, der Zeilenwechsel über die Enter-Taste —, und `execCommand` verweigert den Dienst, wenn es @@ -1751,7 +1737,7 @@ function closeAc(){ acCtx = null; } function updateAc(){ - if(src.readOnly || document.activeElement !== src || + if(document.activeElement !== src || src.selectionStart !== src.selectionEnd){ closeAc(); return; } const ctx = depFragment(src.value, src.selectionStart); if(!ctx){ acSuppress = null; closeAc(); return; } @@ -2221,21 +2207,11 @@ const I18N = { acHint:"{n} ID-Vorschläge – ↑/↓ wählt, Enter übernimmt", tipClose:"Schließen", tipOpenLink:"Link öffnen", - padReadonly:"Wird im Pad bearbeitet — hier nur lesen.", - padEdit:"Pad zum Bearbeiten öffnen", - padRefresh:"Vom Pad neu laden", liveLoadWarn:"Server-Dokument nicht geladen: {url} ({error}). Läuft das Backend, und ist die Adresse eine Dokument-Adresse (…/documents/<uuid>)?", liveStaleWarn:"Deine Änderung war nicht mehr anwendbar ({error}) — der Stand wurde einmal frisch geholt.", liveConflictText:"Jemand hat dieselben Zeilen geändert. Wessen Fassung soll gelten?", liveConflictTheirs:"Fremde übernehmen", liveConflictMine:"Eigene durchsetzen", - padWait:"Noch {seconds} s — Etherpad begrenzt die Abrufe", - padRateLimitWarn:"Noch nicht neu geladen: Etherpad erlaubt nur wenige Abrufe je Zeitfenster (serienmäßig 10 pro 90 s), so häufiges Nachladen ist leider nicht möglich. In {seconds} s geht es wieder.", - padViewTooltip:"Ansicht: {state} — klicken zum Wechseln", - padView_both:"Pad und Text", - padView_pad:"nur Pad", - padView_text:"nur Text", - padGutterAria:"Pad und Texteditor größenverändern", riskTooltip:"High Risk – Aufwand noch unklar.", discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden", cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück", @@ -2307,7 +2283,7 @@ const I18N = { st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen", unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.", sourceLoadWarn:"„{url}“ konnte nicht geladen werden ({error}). Die Datei muss per http(s) erreichbar sein und CORS erlauben (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"„{url}“ hat innerhalb von {seconds} s nicht geantwortet — der Abruf wurde abgebrochen. Etherpad begrenzt, wie oft der Export geholt werden darf (serienmäßig 10-mal pro 90 s); warte einen Moment und lade dann erneut.", + padGoneWarn:"Dieser Link zeigt auf ein Etherpad-Pad. Die Etherpad-Anbindung gibt es nicht mehr — gemeinsames Arbeiten läuft jetzt über ein Werkbaum-Backend (?live=…). Der Text des Pads lässt sich dort einmal einfügen und dann zu zweit bearbeiten.", a11yStatus:"Status: {status}", a11ySize:"Aufwand: {size}", a11ySizeImplicit:"Aufwand: mindestens {size} (angenommen)", a11yTags:"Zuständig: {names}", a11yId:"ID: #{id}", a11yDeps:"hängt ab von: {ids}", a11yFolded:"eingeklappt, {n} verborgen", a11yEffective:"effektiv: {status}", heldTooltip:"effektiv {eff} — selbst schon {own}, wartet auf Abhängigkeiten", a11yOptional:"optional", a11yFocusMark:"hierhin schauen", a11yLink:"verlinkt", hint_indent:"Einrückung (2 Leerzeichen oder Tab) definiert die Hierarchie.", hint_all:"Teilpaket, alle erforderlich", hint_any:"Alternative, eine wählen", @@ -2343,21 +2319,11 @@ const I18N = { acHint:"{n} id suggestions – ↑/↓ to choose, Enter to insert", tipClose:"Close", tipOpenLink:"Open link", - padReadonly:"Edited in the pad — read-only here.", - padEdit:"Open the pad to edit", - padRefresh:"Reload from the pad", liveLoadWarn:"Server document not loaded: {url} ({error}). Is the backend running, and is the address a document address (…/documents/<uuid>)?", liveStaleWarn:"Your change no longer applied ({error}) — the document was fetched afresh once.", liveConflictText:"Someone changed the same lines. Whose version should win?", liveConflictTheirs:"Take theirs", liveConflictMine:"Keep mine", - padWait:"{seconds} s to go — Etherpad limits how often we may fetch", - padRateLimitWarn:"Not reloaded yet: Etherpad only allows a few fetches per time window (10 per 90 s by default), so syncing this often is unfortunately not possible. Try again in {seconds} s.", - padViewTooltip:"View: {state} — click to switch", - padView_both:"pad and text", - padView_pad:"pad only", - padView_text:"text only", - padGutterAria:"Resize pad and text editor", riskTooltip:"High risk – effort still unclear.", discardedTooltip:"Show/hide discarded nodes and their subtree", cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede", @@ -2428,7 +2394,7 @@ const I18N = { st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded", unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.", sourceLoadWarn:"Could not load “{url}” ({error}). The file must be reachable via http(s) and allow CORS (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"“{url}” did not answer within {seconds} s — the request was aborted. Etherpad limits how often the export may be fetched (10 times per 90 s by default); wait a moment, then reload.", + padGoneWarn:"This link points at an Etherpad pad. The Etherpad connection is gone — collaboration now runs through a Werkbaum backend (?live=…). Paste the pad’s text there once, then edit it together.", a11yStatus:"Status: {status}", a11ySize:"Effort: {size}", a11ySizeImplicit:"Effort: at least {size} (assumed)", a11yTags:"Assigned: {names}", a11yId:"ID: #{id}", a11yDeps:"depends on: {ids}", a11yFolded:"collapsed, {n} hidden", a11yEffective:"effective: {status}", heldTooltip:"effectively {eff} — itself already {own}, waiting on dependencies", a11yOptional:"optional", a11yFocusMark:"look here", a11yLink:"has link", hint_indent:"Indentation (2 spaces or a tab) defines the hierarchy.", hint_all:"sub-task, all required", hint_any:"alternative, choose one", @@ -2464,21 +2430,11 @@ const I18N = { acHint:"{n} sugerencias de ID – ↑/↓ elige, Intro inserta", tipClose:"Cerrar", tipOpenLink:"Abrir enlace", - padReadonly:"Se edita en el pad — aquí solo lectura.", - padEdit:"Abrir el pad para editar", - padRefresh:"Recargar desde el pad", liveLoadWarn:"Documento del servidor no cargado: {url} ({error}). ¿Está el backend en marcha y es la dirección la de un documento (…/documents/<uuid>)?", liveStaleWarn:"Tu cambio ya no era aplicable ({error}): se volvió a cargar el estado una vez.", liveConflictText:"Alguien cambió las mismas líneas. ¿Qué versión debe prevalecer?", liveConflictTheirs:"Tomar la ajena", liveConflictMine:"Mantener la mía", - padWait:"Faltan {seconds} s — Etherpad limita la frecuencia", - padRateLimitWarn:"Aún no se ha recargado: Etherpad solo permite unas pocas descargas por ventana de tiempo (10 por 90 s de forma predeterminada), así que sincronizar tan a menudo no es posible. Vuelve a intentarlo en {seconds} s.", - padViewTooltip:"Vista: {state} — clic para cambiar", - padView_both:"pad y texto", - padView_pad:"solo pad", - padView_text:"solo texto", - padGutterAria:"Redimensionar pad y editor de texto", riskTooltip:"Alto riesgo – esfuerzo aún incierto.", discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol", cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan", @@ -2549,7 +2505,7 @@ const I18N = { st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado", unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.", sourceLoadWarn:"No se pudo cargar «{url}» ({error}). El archivo debe ser accesible por http(s) y permitir CORS (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"«{url}» no respondió en {seconds} s — se canceló la petición. Etherpad limita la frecuencia de descarga del export (10 veces por 90 s de forma predeterminada); espera un momento y vuelve a cargar.", + padGoneWarn:"Este enlace apunta a un pad de Etherpad. La conexión con Etherpad ya no existe: ahora se colabora a través de un backend de Werkbaum (?live=…). Pega allí el texto del pad una vez y editadlo juntos.", a11yStatus:"Estado: {status}", a11ySize:"Esfuerzo: {size}", a11ySizeImplicit:"Esfuerzo: al menos {size} (asumido)", a11yTags:"Responsable: {names}", a11yId:"ID: #{id}", a11yDeps:"depende de: {ids}", a11yFolded:"plegado, {n} ocultos", a11yEffective:"efectivo: {status}", heldTooltip:"efectivamente {eff} — por sí mismo ya {own}, espera dependencias", a11yOptional:"opcional", a11yFocusMark:"mirar aquí", a11yLink:"con enlace", hint_indent:"La sangría (2 espacios o un tabulador) define la jerarquía.", hint_all:"subtarea, todas obligatorias", hint_any:"alternativa, elige una", @@ -2585,21 +2541,11 @@ const I18N = { acHint:"{n} suggestions d'ID – ↑/↓ pour choisir, Entrée pour insérer", tipClose:"Fermer", tipOpenLink:"Ouvrir le lien", - padReadonly:"Modifié dans le pad — lecture seule ici.", - padEdit:"Ouvrir le pad pour modifier", - padRefresh:"Recharger depuis le pad", liveLoadWarn:"Document du serveur non chargé : {url} ({error}). Le backend tourne-t-il, et l'adresse est-elle celle d'un document (…/documents/<uuid>) ?", liveStaleWarn:"Ta modification n'était plus applicable ({error}) — l'état a été rechargé une fois.", liveConflictText:"Quelqu'un a modifié les mêmes lignes. Quelle version doit l'emporter ?", liveConflictTheirs:"Prendre la sienne", liveConflictMine:"Garder la mienne", - padWait:"Encore {seconds} s — Etherpad limite la fréquence", - padRateLimitWarn:"Pas encore rechargé : Etherpad n’autorise que quelques récupérations par fenêtre de temps (10 par 90 s par défaut), une synchronisation aussi fréquente n’est donc pas possible. Réessaie dans {seconds} s.", - padViewTooltip:"Vue : {state} — cliquer pour changer", - padView_both:"pad et texte", - padView_pad:"pad seul", - padView_text:"texte seul", - padGutterAria:"Redimensionner le pad et l’éditeur de texte", riskTooltip:"Risque élevé – effort encore incertain.", discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre", cheapTooltip:"Mettre en évidence le chemin le moins coûteux – les alternatives inutiles s'estompent", @@ -2670,7 +2616,7 @@ const I18N = { st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné", unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.", sourceLoadWarn:"Impossible de charger « {url} » ({error}). Le fichier doit être accessible en http(s) et autoriser CORS (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"« {url} » n’a pas répondu en {seconds} s — la requête a été interrompue. Etherpad limite la fréquence de récupération de l’export (10 fois par 90 s par défaut) ; attends un instant, puis recharge.", + padGoneWarn:"Ce lien pointe vers un pad Etherpad. La connexion Etherpad n’existe plus — la collaboration passe désormais par un backend Werkbaum (?live=…). Collez-y une fois le texte du pad, puis modifiez-le à plusieurs.", a11yStatus:"Statut : {status}", a11ySize:"Effort : {size}", a11ySizeImplicit:"Effort : au moins {size} (supposé)", a11yTags:"Responsable : {names}", a11yId:"ID : #{id}", a11yDeps:"dépend de : {ids}", a11yFolded:"replié, {n} masqués", a11yEffective:"effectif : {status}", heldTooltip:"effectivement {eff} — lui-même déjà {own}, en attente de dépendances", a11yOptional:"facultatif", a11yFocusMark:"regarder ici", a11yLink:"avec lien", hint_indent:"L'indentation (2 espaces ou une tabulation) définit la hiérarchie.", hint_all:"sous-tâche, toutes requises", hint_any:"alternative, en choisir une", @@ -2706,21 +2652,11 @@ const I18N = { acHint:"{n} podpowiedzi ID – ↑/↓ wybiera, Enter wstawia", tipClose:"Zamknij", tipOpenLink:"Otwórz link", - padReadonly:"Edytowane w padzie — tu tylko do czytania.", - padEdit:"Otwórz pad do edycji", - padRefresh:"Wczytaj ponownie z padu", liveLoadWarn:"Nie wczytano dokumentu z serwera: {url} ({error}). Czy backend działa i czy adres wskazuje dokument (…/documents/<uuid>)?", liveStaleWarn:"Twoja zmiana nie dała się już zastosować ({error}) — stan pobrano raz od nowa.", liveConflictText:"Ktoś zmienił te same wiersze. Która wersja ma obowiązywać?", liveConflictTheirs:"Przyjmij cudzą", liveConflictMine:"Zachowaj własną", - padWait:"Jeszcze {seconds} s — Etherpad ogranicza częstość", - padRateLimitWarn:"Jeszcze nie wczytano ponownie: Etherpad dopuszcza tylko kilka pobrań w okresie (domyślnie 10 na 90 s), więc tak częsta synchronizacja nie jest możliwa. Spróbuj za {seconds} s.", - padViewTooltip:"Widok: {state} — kliknij, aby zmienić", - padView_both:"pad i tekst", - padView_pad:"tylko pad", - padView_text:"tylko tekst", - padGutterAria:"Zmień rozmiar padu i edytora tekstu", riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.", discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem", cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone", @@ -2791,7 +2727,7 @@ const I18N = { st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone", unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.", sourceLoadWarn:"Nie udało się wczytać „{url}” ({error}). Plik musi być dostępny przez http(s) i zezwalać na CORS (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"„{url}” nie odpowiedział w ciągu {seconds} s — żądanie przerwano. Etherpad ogranicza częstość pobierania eksportu (domyślnie 10 razy na 90 s); odczekaj chwilę i wczytaj ponownie.", + padGoneWarn:"Ten link prowadzi do pada Etherpad. Połączenia z Etherpadem już nie ma — wspólna praca odbywa się teraz przez backend Werkbaum (?live=…). Wklej tam raz tekst pada i edytujcie go razem.", a11yStatus:"Status: {status}", a11ySize:"Nakład: {size}", a11ySizeImplicit:"Nakład: co najmniej {size} (założony)", a11yTags:"Przypisano: {names}", a11yId:"ID: #{id}", a11yDeps:"zależy od: {ids}", a11yFolded:"zwinięte, ukrytych: {n}", a11yEffective:"efektywnie: {status}", heldTooltip:"efektywnie {eff} — sam już {own}, czeka na zależności", a11yOptional:"opcjonalny", a11yFocusMark:"spójrz tutaj", a11yLink:"z linkiem", hint_indent:"Wcięcie (2 spacje lub tabulator) definiuje hierarchię.", hint_all:"podzadanie, wszystkie wymagane", hint_any:"alternatywa, wybierz jedną", @@ -2827,21 +2763,11 @@ const I18N = { acHint:"{n} подсказок ID – ↑/↓ выбирает, Enter вставляет", tipClose:"Закрыть", tipOpenLink:"Открыть ссылку", - padReadonly:"Редактируется в паде — здесь только чтение.", - padEdit:"Открыть пад для редактирования", - padRefresh:"Обновить из пада", liveLoadWarn:"Документ с сервера не загружен: {url} ({error}). Запущен ли бэкенд и является ли адрес адресом документа (…/documents/<uuid>)?", liveStaleWarn:"Ваше изменение больше не применялось ({error}) — состояние загружено заново.", liveConflictText:"Кто-то изменил те же строки. Чья версия должна остаться?", liveConflictTheirs:"Принять чужую", liveConflictMine:"Оставить свою", - padWait:"Ещё {seconds} с — Etherpad ограничивает частоту", - padRateLimitWarn:"Пока не обновлено: Etherpad разрешает лишь несколько загрузок за окно времени (по умолчанию 10 за 90 с), поэтому столь частая синхронизация невозможна. Повторите через {seconds} с.", - padViewTooltip:"Вид: {state} — нажмите для переключения", - padView_both:"пад и текст", - padView_pad:"только пад", - padView_text:"только текст", - padGutterAria:"Изменить размер пада и текстового редактора", riskTooltip:"Высокий риск – оценка ещё не ясна.", discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом", cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются", @@ -2912,7 +2838,7 @@ const I18N = { st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено", unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.", sourceLoadWarn:"Не удалось загрузить «{url}» ({error}). Файл должен быть доступен по http(s) и разрешать CORS (Access-Control-Allow-Origin).", - sourceTimeoutWarn:"«{url}» не ответил за {seconds} с — запрос прерван. Etherpad ограничивает частоту загрузки экспорта (по умолчанию 10 раз за 90 с); подождите немного и обновите снова.", + padGoneWarn:"Эта ссылка ведёт на пад Etherpad. Подключения к Etherpad больше нет — совместная работа теперь идёт через бэкенд Werkbaum (?live=…). Вставьте туда текст пада один раз и редактируйте вместе.", a11yStatus:"Статус: {status}", a11ySize:"Оценка: {size}", a11ySizeImplicit:"Оценка: не меньше {size} (предполагается)", a11yTags:"Ответственные: {names}", a11yId:"ID: #{id}", a11yDeps:"зависит от: {ids}", a11yFolded:"свёрнуто, скрыто: {n}", a11yEffective:"фактически: {status}", heldTooltip:"фактически {eff} — сам уже {own}, ждёт зависимости", a11yOptional:"необязательно", a11yFocusMark:"смотрите здесь", a11yLink:"со ссылкой", hint_indent:"Отступ (2 пробела или табуляция) задаёт иерархию.", hint_all:"подзадача, все обязательны", hint_any:"альтернатива, выберите одну", @@ -2948,21 +2874,11 @@ const I18N = { acHint:"{n} आईडी सुझाव – ↑/↓ से चुनें, Enter से डालें", tipClose:"बंद करें", tipOpenLink:"लिंक खोलें", - padReadonly:"पैड में संपादित होता है — यहाँ केवल पढ़ें।", - padEdit:"संपादित करने के लिए पैड खोलें", - padRefresh:"पैड से फिर लोड करें", liveLoadWarn:"सर्वर दस्तावेज़ लोड नहीं हुआ: {url} ({error})। क्या बैकएंड चल रहा है और क्या पता दस्तावेज़ का पता है (…/documents/<uuid>)?", liveStaleWarn:"आपका बदलाव अब लागू नहीं हो सका ({error}) — स्थिति एक बार नए सिरे से ली गई।", liveConflictText:"किसी और ने वही पंक्तियाँ बदली हैं। किसका संस्करण रहे?", liveConflictTheirs:"दूसरे का लें", liveConflictMine:"अपना रखें", - padWait:"{seconds} स॰ बाकी — Etherpad बार-बार लेने की सीमा रखता है", - padRateLimitWarn:"अभी दोबारा लोड नहीं किया: Etherpad प्रति समय-खिड़की केवल कुछ ही बार लेने देता है (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार), इसलिए इतनी बार सिंक करना संभव नहीं है। {seconds} स॰ में फिर कोशिश करें।", - padViewTooltip:"दृश्य: {state} — बदलने के लिए क्लिक करें", - padView_both:"पैड और टेक्स्ट", - padView_pad:"केवल पैड", - padView_text:"केवल टेक्स्ट", - padGutterAria:"पैड और टेक्स्ट संपादक का आकार बदलें", riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।", discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ", cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं", @@ -3033,7 +2949,7 @@ const I18N = { st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत", unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।", sourceLoadWarn:"„{url}“ लोड नहीं हो सका ({error})। फ़ाइल http(s) से उपलब्ध होनी चाहिए और CORS की अनुमति देनी चाहिए (Access-Control-Allow-Origin)।", - sourceTimeoutWarn:"„{url}“ ने {seconds} स॰ में उत्तर नहीं दिया — अनुरोध रद्द कर दिया गया। Etherpad सीमित करता है कि एक्सपोर्ट कितनी बार लिया जा सके (डिफ़ॉल्ट रूप से 90 स॰ में 10 बार); कुछ क्षण रुकें, फिर दोबारा लोड करें।", + padGoneWarn:"यह लिंक एक Etherpad पैड की ओर इशारा करता है। Etherpad कनेक्शन अब नहीं है — साझा काम अब Werkbaum बैकएंड (?live=…) से होता है। पैड का टेक्स्ट वहाँ एक बार चिपकाएँ और मिलकर संपादित करें।", a11yStatus:"स्थिति: {status}", a11ySize:"आकार: {size}", a11ySizeImplicit:"आकार: कम से कम {size} (अनुमानित)", a11yTags:"जिम्मेदार: {names}", a11yId:"आईडी: #{id}", a11yDeps:"निर्भर: {ids}", a11yFolded:"समेटा हुआ, {n} छिपे", a11yEffective:"प्रभावी: {status}", heldTooltip:"प्रभावी रूप से {eff} — स्वयं {own} है, निर्भरताओं की प्रतीक्षा में", a11yOptional:"वैकल्पिक", a11yFocusMark:"यहाँ देखें", a11yLink:"लिंक सहित", hint_indent:"इंडेंट (2 स्पेस या टैब) पदानुक्रम तय करता है।", hint_all:"उप-कार्य, सभी आवश्यक", hint_any:"विकल्प, एक चुनें", @@ -3080,21 +2996,11 @@ const I18N = { acHint:"{n} 个 ID 建议 – ↑/↓ 选择,Enter 插入", tipClose:"关闭", tipOpenLink:"打开链接", - padReadonly:"在 Pad 中编辑 — 此处只读。", - padEdit:"打开 Pad 进行编辑", - padRefresh:"从 Pad 重新加载", liveLoadWarn:"未能加载服务器文档:{url}({error})。后端在运行吗?该地址是文档地址(…/documents/<uuid>)吗?", liveStaleWarn:"你的更改已无法应用({error})——已重新获取一次当前状态。", liveConflictText:"有人改动了同样的行。以谁的版本为准?", liveConflictTheirs:"采用对方的", liveConflictMine:"保留我的", - padWait:"还需 {seconds} 秒 — Etherpad 限制获取频率", - padRateLimitWarn:"尚未重新加载:Etherpad 每个时间窗口只允许少量获取(默认每 90 秒 10 次),因此无法如此频繁地同步。请在 {seconds} 秒后再试。", - padViewTooltip:"视图:{state} — 点击切换", - padView_both:"Pad 和文本", - padView_pad:"仅 Pad", - padView_text:"仅文本", - padGutterAria:"调整 Pad 与文本编辑器大小", riskTooltip:"高风险 – 工作量尚不明确。", editorTitle:"结构(文本)", diagramTitle:"图表", docSwitchTooltip:"选择或管理文档", docMenuAria:"文档", @@ -3154,7 +3060,7 @@ const I18N = { st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃", unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。", sourceLoadWarn:"无法加载“{url}”({error})。该文件必须可通过 http(s) 访问并允许 CORS(Access-Control-Allow-Origin)。", - sourceTimeoutWarn:"“{url}” 在 {seconds} 秒内没有响应 — 请求已中止。Etherpad 会限制导出的获取频率(默认每 90 秒 10 次);请稍候再重新加载。", + padGoneWarn:"此链接指向一个 Etherpad pad。Etherpad 连接已移除——协作现在通过 Werkbaum 后端(?live=…)进行。把 pad 的文本粘贴过去一次,然后一起编辑。", a11yStatus:"状态:{status}", a11ySize:"工作量:{size}", a11ySizeImplicit:"工作量:至少 {size}(假定)", a11yTags:"负责人:{names}", a11yId:"ID:#{id}", a11yDeps:"依赖:{ids}", a11yFolded:"已折叠,隐藏 {n} 项", a11yEffective:"实际:{status}", heldTooltip:"实际为 {eff}——自身已是 {own},等待依赖完成", a11yOptional:"可选", a11yFocusMark:"看这里", a11yLink:"含链接", hint_indent:"缩进(2 个空格或制表符)定义层级。", hint_all:"子任务,全部必需", hint_any:"备选项,择其一", @@ -3201,21 +3107,11 @@ const I18N = { acHint:"ID候補 {n} 件 – ↑/↓で選択、Enterで挿入", tipClose:"閉じる", tipOpenLink:"リンクを開く", - padReadonly:"パッドで編集します — ここでは読み取り専用です。", - padEdit:"編集するにはパッドを開く", - padRefresh:"パッドから再読み込み", liveLoadWarn:"サーバー文書を読み込めませんでした: {url}({error})。バックエンドは動いていますか。アドレスは文書のアドレス(…/documents/<uuid>)ですか。", liveStaleWarn:"あなたの変更はもう適用できませんでした({error})。状態を一度取り直しました。", liveConflictText:"同じ行が他の人にも変更されました。どちらの版を採りますか。", liveConflictTheirs:"相手の版", liveConflictMine:"自分の版", - padWait:"あと {seconds} 秒 — Etherpad は取得頻度を制限します", - padRateLimitWarn:"まだ再読み込みしていません: Etherpad は一定時間内の取得回数を制限します(既定で 90 秒あたり 10 回)。これほど頻繁な同期はできません。{seconds} 秒後にもう一度お試しください。", - padViewTooltip:"表示: {state} — クリックで切り替え", - padView_both:"パッドとテキスト", - padView_pad:"パッドのみ", - padView_text:"テキストのみ", - padGutterAria:"パッドとテキストエディターのサイズを変更", riskTooltip:"高リスク – 規模はまだ不明。", editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム", docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント", @@ -3275,7 +3171,7 @@ const I18N = { st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄", unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。", sourceLoadWarn:"「{url}」を読み込めませんでした({error})。ファイルは http(s) でアクセス可能で、CORS(Access-Control-Allow-Origin)を許可する必要があります。", - sourceTimeoutWarn:"「{url}」が {seconds} 秒以内に応答しませんでした — 要求を中止しました。Etherpad はエクスポートの取得回数を制限します(既定で 90 秒あたり 10 回)。少し待ってから再読み込みしてください。", + padGoneWarn:"このリンクは Etherpad のパッドを指しています。Etherpad 連携は廃止されました。共同編集は Werkbaum バックエンド(?live=…)で行います。パッドの本文を一度貼り付ければ、複数人で編集できます。", a11yStatus:"ステータス: {status}", a11ySize:"規模: {size}", a11ySizeImplicit:"規模: 少なくとも {size}(想定)", a11yTags:"担当: {names}", a11yId:"ID: #{id}", a11yDeps:"依存先: {ids}", a11yFolded:"折りたたみ中、{n} 件非表示", a11yEffective:"実効: {status}", heldTooltip:"実効では {eff} — 自身は既に {own}、依存待ち", a11yOptional:"任意", a11yFocusMark:"ここを見る", a11yLink:"リンクあり", hint_indent:"インデント(スペース2つまたはタブ)で階層を定義します。", hint_all:"サブタスク、すべて必須", hint_any:"選択肢、1つを選ぶ", @@ -3555,11 +3451,6 @@ function saveUI(){ agenda: !!document.querySelector('.agenda.open'), hcol: app.style.getPropertyValue('--hcol') || null, hrow: app.style.getPropertyValue('--hrow') || null, - /* Pad-Ansicht + Aufteilung Pad|Spiegel (D31). Global über alle Dokumente, - wie der übrige Ansichts-Zustand (D22). */ - padView: padView, - pcol: app.style.getPropertyValue('--pcol') || null, - prow: app.style.getPropertyValue('--prow') || null, /* Sichtbarer Bereich auf kleinem Bildschirm (D17-Nachtrag) — ebenfalls global über alle Dokumente wie der übrige Ansichts-Zustand. */ mobilePane: mobilePane, @@ -3609,9 +3500,6 @@ function restoreState(){ if(ui && ui.hcol) app.style.setProperty('--hcol', ui.hcol); if(ui && ui.hrow) app.style.setProperty('--hrow', ui.hrow); if(ui && (ui.mobilePane === 'diagram' || ui.mobilePane === 'text')) mobilePane = ui.mobilePane; - if(ui && PAD_VIEWS.indexOf(ui.padView) >= 0) padView = ui.padView; - if(ui && ui.pcol) app.style.setProperty('--pcol', ui.pcol); - if(ui && ui.prow) app.style.setProperty('--prow', ui.prow); setAgendaOpen(!!(ui && ui.agenda)); applyZoom(); restoring = false; @@ -3633,7 +3521,6 @@ function updateDocName(){ ? d.source + '\n' + t('docSwitchTooltip') : t('docSwitchTooltip'); } - updatePadLink(); /* Schreibschutz + Pad-Knopf hängen am aktiven Dokument (D31) */ } let renamingId = null; /* id des gerade inline umbenannten Dokuments (oder null) */ /* Kam das Umbenennen von einem frisch angelegten Dokument? Dann geht es danach @@ -3741,9 +3628,8 @@ function flushActive(){ const d = activeDoc(); if(d) d.text = src.value; } wer weiter zurück will, hat Git. Die Regeln stehen in snapshots.js und sind dort getestet; hier bleibt nur, - was DOM oder Speicher berührt: welches Dokument aktiv ist, ob sein Text - beschreibbar ist (Pad-Dokumente, D31, bleiben außen vor — ein alter Stand - ließe sich dort gar nicht einsetzen), und das Nachzeichnen des Menüs. */ + was DOM oder Speicher berührt: welches Dokument aktiv ist und das + Nachzeichnen des Menüs. */ let snaps = {}; /* {docId: [{t, text}, …]} — ältester zuerst */ let snapBase = ''; /* Text bei Dokumentwechsel; Vergleich, solange es keinen Stand gibt */ @@ -3751,7 +3637,7 @@ function loadSnaps(){ snaps = parseSnaps(localStorage.getItem(LS_SNAPS)); } function snapshotNow(manuell){ const d = activeDoc(); - if(!d || src.readOnly) return false; + if(!d) return false; const text = src.value; if(!addSnapshot(snaps, d.id, text, Date.now(), {base: snapBase, manual: manuell})) return false; snapBase = text; @@ -3796,7 +3682,6 @@ function renderSnapMenu(){ keine Rückfrage. Der aktuelle Stand wird vorher weggelegt, falls er noch nicht drin ist — sonst wäre er das Einzige, was der Griff verlöre. */ function loadSnapshot(s){ - if(src.readOnly) return; snapshotNow(true); /* bewusstes Wegleg-Ereignis wie der Knopf, nicht der Takt */ closeSnapMenu(); if(!replaceTextUndoable(s.text)) render(); @@ -3811,10 +3696,6 @@ function closeSnapMenu(){ snapMenu.hidden = true; snapBtn.setAttribute('aria-expanded', 'false'); } -function updateSnapBtn(){ - snapBtn.hidden = snapAddBtn.hidden = src.readOnly; - if(src.readOnly) closeSnapMenu(); -} snapBtn.addEventListener('click', e => { e.stopPropagation(); snapMenu.hidden ? openSnapMenu() : closeSnapMenu(); @@ -4010,7 +3891,7 @@ document.addEventListener('pointerdown', e => { function loadActiveIntoEditor(){ const d = activeDoc(); src.value = d ? d.text : ''; /* Vergleichsstand für den nächsten Snapshot (D54): Ohne ihn legte der erste Takt nach dem Öffnen auch ein unverändertes Dokument weg. */ - snapBase = src.value; closeSnapMenu(); updateSnapBtn(); + snapBase = src.value; closeSnapMenu(); render(); updateDocName(); updateFreshBtn(); } function switchDoc(id){ if(id === activeId) return; @@ -4072,7 +3953,6 @@ function deleteDoc(){ const d = activeDoc(); if(!d) return; if(!window.confirm(t('docDeleteConfirm', {name: d.name}))) return; - if(padSource && padSource.id === d.id) stopPad(); /* danach gibt es nichts mehr zu holen (D31) */ if(liveState && liveState.id === d.id) stopLive(); /* dito fürs Server-Dokument (D76) */ if(fileHandles.has(d.id)){ fileHandles.delete(d.id); idbDeleteHandle(d.id); } /* mit dem Dokument geht sein Datei-Handle (D72) */ docs = docs.filter(x => x.id !== d.id); @@ -4336,24 +4216,21 @@ function initDocs(){ src.value = d ? d.text : ''; snapBase = src.value; restoring = false; - updateSnapBtn(); persistDocs(); /* migrierte/geladene Liste festschreiben (stabil über Reload) */ updateDocName(); render(); } -/* ---------- Text von außen: ?sourceUrl= (D23) und ?etherpad= (D31) ---------- - Beide holen einen Notationstext über http(s) und führen ihn als eigenes - Dokument. Die id leitet sich aus der URL ab: derselbe Link aktualisiert dieses - Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des - Nutzers bleiben unberührt. Scheitert das Laden (häufigster Fall: das Ziel - sendet keinen CORS-Header), bleibt der bisherige Stand stehen und es - erscheint eine Warnung. +/* ---------- Text von außen: ?sourceUrl= (D23) ---------- + Holt einen Notationstext über http(s) und führt ihn als eigenes Dokument. Die + id leitet sich aus der URL ab: derselbe Link aktualisiert dieses Dokument, + statt bei jedem Aufruf ein neues anzulegen. Eigene Dokumente des Nutzers + bleiben unberührt. Scheitert das Laden (häufigster Fall: das Ziel sendet + keinen CORS-Header), bleibt der bisherige Stand stehen und es erscheint eine + Warnung. Eine statische Datei, einmal pro Laden geholt. - Unterschied: `sourceUrl` ist eine statische Datei, einmal pro Laden geholt - (D23, unverändert). `etherpad` ist ein lebendes Pad — wiederholt geholt und - hier schreibgeschützt, weil das Zusammenführen gleichzeitiger Änderungen - Etherpads Aufgabe ist. Ein Fetch-Pfad, zwei Eingänge. */ + `?etherpad=` gab es hier einmal daneben (D31) und ist ausgebaut (D78) — ein + alter Link meldet sich, statt still nichts zu tun. */ const SOURCE_PARAM = 'sourceUrl'; const ETHERPAD_PARAM = 'etherpad'; function urlParam(name){ @@ -4361,16 +4238,14 @@ function urlParam(name){ } function sourceUrlParam(){ return urlParam(SOURCE_PARAM); } -/* Woher kommt der Text? null = kein Parameter, {bad,error} = unbrauchbare - Angabe, sonst der Beschreiber für loadRemoteSource(). Die Normalisierung der - Pad-Adresse steht headless in remote.js — dort auch ihre Begründung. */ +/* Woher kommt der Text? null = kein Parameter, {gone} = ausgebauter Eingang, + {bad,error} = unbrauchbare Angabe, sonst der Beschreiber für + loadRemoteSource(). */ function remoteSource(){ - const padRaw = urlParam(ETHERPAD_PARAM); - if(padRaw){ - const p = padUrls(padRaw, location.href); - if(!p) return {bad: padRaw, error: 'not an Etherpad URL'}; - return {fetchUrl: p.text, id: 'url:' + p.pad, name: p.pad, source: p.pad, live: true}; - } + /* Ein alter `?etherpad=`-Link soll nicht stillschweigend ein leeres + Werkbaum zeigen: Wer ihn irgendwo stehen hat, erfährt hier, wohin die + Zusammenarbeit gezogen ist (D78). */ + if(urlParam(ETHERPAD_PARAM)) return {gone: true}; const raw = sourceUrlParam(); if(!raw) return null; let url; @@ -4380,25 +4255,15 @@ function remoteSource(){ („Failed to fetch", „HTTP 404") — der Rahmentext ist lokalisiert. */ try{ url = new URL(raw, location.href); }catch(_){ return {bad: raw, error: 'invalid URL'}; } if(url.protocol !== 'http:' && url.protocol !== 'https:') return {bad: url.href, error: url.protocol}; - return {fetchUrl: url.href, id: 'url:' + url.href, name: url.href, source: url.href, live: false}; + return {fetchUrl: url.href, id: 'url:' + url.href, name: url.href, source: url.href}; } -/* `timeoutMs` nur beim Pad-Takt (D31): Hängt die Gegenseite, muss der Abruf - abbrechen, sonst bliebe der Riegel `padBusy` für immer zu und es käme nie - wieder etwas. Der erste Ladeversuch wartet unbegrenzt wie bisher (D23). */ -async function fetchRemote(url, timeoutMs){ - const ctl = timeoutMs ? new AbortController() : null; - const timer = ctl ? setTimeout(() => ctl.abort(), timeoutMs) : null; - try{ - const resp = await fetch(url, {cache:'no-store', credentials:'omit', - signal: ctl ? ctl.signal : undefined}); - if(!resp.ok) throw new Error('HTTP ' + resp.status); - return await resp.text(); - } finally { if(timer) clearTimeout(timer); } +async function fetchRemote(url){ + const resp = await fetch(url, {cache:'no-store', credentials:'omit'}); + if(!resp.ok) throw new Error('HTTP ' + resp.status); + return await resp.text(); } -/* Geholten Text als Dokument übernehmen und aktivieren. Gemeinsam genutzt vom - ersten Ladeversuch und vom Pad-Takt, wenn dieser einen gescheiterten ersten - Versuch nachholt — sonst gäbe es zwei Fassungen derselben sechs Zeilen. */ +/* Geholten Text als Dokument übernehmen und aktivieren. */ function adoptRemote(s, text){ flushActive(); /* laufende Bearbeitung nicht verlieren */ let d = docs.find(x => x.id === s.id); @@ -4414,28 +4279,23 @@ function adoptRemote(s, text){ async function loadRemoteSource(){ const s = remoteSource(); if(!s) return; + if(s.gone){ + sourceWarning = {type:'padGone'}; + render(); + return; + } if(s.bad !== undefined){ sourceWarning = {type:'sourceLoad', url: s.bad, error: s.error}; render(); return; } - /* `padSource` **vor** dem ersten Abruf setzen: Es trägt den Schreibschutz - (muss also vor loadActiveIntoEditor() stehen) und macht den Neu-laden-Knopf - erreichbar. Scheitert der erste Abruf — die Gegenseite fällt beobachtbar mal - aus —, bleibt der Knopf also da und man kommt ohne Neuladen weiter. */ - if(s.live){ padSource = s; padBusy = true; setPadCooldown(PAD_MIN_GAP_MS); } try{ adoptRemote(s, await fetchRemote(s.fetchUrl)); }catch(err){ /* CORS-Fehler melden sich als „TypeError: Failed to fetch" ohne Details — - der Warntext nennt CORS daher ausdrücklich als wahrscheinliche Ursache. - Bei einem Pad ist das nicht das Ende: der Knopf holt es nach und räumt die - Warnung weg, sobald ein Abruf gelingt. */ + der Warntext nennt CORS daher ausdrücklich als wahrscheinliche Ursache. */ sourceWarning = {type:'sourceLoad', url: s.fetchUrl, error: (err && err.message) || String(err)}; - updatePadLink(); /* Knopf zeigen, obwohl kein Dokument entstanden ist */ render(); - }finally{ - if(s.live) padBusy = false; } } @@ -4886,204 +4746,6 @@ async function putOnServer(){ } } -/* ---------- Pad: auf Knopfdruck neu holen (D31) ---------- - Kein Hintergrund-Takt. Der erste Entwurf holte alle 2,5 s selbsttätig — und - lief damit in Etherpads **Drosselung**: `importExportRateLimiting` ist - serienmäßig an und lässt 10 Abrufe je 90 s und IP zu, der Takt wollte 36. - Danach hält die Gegenseite die Verbindung einfach offen (kein `429`, keine - Kopfzeilen), der Abbruch reißt sie ab — im Netzwerk-Mitschnitt „cancelled", - und es kommt kaum je etwas an. Ein Knopf ist ohnehin ehrlicher gegenüber - fremder Infrastruktur: geholt wird, wenn jemand es will. Zusammen mit - „Was ist neu?" (D28) ergibt das die bessere Geschichte — Knopf drücken, und - was seither in Produktion ging, leuchtet auf. */ -const PAD_FETCH_TIMEOUT_MS = 20000; -/* Mindestabstand zwischen zwei Abrufen. 10 s ergibt höchstens **9** Abrufe je - 90 s und bleibt damit beweisbar unter Etherpads Voreinstellung (10 je 90 s) — - die Drosselung wird also gar nicht erst ausgelöst. Nach einem Abbruch sind wir - schon drüber: dann das ganze Fenster abwarten, statt weiter dagegen zu rennen. */ -const PAD_MIN_GAP_MS = 10000; -const PAD_BACKOFF_MS = 90000; -let padSource = null; /* Beschreiber der aktiven Pad-Quelle, oder null */ -let padBusy = false; /* ein Abruf unterwegs — schützt vor Doppelklick */ -let padNextAllowed = 0; /* Zeitpunkt (ms), ab dem wieder geholt werden darf */ -let padCoolTimer = null; -/* Wer das Pad-Dokument löscht, meint es: danach gibt es nichts mehr zu holen. */ -function stopPad(){ padSource = null; } -function padCoolLeft(){ return Math.max(0, padNextAllowed - Date.now()); } -function setPadCooldown(ms){ padNextAllowed = Date.now() + ms; tickPadCooldown(); } -/* Der Sekundenzähler läuft **nur** während der Sperre und hält sich selbst an. */ -function tickPadCooldown(){ - if(padCoolTimer){ clearInterval(padCoolTimer); padCoolTimer = null; } - updatePadRefreshLabel(); - if(padCoolLeft() <= 0) return; - padCoolTimer = setInterval(() => { - updatePadRefreshLabel(); - if(padCoolLeft() <= 0){ clearInterval(padCoolTimer); padCoolTimer = null; } - }, 1000); -} -/* Sperre sichtbar machen, aber den Knopf **klickbar** lassen: Ein `disabled` - erklärt nichts. So kann der Klick den Grund melden (Live-Region `#warn`). */ -function updatePadRefreshLabel(){ - if(!padRefreshBtn || padRefreshBtn.hidden) return; - const left = Math.ceil(padCoolLeft()/1000); - padRefreshBtn.classList.toggle('cooling', left > 0); - padRefreshBtn.setAttribute('aria-disabled', left > 0 ? 'true' : 'false'); - const tip = left > 0 ? t('padWait', {seconds: left}) : t('padRefresh'); - padRefreshBtn.title = tip; - padRefreshBtn.setAttribute('aria-label', tip); -} -/* Rückmeldung während des Abrufs: Bei einer gedrosselten Gegenseite können das - die vollen 20 s sein — ohne Zeichen wirkt der Knopf kaputt. */ -function setPadBusy(on){ - if(!padRefreshBtn) return; - padRefreshBtn.classList.toggle('busy', on); - padRefreshBtn.setAttribute('aria-busy', on ? 'true' : 'false'); -} -async function refreshPad(){ - const s = padSource; - if(!s || padBusy) return; - if(padCoolLeft() > 0){ - /* Nicht heimlich nichts tun: Der Grund ist eine fremde Grenze, und die - gehört gesagt — sonst wirkt der Knopf kaputt. Die Meldung landet im - Warnbereich, der eine Live-Region ist und deshalb angesagt wird. */ - sourceWarning = {type:'padRateLimit', seconds: Math.ceil(padCoolLeft()/1000)}; - render(); - return; - } - padBusy = true; - setPadBusy(true); - setPadCooldown(PAD_MIN_GAP_MS); - let text; - try{ text = await fetchRemote(s.fetchUrl, PAD_FETCH_TIMEOUT_MS); } - catch(err){ - /* Anders als ein Hintergrund-Takt ist das eine bewusste Handlung — sie - braucht eine Antwort, auch wenn sie schiefgeht. Ein Abbruch bekommt einen - **eigenen** Warnungstyp: `sourceLoad` zeigt auf CORS, und bei einer - Drosselung schickte das den Leser auf die falsche Fährte. */ - if(err && err.name === 'AbortError'){ - /* Abbruch heißt: Wir sind schon in der Drosselung. Weiterklicken hilft - nicht, also das ganze Fenster abwarten — und das dem Nutzer sagen. */ - setPadCooldown(PAD_BACKOFF_MS); - sourceWarning = {type:'sourceTimeout', url: s.fetchUrl, - seconds: Math.round(PAD_FETCH_TIMEOUT_MS/1000)}; - } else { - sourceWarning = {type:'sourceLoad', url: s.fetchUrl, error: (err && err.message) || String(err)}; - } - render(); - return; - } - finally{ padBusy = false; setPadBusy(false); } - if(padRefreshBtn) flashBtn(padRefreshBtn); - const d = docs.find(x => x.id === s.id); - if(!d){ - /* Kein Dokument: der erste Ladeversuch ist gescheitert. Jetzt nachholen — - anlegen und aktivieren wie beim Laden. */ - adoptRemote(s, text); - return; - } - /* Ein geglückter Abruf räumt die Warnung des ersten Versuchs weg — sonst - stünde „konnte nicht geladen werden", während der Text längst da ist. */ - const hadWarning = !!sourceWarning; - sourceWarning = null; - if(text === d.text){ if(hadWarning) render(); return; } - d.text = text; - persistDocs(); - if(activeId !== d.id) return; /* im Hintergrund still aktualisiert */ - /* Auswahl und Scrollstand erhalten — der Sprung aus dem Diagramm (D25) und - die Cursor-Zeile sollen einen Abruf überleben. */ - const top = src.scrollTop, a = src.selectionStart, b = src.selectionEnd; - src.value = text; - try{ src.setSelectionRange(a, b); }catch(_){} - src.scrollTop = top; - computeFresh(d.id, text); /* Basis bleibt die zuletzt bestätigte Fassung */ - render(); - updateFreshBtn(); -} - -/* ---------- Ansicht bei einem Pad-Dokument: beide | nur Pad | nur Spiegel ---- - Das Pad lässt sich einbetten (nachgemessen: kein `X-Frame-Options`, keine CSP - mit `frame-ancestors`), doch es ersetzt nicht einfach das Textfeld: Alt+Klick - und die Cursor-Zeile (D25) arbeiten auf **unserem** `