Zuhause ist künftig git.javagil.de/mi/werkbaum (origin); GitHub bleibt unter
dem Remote `github` ein Klon, den scripts/push-github.sh mit `main` versorgt —
nur vorwärts, nie erzwungen.
Der Klon bleibt aus zwei gemessenen Gründen: Gitea sendet auf raw-Dateien kein
Access-Control-Allow-Origin (nachgemessen an mi/werkdock), die
?sourceUrl=-Beispiellinks der READMEs müssen also auf raw.githubusercontent.com
zeigen; und der Pages-Workflow lässt sich nicht mitnehmen.
Footer, llms.txt und llms.md zeigen jetzt auf Gitea (Repo-Seite ohne Anmeldung
lesbar, HTTP 200 geprüft); beide COMMIT_URL-Stellen (deploy-prod.sh und der
Pages-Workflow) ziehen mit. deploy-prod.sh prüft origin und github getrennt und
erinnert an das Spiegeln, statt selbst zu pushen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nutzer-Einwand, folgerichtig aus dem Vorgänger-Commit: Wenn Falten eine
Änderung ist, muss der Nur-Ansehen-Modus auch das Diagramm stillstellen —
im haltenden Fenster natürlich weiter nicht.
Es war schlimmer als eine fehlende Sperre: `readonly` hielt gar nichts auf.
`replaceTextUndoable` fällt auf `src.value = neu` zurück, wenn `execCommand`
scheitert, und eine Zuweisung an `value` schreibt auch in ein
schreibgeschütztes Feld. Nachgemessen: Ein Klick auf das Falt-Zeichen im
Nur-Ansehen-Fenster setzte `- > Ast A (M)` in Textfeld UND Speicher — der
Modus war nur eine Beschriftung.
Gesperrt wird jetzt an der Schreibstelle: eine Zeile in
`replaceTextUndoable` weist jeden programmatischen Schreibzugriff ab
(Falten, Durchschalter, „aus Taiga übernehmen", früherer Stand,
ID-Kurzform). Dazu ruhen die Gesten selbst, damit nichts still ins Leere
geht — toggleFold und der Durchschalter kehren früh um, Falt-Zeichen und
Knopf treten zurück und nehmen keinen Klick an (body.viewonly), und im
Knoten-Fenster entfallen die Aktionen, die in die Zeile schreiben.
„Nach Taiga schreiben" bleibt: Es fasst den Plan nicht an. Ansicht bleibt
Ansicht — Zoom, Modus, Pfad, Export, Sprünge unverändert bedienbar.
Nachgemessen mit zwei Fenstern: Chip, Tastatur und Durchschalter lassen im
Nur-Ansehen-Fenster Text und Speicher unangetastet; im haltenden Fenster
faltet dieselbe Geste weiter; verlässt es das Dokument, wird das andere von
selbst wieder beschreibbar und faltbar. SPEC §9, D94-Nachtrag 3 und
CHANGELOG nachgezogen; 666 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nutzer-Rückfrage: „Was für ein letzter Tastendruck?" — die Formulierung
liest sich, als zähle nur Tippen. Tut es nicht: Umklappen im Diagramm
schreibt die Faltmarke in den Text (D38-Nachtrag 2) und damit in den
Speicher, nimmt also am selben Wettlauf teil. Nachgemessen mit zwei
Fenstern: A klappt zu, im Speicher steht `- > Ast A (M)`, B's Editor und
Diagramm bleiben unverändert — und B's nächster Tastendruck überschreibt
den Speicher, A's Faltung ist weg.
Beide Texte in allen neun Sprachen umformuliert (tabConflictWarn,
docLockText); der Dialogtext nennt das Falten ausdrücklich. SPEC §9 sagt
jetzt dazu, dass das andere Fenster nichts nachzieht und dass ein
Dokumentwechsel dort nicht mitwandert — `werkbaum-active` entscheidet erst
wieder, was ein Neustart öffnet (auch das eine Nutzer-Rückfrage,
nachgemessen). D94-Nachtrag 2 und CHANGELOG nachgezogen.
Benannt, nicht behoben: Wer „trotzdem hier bearbeiten" gewählt hat, behält
die Warnung, auch wenn das andere Fenster das Dokument längst verlassen
hat — dieser Ausweg fordert die Sperre bewusst nicht mehr an.
666 Tests grün; die neuen Texte im laufenden Editor nachgesehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemeldet aus der produktiven Instanz: „die Buttons mit den anderen
Dokumenten überlappen sich und sind unleserlich". Ursache ist eine
Flexbox-Falle, nicht die Zahl der Dokumente: Die Liste war ein
flex-Spaltenkasten mit max-height 9rem, und ihre Knöpfe tragen serienmäßig
flex-shrink 1 — statt zu scrollen, schrumpfte der Kasten sie unter ihre
eigene Textzeile. Nachgemessen bei acht Dokumenten: 14,5 px Kastenhöhe
gegen 32 px Inhalt, 17,5 px Überstand je Knopf bei 4 px Abstand.
Statt der naheliegenden Reparatur (flex-shrink 0, dann scrollt die Liste)
das vom Nutzer gewählte Aufklapp-Feld: eine Zeile hoch, unabhängig von der
Anzahl — der Dialog steckt im Editor-Panel und ist dort ohnehin knapp. Die
Kopfzeile ist jetzt ein echtes <label for>, der Platzhalter ein Strich,
die Auswahl schaltet sofort um (dieselbe Ein-Klick-Geste wie der Knopf).
Kein neuer i18n-Schlüssel.
Nachgemessen: Feld 36 px, kein Überstand, Karte 364 -> 256 px; Auswahl
schaltet um, Dialog schließt, Textfeld wird beschreibbar. D94-Nachtrag und
CHANGELOG nachgezogen; 666 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kein Fenster schreibt mehr den Schlüssel eines anderen: Text, Meta und
frühere Stände liegen je Dokument unter eigenen Schlüsseln, der Index ist
nur noch Reihenfolge-Hinweis und löscht nie, Löschungen hinterlassen einen
Tombstone (7 Tage). Der Voll-Flush weicht dem Dirty-Flush. Damit sind
Befund 2 und 3 des RFC weg — verschiedene Dokumente in zwei Fenstern
kollidieren gar nicht mehr. docsync.js zieht Liste, Namen, Tombstones und
den Stände-Cache aus dem storage-Ereignis nach; am aktiven Dokument die
drei Fälle umbenannt / anderswo gelöscht (behalten, bis getippt wird) /
fremd geschrieben. Der modale D89-Dialog samt Präsenz-Kanal ist ersatzlos
raus; den einen Verlustfall — dasselbe nicht-`live:`-Dokument in beiden
Fenstern vorn — findet eine Sperre per Web Locks, und das zweite Fenster
bekommt drei Auswege statt „trotzdem fortfahren".
Im Browser mit zwei echten Tabs nachgemessen (RFC §10): Fälle 1–8 grün,
Fall 1 im Vor-v3-Build gegengeprüft und dort nachweislich rot. Fall 9
deckte auf, dass reviveGoneDoc() den Index-Hinweis nicht mitschrieb — ein
durch Tippen wiederbelebtes Dokument hing bis zum nächsten Flush allein an
seinen eigenen Schlüsseln und wäre bei einem Rückbau auf einen Build vor
v3 still verlorengegangen; behoben, Gegenprobe per Mutation gezogen.
Headless 666 Tests grün. Offen bleibt die Handarbeit: PWA neben Tab,
Firefox, Safari, ein Browser ohne Locks-API.
SPEC §9, D94, D89-Nachtrag, RFC (Status, §10, Revisionsgeschichte),
CHANGELOG und Plan-Knoten #ed.docs.windows nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der modale Zwei-Fenster-Dialog (D89) verbietet den Fall, der funktioniert
(dasselbe geteilte Dokument in App und Tab), und schützt nicht vor dem,
wovor er warnt: Der Voll-Flush löscht Dokumente und Stände des anderen
Fensters. Entschieden: je Dokument eigene Schlüssel, Tombstones, Sperre je
Dokument per Web Locks, Dialog mit drei Auswegen. Nichts gebaut.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nutzer-FYI: Node.js einzuführen ist eine Dimension schwerer als eine
Backend-Abhängigkeit und muss dem Entwickler deutlich gemacht werden —
als Regel in CLAUDE.md. Aus den nachgefragten Beweggründen folgt: kein
lokaler Prozess. Der MCP-Server lebt im Backend (Streamable HTTP,
Bearer-Token), führt die Frontend-Module per GraalJS aus, jedes Werkzeug
hat eine text- und eine document-Form; das Node-Paket ist verworfen,
seine Tool-Schicht lebt als frontend/src/inspect.js, guard.js, edit.js.
Backend-Abhängigkeiten GraalJS + Spring-AI-Starter zugestimmt; der
GraalJS-Spike steht vor allem anderen. RFC neu geschrieben (§12 hält die
drei Runden fest), Plan-Teilbaum #ai.mcp neu, D93-Nachtrag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der erste Entwurf nahm an, MCP erzwinge Node; tatsächlich erzwang es nur
der Parser. Das Backend darf den HTTP-Transport tragen, wenn es die
JS-Module per GraalJS ausführt (dieselbe Technik wie #idea.drift.js) —
Kotlin ist Transport und Zugang, die Logik bleibt einmal vorhanden;
Rückfall ist der Node-Dienst hinter Apache. Urheber im Git-Spiegel:
Historie im Rumpf plus Trailer statt git --author, damit blame nicht lügt.
Entscheidungen in RFC §11 und D93 (zweite Runde).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ein Node-Paket, das die headless-Module des Frontends unverändert
importiert (kein zweiter Parser, D14), den Plan als Ressource und Tools
anbietet (Baum, Warnungen, günstigster Pfad, Knoten-Verben) und
Änderungen als Zeilen-Diff mit Basisversion schreibt (D76); Leitplanken:
keine beschädigte Notation, kein [^] von einem Agenten. Geteiltes Dokument
und Git zugleich: nach jedem Agenten-Zug committet der Server über
pull-doc --git-commit (D88). Entscheidungen der Multiple-Choice-Runde in
RFC §11 und D93.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der einfache Klick toggelt das Fenster überall — wie der Tipp auf Touch
(D52); der Link eines Knotens ist damit auch am Zeiger der ↗-Knopf im
Fenster, Enter und Strg+Klick öffnen die URL weiterhin direkt. Tastatur-
fokus öffnet nur noch echten (:focus-visible), focusout schließt nicht in
das Fenster hinein, und die Fokus-Rückgabe nach dem Falten ruft das
Fenster ausdrücklich nie.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eine gemeinsame flex-wrap-Aktions-Zeile für Schreiben/Übernehmen/Öffnen in
der Ticket-Box; mit Taiga-Knöpfen wird das Fenster auf 31rem breiter
(gemessen an den deutschen Labels), umgebrochen wird nur bei echtem
Platzmangel. Der Öffnen-Knopf heißt nur noch «{ref} öffnen» — «in Taiga»
sagen die Nachbarknöpfe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein Knopf im Knoten-Fenster bindet ein bestehendes Taiga-Ticket an den
Knoten: Die eingefügte Ticket-URL trägt Slug, Typ und Nummer selbst; eine
nackte Nummer wird per Probe Story-dann-Task aufgelöst. Verknüpft wird erst
nach Anzeige des Betreffs; geschrieben werden Ref-Token und ggf.
&taiga.<slug> in einem Undo-Schritt, der geholte Stand füllt den
Ticket-Cache. Eine URL einer fremden Instanz wird benannt statt verknüpft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Weicht der Ticket-Status von der Statusbox ab, markiert das Knoten-Fenster
die Abweichung und bietet beide Richtungen ausdrücklich an.
- „nach Taiga schreiben": Spalte des Projekts suchen (Taiga schreibt nach Id,
die Namen sind je Projekt frei) und mit der zuletzt GELESENEN `version`
patchen — hat jemand dazwischen geändert, lehnt Taiga ab und der Text steht
im Fenster, statt dass etwas überschrieben wird.
- „aus Taiga übernehmen": `setStatusBox()` schreibt die Box in die Textzeile,
undo-fähig wie jede andere Änderung.
- Schreibbar sind nur die fünf abgebildeten Zustände; `[?]`, `[!]`, `[-]` und
der neutrale Knoten lassen das Ticket unangetastet — mit Begründung im
Fenster.
- Proxy: zwei Spaltenlisten (`/taiga/{userstory,task}-statuses?slug=`) und
zwei Schreib-Endpunkte (`PATCH …/{ref}/status?slug=`); die Zielspalte wählt
der Editor, das Backend parst die Notation nicht (D14).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die offene Frage von #trk.write ist entschieden, gebaut ist nichts: Weicht
der Ticket-Status von der Statusbox ab, wird die Abweichung markiert und mit
zwei ausdrücklichen Aktionen angeboten — nichts geschieht von selbst.
Verworfen sind „der Text gewinnt" (schreibt selbsttätig in ein fremdes,
geteiltes System) und „Taiga gewinnt" (schriebe fortlaufend im Notationstext,
gegen D14); „Zeitstempel entscheidet" ist ohne erfundene Metadaten gar nicht
baubar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wo eine Ref steht und ein `&taiga.<slug>` gilt, zeigt das Knoten-Fenster
Betreff, Status und Zuständigen des Tickets; Taigas Statusname steht neben
der Statusbox der Notation (`In progress → [~]`).
- Proxy: zwei benannte Lese-Endpunkte (`GET /taiga/userstories/{ref}` und
`…/tasks/{ref}`, je `?slug=`) — das Präfix der Ref trägt den Typ, Taiga
hat getrennte `by_ref`-Endpunkte. Erst `/projects/by_slug`, dann `by_ref`
(eine Ref ist nur je Projekt eindeutig); der Slug wird kodiert angehängt.
- Die Abbildung Status → Statusbox liegt im Editor (`mapTaigaStatus`,
headless): Statuscodes sind Notation, das Backend parst sie nicht (D14).
Unbekannte Namen bleiben unabgebildet — Raten hieße, dem Knoten eine
Aussage zu geben, die niemand gemacht hat.
- Geholt wird erst nach 400 ms Verweilen und je Ticket einmal je Sitzung
(↻ holt neu); ohne Anmeldung gar nicht — der Knopf meldet erst an.
- Nichts wird geschrieben: kein Text, keine Statusbox (das bleibt
`#trk.write`).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Je @Person eine Pille mit Anteil an der offenen Pfad-Arbeit (D71-Maß),
dazu ein Eintrag für Unzugewiesenes — die Anteile summieren auf 100 %.
Ein Tipp filtert das Diagramm auf die Knoten der Person: alles andere
faltet, ihre Pakete stehen als ▸ n, die Pillen färben petrol. Die Linse
ist rein Ansicht und Sitzungssache — in den Text wird nichts geschrieben,
in geteilten Dokumenten ginge der persönliche Filter sonst an alle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die helle Umkehrung des Chips war für die dunkle Tinte-Wurzel gebaut —
seit D39 ist die Wurzel mit Status aber pastellfarben, und Weiß darauf
unsichtbar. Der Chip ist jetzt überall derselbe; desc-mark und nid
bleiben nur auf der statuslosen (dunklen) Wurzel weiß.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Ausnahme von der Pfad-Inversion stellte den ganzen Knoten auf volle
Stärke, weil opacity/filter den Ring mitträfen. Jetzt bleibt der Knoten
deckend und die Blässe wird nachgebildet: Papier-Wasch über der Füllung,
gefadete Text-/Rahmenfarbe, Kinder einzeln gedimmt — der Ring steht voll.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Verlaufs-Knopf zeigt bei Geteilten die Meilenstein-Historie des
Servers (GET /history, jetzt samt clientId/displayName); Laden ist ein
Server-Rollback (POST /restore, ROLLED_BACK — neue Version für alle,
nichts geht verloren, mit Rückfrage). Die Kamera legt einen
Server-Meilenstein an (pushLive(true), leeres Diff erlaubt); lokale
Momentaufnahmen sammelt snapshotNow() für Server-Dokumente nicht mehr —
sie enthielten fremde Arbeit und überschrieben sie beim Laden als
eigenes Diff. Dazu der Anzeigename: einmal gefragt, im Browser gemerkt,
füllt das 'geändert von' der Historie (Behauptung, kein Nachweis).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend: PATCH /documents/{id}/title mit expectedVersion (409 bei
veralteter Version, 400 bei leerem/zu langem Titel), neuer ChangeType
RENAMED (strukturell, immer Meilenstein), der Feed stellt den neuen Titel
im Klartext zu (ChangeEvent.title). Unter derselben Stripe-Sperre wie die
Inhalts-Patches; Owner-Vormerkung in der API-Beschreibung. Vier neue
Cucumber-Szenarien, zwei Unit-Tests.
Frontend: Der Zeilen-Stift eines Server-Dokuments benennt über den Server
um (optimistisch, 409-Retry, Rücknahme + Warnung bei Fehlschlag); fremde
Umbenennungen kommen als RENAMED über den Feed in Chip und Menü.
URL-Dokumente verlieren den Stift — ihr Name ist die URL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. Neu laden fragt bei URL-Dokumenten nicht mehr (F5 verwirft ohnehin
still, D23); bei Datei-Dokumenten bleibt die Rückfrage. Dabei gefunden
und behoben: Der Knopf erschien auch bei Server-Dokumenten und hätte
die JSON-Antwort der API als Text an alle gepusht.
2. Die Rückfrage beim letzten Dokument sagt, dass danach wieder das
Beispiel dasteht (docDeleteLastConfirm, 9 Sprachen).
3. Ein zweiter Tab, der in dieselbe Ablage schreibt, wird per
storage-Ereignis erkannt und gemeldet (tabConflict, isDocKey in
docstore.js) — kein Sync, aber kein stiller Verlust mehr.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eigene Aktion für die Geteilt-Gruppe (Server- und URL-Dokumente): Wort
(Verlassen), Icon (Tür mit Pfeil statt Papierkorb), Rückfrage (nur der
eigene Listeneintrag geht, Server bleibt) und normales statt rotem Hover.
Der Link wandert beim Verlassen in die Zwischenablage — der Listeneintrag
ist zugleich das Lesezeichen. Verhalten wie lokales Löschen
(removeDocLocally, gemeinsamer Kern); i18n in neun Sprachen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nutzer-Wortwahl statt „Eigene"/„Quellen": Lokale leben nur in diesem
Browser, Geteilte (Server- und URL-Dokumente) erreicht man über eine
weitergegebene Adresse. In allen neun Sprachen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
werkbaum-docs ist nur noch der Index [{id,name,source?}]; der Text jedes
Dokuments liegt unter werkbaum-doc:<id>. Damit trifft eine volle Quota nur
das eine zu große Dokument statt jeden Flush, und ein kaputter Schlüssel
kostet ein Dokument statt aller (vorher: JSON.parse-Fehler -> Rückfall auf
das Beispiel, alle Dokumente unsichtbar). Der Tastendruck schreibt direkt
den Text-Schlüssel; der Voll-Flush vergleicht vor jedem Schreiben und räumt
verwaiste Schlüssel ab. Altformat migriert beim ersten Laden; die
Spiegel-Regel aus D82 gilt nur noch dieser Migration. Schema headless in
docstore.js (+10 Tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
saveSrc() serialisierte bei jedem Tastendruck ALLE Dokumente in
werkbaum-docs. Jetzt schreibt der Tastendruck nur noch den Spiegel
werkbaum-src (ein Text); das volle Array schreiben die Flush-Punkte
(Wechseln/Anlegen/Löschen/Umbenennen, pagehide, verborgener Tab). Beim
Laden gewinnt der Spiegel für das aktive Dokument — zwingend VOR dem
Nachziehen mitgelieferter Fassungen, sonst dreht er sie zurück.
Und: persistDocs schluckte Speicherfehler still — bei voller Quota lief
der Editor scheinbar normal weiter, der Verlust fiel erst beim Neuladen
auf. Jetzt meldet die persistente Warnung storeFailed (9 Sprachen),
zuoberst, bis ein Schreiben wieder gelingt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ihr Name ist Auslieferungsstand: Wiederherstellen stellt ihn mit her, der
Neu-laden-Vergleich prüft ihn, und ein umbenanntes, aber unverändertes
Beispiel bekäme weiter still neue Fassungen nachgezogen (das Nachziehen
hängt an id + Text-Fingerabdruck, nicht am Namen). Kein Stift an
mitgelieferten Zeilen; renameDoc() prüft es selbst. Löschen bleibt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Das Zeilen-Symbol im Dokumenten-Menü konnte ein UNGEÖFFNETES mitgeliefertes
Dokument zurücksetzen — ohne zu sehen, was man verwirft (Nutzer-Einwand).
Umbenennen und Löschen bleiben je Zeile; restoreDoc() wirkt nur noch auf das
geöffnete Dokument und prüft das selbst.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auf einer Instanz ohne eigenes Backend (GitHub Pages) endete Teilen mit
HTTP 405: Die Vorgabe "eigene Herkunft" (D76-Nachtrag 8) stimmt nur auf der
produktiven Installation. serverBaseOrAsk() prüft die Vorgabe jetzt mit
GET /api/v1/info (D77), bevor gePOSTet wird, und fragt sonst nach der
Server-Adresse; gemerkt wird nur eine Adresse, die die Probe besteht.
Von Pages aus trägt man werkbaum.javagil.de ein — CORS erlaubt es.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Dokument-Wähler steht jetzt als gerahmter Chip "Werkbaum › Name" im
App-Kopf und ist damit auch auf dem Telefon aus beiden Bereichen erreichbar;
sein Name übernimmt dort die Untertitel-Zeile. Das Menü gruppiert nach
Dokumentart (docKind, headless getestet), trägt Umbenennen/Löschen/
Wiederherstellen als Symbole je Zeile und scrollt als Ganzes, statt auf
kleinen Bildschirmen abgeschnitten zu werden. Die Editor-Titelzeile heißt
wieder "Text-Editor" und trägt die Stand-Funktionen des aktiven Dokuments:
Speichern (Strg+S), Stand sichern, frühere Stände, Neu laden (Original/URL/
Datei) und den neuen kurzen Teilen-Knopf (vorher "Auf den Server legen" im
Menü). subtitleShort ist ersatzlos ausgebaut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wer bei offenem `?live=` umschaltet, behielt die alte Adresse — optisch
falsch, und ein Neuladen holte das falsche Dokument zurück. Der Parameter
gehört jetzt zum aktiven Dokument, für `?live=` wie für `?sourceUrl=`;
`?etherpad=` wird nur noch weggeräumt. Fremde Parameter (`?server=`)
bleiben wörtlich stehen, damit die URL lesbar bleibt.
Dabei gefunden: Die Live-Sitzung lief weiter, während ein anderes Dokument
vorn stand — `setLiveText()` schrieb fremde Änderungen in dessen Text. Die
Sitzung gehört jetzt dem sichtbaren Dokument: Umschalten beendet sie,
Umschalten auf ein Server-Dokument nimmt sie auf. Was noch im Debounce
steckt, geht vorher raus, und `pushLive()` hält seine Sitzung fest statt
anzunehmen, dass sich über ein `await` hinweg nichts ändert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gemeldet: ~3 s Verzoegerung zwischen zwei Browsern. Zerlegt statt geraten -
von 1,73 s gemessenem Weg A->B entfallen 1,67 s auf die Wartezeit vor dem
Senden. Alles andere sind zusammen ~70 ms.
Zwei Verdaechtige sind freigesprochen: Der Server weckt den wartenden Feed
39 ms nach dem PATCH (isoliert per curl, ohne Browser), und der Apache der
produktiven Instanz haelt den Long-Poll die vollen 25 s durch und schliesst
sauber mit 204 - kein Fenster ohne offenen Feed, kein 5-Sekunden-Fehlerpfad.
Produktiv kommen ~130 ms Rundlauf je Anfrage dazu.
Der Debounce bleibt ein Debounce (kein Takt): Wer durchtippt, erzeugt
weiterhin keine Version. Der Grund fuer die 1,5 s stammte aus der
Rate-Limit-Disziplin des Etherpad-Konzepts - und Etherpad ist ausgebaut (D78).
Die Aufbewahrung zahlt die haeufigeren Pushes: Jede Version speichert den
ganzen Text, und die Frist entscheidet einzig, ob ein zurueckgefallener Client
ein Diff oder den Volltext bekommt. Nutzersichtbar sind die Meilensteine, und
die werden nie verdichtet; zurueckfallen kann nur ein ruhender Feed
(Hintergrund-Tab). Zusammen sinkt die Spitze je aktiv getipptem Dokument von
115 MB auf 24 MB (49-kB-Plan, Dauertippen).
Dabei gefunden: Eine Schreibpause laenger als die Frist war mit einer Stunde
der Ausnahmefall und ist mit fuenf Minuten der Normalfall. Dass die letzte
Sync-Version davor nicht verlorengeht, haengt allein daran, dass
recordHistory() zuerst befoerdert und danach verdichtet - sonst loeschte die
Verdichtung genau den Stand, den die Befoerderung gleich zum Meilenstein
gemacht haette. Die Reihenfolge hat jetzt eine Zusicherung; vertauscht faellt
genau der danach benannte Test.
Werkzeuggrenze notiert: Der Automatisierungs-Browser zeigt seine Flaeche nicht
an, Chrome drosselt Timer verborgener Seiten auf 1 Hz (gemessen: ein blanker
setTimeout(600) feuert nach 999-1053 ms). Ein Sub-Sekunden-Debounce ist dort
grundsaetzlich nicht messbar.
501 Frontend-Tests, 139 Backend-Tests.
D31 lieh sich fuer die Echtzeit-Zusammenarbeit ein Etherpad: Das Pad war die
Schreibflaeche, Werkbaum die Ansicht. Richtig fuer den Zeitpunkt - die schwere
Arbeit war dort getan, und Werkbaum hatte kein Backend. Jetzt hat es eins
(D76), das dasselbe besser und im Editor selbst kann.
D31 bleibt stehen: Was dort gemessen wurde, ist der Grund, warum die Anbindung
nie gut wurde - Export-Drosselung (10 je 90 s), SameSite=Lax beim
Autoren-Cookie, abgelehnter Socket-Upgrade. Uebrig blieb ein Neu-laden-Knopf
und ein schreibgeschuetztes Textfeld.
Ein alter ?etherpad=-Link meldet sich, statt still nichts zu tun: Der Parameter
bleibt erkannt und ergibt die zeilenlose Warnung padGone, die auf ?live= zeigt
(neun Sprachen). Geholt wird nichts - nachgemessen geht keine Anfrage mehr an
den Pad-Host. Vorhandene Pad-Dokumente bleiben liegen und werden dabei besser:
ohne Schreibschutz sind sie bearbeitbar und sammeln fruehere Staende.
Mit ausgebaut, weil sie nur der Anbindung dienten: remote.js samt Tests, der
Ansichts-Waehler und sein Splitter, der Neu-laden-Knopf samt
Drosselungs-Zaehler, die Warnungen sourceTimeout/padRateLimit, elf i18n-
Schluessel x 9 Sprachen - und src.readOnly ganz, denn nur Pad-Dokumente haben
es gesetzt (mit ihm updateSnapBtn und sieben tote Waechter). Auch #srcArea
faellt weg: den Kasten gab es nur, damit Pad-Rahmen und Spiegel sich einen
Bereich teilen. ?sourceUrl= bleibt unangetastet - beide teilten sich einen
Fetch-Pfad, das war die Stelle, an der nichts verrutschen durfte.
Im Plan wird aus dem #col.pad-Zweig (14 Knoten samt der ganzen
"Update by itself"-Gruppe) ein verworfener Knoten mit Begruendung; der
gemeinsame Zeigefinger wandert als #col.point heraus, denn !!! gilt jedem
geteilten Dokument. 196 Knoten, 0 Warnungen.
Nebengewinn: Der README bekommt endlich den Abschnitt zu ?live= - den gab es
nie, die Einstiegs-Beschreibung stand weiter beim Pad.
Nachgemessen im Browser: ?etherpad= zeigt die Warnung ohne Netz-Anfrage,
?sourceUrl= laedt unveraendert und ist beschreibbar, Textfeld und
Zahlenstreifen liegen buendig, Legenden-Splitter teilt wie zuvor (954+10+300),
Falten und Staende funktionieren. 501 Tests, 946 Zeilen weniger.
Gemeldet: zwei Browser am selben Dokument, einen Knoten zuklappen, und es
kommt "Someone changed the same lines. Whose version should win?".
Der Feed beantwortet "was ist seit Version N geschehen" - und wer da
mitgeschrieben hat, steht nicht in der Frage. Er liefert also die EIGENE
Aenderung zurueck, und wacht er im Moment des eigenen Sendens auf, kommt sie
an, bevor die Antwort darauf da ist. Die Schattenkopie steht dann noch auf dem
Stand davor: Der Client haelt die eigene Aenderung fuer fremd, sieht sie sich
mit dem eigenen Text ueberschneiden und fragt. Die Erkennung hatte recht,
falsch war nur, wen sie fuer den anderen hielt. Der zweite Browser ist dafuer
gar nicht noetig; das Falten ist nur die kuerzeste Geste, die eine ganze Zeile
aendert.
Auf localhost liegen PATCH- und Feed-Antwort 7 ms auseinander und die
PATCH-Antwort gewinnt - der Fehler tritt dort nie auf. Reproduziert mit im
Client um 500 ms verzoegerter PATCH-Antwort (eine Reihenfolge, die uebers Netz
jederzeit auftritt): PATCH an 200 / FEED an 200 / KONFLIKT-BANNER.
Behoben in feedAction() (live.js) - dort steht ohnehin, wann eine Feed-Antwort
angewendet werden darf; Gegenprobe: Sperre entfernt => genau die zwei neuen
Zusicherungen fallen. Dieselbe Sperre gehoert in die runFeed-Schleife, sonst
fragt sie sofort wieder und dreht eine enge Runde uebers Netz.
Dabei gefunden: pushLive() las seine Basis erst NACH dem await und nahm damit
an, dass sich dazwischen nichts aendert - der Feed brach genau die Annahme und
haette die eigene Aenderung ein zweites Mal aufgerechnet. Jetzt vorher
festgehalten.
Nachgemessen: Falten erzeugt kein Banner mehr, fremde Aenderungen kommen
weiterhin an, und der ECHTE Konflikt wird weiterhin erkannt (A haelt
ungesendeten Text auf Zeile 1, B aendert dieselbe Zeile). 525 Tests.
Deployen, Log ansehen, Dienst schalten und fragen was laeuft waren vier
verschiedene Beschwoerungen, drei davon von Hand als ssh + systemctl. Jetzt:
remote backend deploy|upload|setup|install-jdk|reset-password
remote backend start|stop|restart|status|enable|disable
remote backend log|info|documents|backup
remote frontend deploy|preview|info
remote ssh
Die Skripte unter scripts/ bleiben die Implementierung und einzeln aufrufbar;
tools/remote bringt nur mit, wofuer es bisher nichts gab. Wo ein Schalter
noetig war, kam er ins Skript statt ins Werkzeug: --unit-only in
deploy-backend.sh (sonst kennte eine zweite Stelle die Unit-Platzhalter) und
--dry-run in deploy-prod.sh, das die Befoerderung ausdruecklich mit abschaltet.
Neu ist die Sicherung: H2 haelt die Datei offen, also anhalten, holen, wieder
starten (gemessen ~8 s Auszeit) - und das Archiv lesen, bevor der Befehl es
behaelt. Gegenprobe von Hand: lokal ausgepackt, Backend mit --werkbaum.data-dir
dagegen gestartet, es liefert genau die Dokumente des Servers.
.envrc legt tools/ auf den PATH (direnv), 217 Plan-Knoten, 0 Warnungen.