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>
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>
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>
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.
Der Editor fuehrt ein Dokument des Backends: laden, nach 1,5 s Ruhe das Diff
schicken, ueber einen offenen Abruf fremde Aenderungen einspielen — ohne
Neuladen, mit mitwandernder Schreibmarke. Dazu CORS im Backend; ohne das
blockiert der Browser jeden Aufruf.
Zwei Fehler hat erst der Live-Test gegen das laufende Backend gefunden, beide
an der Naht zwischen Modul und Verdrahtung (D54-Nachtrag 3):
Der Konflikt entstand nie. Mit laufendem Feed zieht die Schattenkopie staendig
nach, die eigene Basis ist also nie veraltet — der Server haette nie 409
geantwortet, und die fremde Zeile waere stillschweigend ueberschrieben worden.
Der Client prueft die Ueberschneidung jetzt selbst gegen den ungesendeten
Text; den kennt der Server nicht.
Die Nummer begann nach jedem Neuladen wieder bei 1, waehrend die Kennung
blieb — der Server hielt die erste echte Aenderung fuer eine Wiederholung und
tat nichts. Beides liegt jetzt im sessionStorage: je Tab, ueberlebt Neuladen.
Je Tab ist zugleich die richtige Aussage, zwei Tabs sind zwei Schreiber.
Nachgemessen im Browser: fremde Aenderung erscheint ohne Neuladen, Getipptes
erreicht den Server, beide Konflikt-Knoepfe tun was sie sagen, und die
Schreibmarke steht nach zwei fremd eingefuegten Zeilen darueber unveraendert
bei Zeile+2, Spalte 8. 518 Frontend-Tests, 135 im Backend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trägt ein @name mehr als die Hälfte der offenen Arbeit des günstigsten
Pfads (Marginalkosten-Maß, D69) und tragen mindestens zwei Personen Last
auf der offenen Front, meldet die zeilenlose Warnung assigneeOverload die
mögliche Engstelle; die Pillen der Person wechseln an offenen Pfad-Knoten
auf die Warnfarbe. Knoten ohne Tags erben für die Rechnung vom nächsten
getaggten Vorfahren, mehrere Tags teilen sich den Beitrag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Größen zählen für diese eine Prüfung als Bereiche (Untergrenzen verdoppeln
sich, XXL nach oben offen); Konflikt erst, wenn selbst die günstigste Lesart
der Kinder die großzügigste des Elternknotens erreicht. Warnung sizeConflict
an der Elternzeile, Badge in --warn, Grund in Tooltip und aria-label. Nichts
wird automatisch korrigiert. Nebenbefund behoben: der Grafikexport zeichnete
das Größen-Badge mit festen Farben — das invertierte implizite M stand seit
jeher gefüllt im Bild. Die acht berechtigten Konflikte im eigenen Plan sind
nachgezogen. 372 Tests, 16 neue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Streifen sagte bisher nur, DASS eine Zeile eine Warnung hat; welche,
stand allein unter dem Diagramm — man sah die orange Zahl und suchte dann
in der Liste die passende Zeilennummer.
- warnings.js: `warningText()` neben `formatWarning()`, beide aus demselben
`switch` (`build(w, t, esc)`) — der Warnungsbereich ist HTML, ein `title`
ist Klartext. Ohne die Trennung stünde dort „Drag & Drop“.
- app.js: `lineNoWarn` ist jetzt Map Zeile -> Text; mehrere Warnungen einer
Zeile stehen im Tooltip untereinander. Titel verschwinden mit der Warnung.
- style.css: Der Kasten der Warn-Zahl reicht über die ganze Streifenbreite
(Ziffern bleiben punktgenau stehen, nachgemessen 21,14 px), `cursor:help`.
Nachgemessen (4 Warnungen in 3 Zeilen): Tooltips wortgleich mit dem
Warnungsbereich, `&`/`"` unverfälscht, keine Zahl ohne Warnung mit `title`;
nach dem Beheben verschwinden Klasse und Titel mit. Werkbaum-Plan: 0.
SPEC §9 (Zeilennummern) + D33-Nachtrag; Plan: #ed.lineno.tip [x].
243 Tests grün (3 neue).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jeder nötige Knoten zieht seine :#…-Ziele samt Realisierung in die
nötige Menge; gemeinsam Gebrauchtes zählt über die Vereinigung nur
einmal. Verfahren gewählt und benannt (D34): erschöpfende Suche, aber
nur über die GEKOPPELTEN Gruppen (Teilbaum enthält Abhängigkeiten oder
wird gebraucht) — ungekoppelte wählen lokal wie bisher, ohne
Abhängigkeiten bleibt es bei genau einer Auswertung zum alten Preis.
Lexikografischer Odometer, strikt < : bei Gleichstand die erste
Alternative. Oberhalb des Suchlimits (20 000) gierig mit zeilenloser
Warnung cheapApprox statt stillschweigender Pseudo-Optimalität.
Abhängigkeiten ziehen auch optionale und nicht gewählte Ziele (ohne
deren Vorfahren), verworfene nie. Neue API computeCheapPlan {set,exact};
markCheapest entfällt. 8 neue Tests (tests/closure.test.js).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parser: `"`-Zeilen (Leerraum-Regel, nur ohne Zerlegungszeichen) hängen am
vorangehenden Knoten; hinter einem `---`-Trenner eröffnen #id-Zeilen
Blöcke, deren eingerückte Zeilen der Text sind (Leerzeilen = Absätze,
weitere Trenner bedeutungslos). Freitext ohne §1-Extraktion, nur %% fällt
weiter weg. Warnungen: unknownDesc (ID ohne Knoten, Blocktext wird still
geschluckt) und descStray je verwaister Zeile — ein versehentlicher
Trenner meldet die verschluckten Knotenzeilen zeilengenau. Anzeige im
Tooltip (Text zuerst, mehrzeilig) + aria-label, ”-Marke am Knoten
(nicht im Export — eine Marke ohne Ziel wäre Rauschen). i18n in allen
9 Sprachen; SPEC-§11-Abschnitt in §1/§9 überführt; 16 neue Tests
(tests/desc.test.js).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extraktionsschritt 7: alle alleinstehend angesetzten `:#a,#b`-Token —
EIN zusammenhängendes Token ohne Leerraum, mehrere je Zeile werden
zusammengeführt; eingeklammerte Erwähnungen wie `(:#auth,#api)` bleiben
Label (Zitier-Konvention wie `(#auth)`, deshalb enger als die frühere
§11-Formulierung). `deps` sind ID-Strings am Knoten; der Parser prüft
nur Existenz (Warnung unknownDep, Vorwärts-Referenzen normal), Zyklen
werden bewusst nicht einmal erkannt. Sichtbar als `→ #a, #b` im Tooltip
und als a11yDeps; Legendenzeile + Warntext in allen 9 Sprachen. Im
mitgelieferten Plan: Zyklen-legal und unknown-id-Warnung auf [x],
Abhängigkeits-Knoten auf [~]. 12 neue Tests (tests/deps.test.js).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extraktionsschritt 6 (nach den Tags): das erste alleinstehend angesetzte
`#name`-Token wird die Knoten-ID (Zeichenmenge wie `@name`); weitere
`#`-Token bleiben im Label (reservierte Ticket-Referenzen), `:#a,#b`
und `C#` werden nicht gefressen. Doppelte ID → Warnung duplicateId an
der späteren Zeile mit Nennung der ersten; die spätere gilt trotzdem.
Sichtbar im Tooltip (erste Position) und als a11yId im aria-label —
noch keine eigene Diagramm-Darstellung. Legendenzeile + Warntext in
allen 9 Sprachen; die drei `#`-Erwähnungen im mitgelieferten Plan sind
eingeklammert, damit sie Erwähnungen bleiben. 12 neue Tests
(tests/ids.test.js).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parser erkennt `=` als disjunktives Gate (nur mit folgendem Leerraum —
`=SUMME(…)` bleibt Label); Mischungen melden mixedGate wie gehabt. Die
XOR-Regel warnt je WEITERER realisierter Alternative (xorConflict mit
Zeilennummer; realisiert = [~]/[/]/[x]/[^], siehe D35). Darstellung erbt
die komplette any-of-Geometrie (ul class="or xor"), ergänzt um eine
„1"-Plakette am Austritt der Sammelleiste — auch im Grafikexport.
Legende (+ hint_root) und Warntext in allen 9 Sprachen; SPEC §11-Eintrag
in §1/§3/§9 überführt; 13 neue Tests (tests/xor.test.js).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Hintergrund-Takt aus dem letzten Commit funktionierte in der Praxis nicht:
im Netzwerk-Mitschnitt stapelten sich Anfragen und wurden abgebrochen
("cancelled"). Die Ursache ist nicht Langsamkeit, sondern ein Rate Limit —
Etherpads `importExportRateLimiting` ist serienmäßig an und lässt 10 Abrufe je
90 s und IP zu; der Takt wollte 36. Jenseits davon antwortet die Gegenseite
nicht mit 429, sondern hält die Verbindung ohne Antwort offen. Nachgemessen:
12:48:12 Abbruch nach 25 s (0 Bytes)
Abbruch nach 25 s (0 Bytes)
12:50:23 HTTP 200 nach 0,436 s <- nach ~40 s Pause
Zwei Minuten totgestellt, dann sofort in 0,4 s da. Gegen eine Drosselung kann
ein Takt nicht gewinnen, er erzeugt sie. Also ein Knopf. Damit entfallen
Stabilitätstakt, Sichtbarkeits-Wächter, Anti-Stapel-Riegel und Wiederanlauf.
Der Knopf greift zudem gut mit "Was ist neu?" (D28) zusammen: drücken, und was
seither in Produktion ging, leuchtet auf.
Verhindern UND melden, beides:
- Mindestabstand 10 s zwischen zwei Abrufen. Das ergibt höchstens 9 je 90 s und
bleibt damit beweisbar unter Etherpads Budget — die Drosselung wird gar nicht
erst ausgelöst. Nach einem Abbruch sind wir schon drüber: dann das ganze
Fenster (90 s) abwarten statt weiter dagegen zu rennen.
- Der Knopf bleibt in der Sperre **klickbar** (nur `aria-disabled`, nicht
`disabled`): Ein Klick meldet den Grund und nennt die Grenze samt Restzeit.
Die Meldung landet in `#warn`, das eine Live-Region ist, wird also angesagt.
Ein `disabled` erklärt nichts.
- Eigener Warnungstyp `sourceTimeout` für den Abbruch: `sourceLoad` zeigt auf
CORS und schickte hier auf die falsche Fährte.
- `setPadBusy()` dreht das Symbol während des Abrufs — das können bis zu 20 s
sein, und ohne Zeichen wirkt der Knopf kaputt.
Zwei Fehler des Vorgänger-Commits fielen dabei auf:
- `padSource` und der Takt standen HINTER dem `await` des ersten Abrufs. Ein
einziger Fehlschlag — bei dieser Gegenseite der Normalfall — ließ das Dokument
tot liegen, ohne Knopf und ohne Wiederversuch, bis zum Neuladen der Seite.
Genau das war die vom Nutzer gemeldete Fehlermeldung.
- Kein Erfolg räumte die Warnung des ersten Versuchs weg; sie hätte gestanden,
während der Text längst da war.
Dazu: `deleteDoc()` beendet die Pad-Quelle, sonst legte ein späterer Abruf das
gelöschte Dokument wieder an und aktivierte es auch gleich.
Nebenbefunde, beide nachgemessen und in D31 festgehalten:
- Das *Tippen* im Etherpad-Editor war der letzte offene Punkt. Der Nutzer hat
` - [ ] Layout` eingerückt eingetippt; der Export gibt es zeichengenau
zurück (vier echte Leerzeichen, kein Tab, kein Listen-Markup).
- Einbetten verweigert der Server nicht: kein `X-Frame-Options`, keine CSP mit
`frame-ancestors`, und im Versuch baute das Pad in einem fremdstämmigen Rahmen
seine Editorframes auf. Preis ist `SameSite=Lax` am Autoren-Cookie (jedes
Laden ein neuer Autor; nur serverseitig zu beheben) und der Verlust beider
Richtungen von D25, falls das Textpanel ersetzt wird.
86 Tests (85 -> 86); Schreibschutz zeigt den Text jetzt in Tinte statt grau —
hier wird gelesen, der Plantext ist der Hauptinhalt.
Der Editor kann den Text aus einer externen Textdatei beziehen:
…/index.html?sourceUrl=https://example.org/plan.txt
- Das Geladene wird als eigenes Dokument (D22) geführt, dessen Name die URL
ist; die id leitet sich aus der URL ab, derselbe Link aktualisiert also
dieses Dokument, statt bei jedem Aufruf ein neues anzulegen. Eigene
Dokumente bleiben unberührt. Die volle URL steht zusätzlich im Tooltip
(der Name wird in der Titelzeile elidiert).
- Ist der Parameter gesetzt, wird bei jedem Laden neu geholt — die URL ist
die Quelle der Wahrheit.
- Nur http(s) (relative Angaben werden aufgelöst), credentials:'omit';
file:/data:/javascript: werden abgewiesen. Fremder Text ist ungefährlich:
Labels werden escaped, der Parser erkennt als Link nur https?:// (SPEC §1).
- Fehler (CORS, 404, Netz) landen als zeilenlose Warnung `sourceLoad` im
Warnbereich und nennen CORS ausdrücklich als wahrscheinliche Ursache; der
bisherige Stand bleibt stehen. i18n in allen 9 Sprachen.
- Nebenbei: checkForUpdates() baut die Cache-Buster-URL nun über
URL/searchParams — mit vorhandenem Query-String hätte '?t=' ein zweites
'?' erzeugt.
Verifiziert headless (jsdom gegen den gebauten Bundle: Erfolg, CORS-Fehler,
404, ohne Parameter, verbotenes Schema) und im Browser gegen
raw.githubusercontent.com. Dokumentiert als D23 + SPEC §9 + frontend/CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TASKS Phase 1 (abgeschlossen). Warnungen sind jetzt maschinenlesbare Objekte
{type, line, ...data} statt vorformatierter Strings: der Renderer emittiert
{type:'mixedGate', line, label}; src/warnings.js `formatWarning(w, t)` macht
daraus an EINER Stelle den lokalisierten, HTML-escapten Anzeigetext. Damit sind
Typ und Zeilennummer sortier-/filter-/testbar, und Phase 2 (unbekannte
Statuszeichen -> {type:'unknownStatus', line, code}) dockt ohne Formatstreuung an.
tests/warnings.test.js + Mixed-Gate-Test im Renderer (29 Tests grün).
Ende-zu-Ende im Browser verifiziert (lokalisierte Warnung mit Zeile + Label).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>