Commit Graph
144 Commits
Author SHA1 Message Date
mhoennigandClaude Opus 5 4aff995a0e feat(deploy): Backend auf die stabile Instanz bringen (D77)
Drei Teile, die das Frontend-Deploy nicht braucht: ein eigenes JDK 21 im Home
des Servers (dort ist nur 17 installiert, und die Toolchain zu senken hiesse,
Entwicklung und Produktion auseinanderlaufen zu lassen), ein systemd-User-Unit
statt nohup, und die Proxy-Regel in der .htaccess — gemessen ist, dass das
P-Flag auf diesem Hoster erlaubt ist und eine Verbindung 30 s durchhaelt.

Der Port steht an genau einer Stelle: deploy-prod.sh setzt ihn in die
Proxy-Regel, deploy-backend.sh in die Unit. Zwei Zahlen, die zueinander passen
muessen, sind eine zu viel.

Die JVM-Flags sind gemessen, nicht geschaetzt. Mein erster Entwurf setzte
-Xmx384m; nachgemessen kam heraus, dass die Obergrenze der kleine Hebel ist:
Ohne Freiraum-Verhaeltnisse behaelt der Kollektor den gewachsenen Heap, obwohl
nach einem GC nur ~45 MB leben. Mit ihnen 174 MB RSS statt 291 MB ohne jede
Angabe — auf einem Host mit rund 300 MB frei ist das der Unterschied zwischen
"passt" und "draengt die Datenbank weiter in den Swap".

Zwei Fallen sind eingebaut, weil beide nur am Ziel auffielen: systemd
expandiert kein $HOME (deshalb %h), und `systemctl --user` findet ohne
XDG_RUNTIME_DIR seinen Manager nicht.

Geprueft bis an die SSH-Grenze: Das Jar startet mit genau den Flags der Unit
in einer Sekunde, antwortet auf die Lebendprobe mit 404 und ist von aussen
nicht erreichbar; die erzeugte Unit besteht systemd-analyze verify; der ganze
Ablauf lief mit gestelltem ssh/rsync durch; deploy-prod.sh liefert die
Proxy-Regel mit eingesetztem Port aus. Der Deploy selbst laeuft erst, wenn
jemand ihn startet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:11:56 +02:00
mhoennigandClaude Opus 5 08965157a1 feat(frontend): gemeinsam am Server-Dokument arbeiten (?live=, Schritt 6)
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>
2026-08-26 17:48:17 +02:00
mhoennigandClaude Opus 5 bc8850a518 feat(backend): Master-Passwort fuer die Dokumentenliste (Schritt 5)
GET /documents listet alle Dokumente und machte damit jede UUID auffindbar —
das Zugriffsmodell "unerratbarer Link" waere hinfaellig. Der Endpunkt
verlangt jetzt HTTP Basic; alles andere bleibt bewusst offen.

Ohne konfigurierten Hash ist die Liste ausdruecklich gesperrt (denyAll),
nicht offen: Ein vergessener Umgebungswert gaebe sonst jede UUID preis, und
niemandem fiele es auf, weil alles funktioniert. Bewusst als Regel und nicht
bloss als zufaelliges Passwort, das niemand kennt — der Unterschied ist
pruefbar: Mit dem Zufallspasswort blieb die Gegenprobe stumm, mit denyAll
faellt genau die danach benannte Zusicherung.

Die Sperre nach Fehlversuchen ist global statt je Adresse: Es gibt genau ein
Passwort, und hinter dem Reverse Proxy der Zielumgebung saehe der Server
ohnehin fuer alle dieselbe Adresse. Preis benannt (D76-Nachtrag 6).

135 Tests. Gegenproben: Schutz entfernt, Sperre entfernt, Voreinstellung
geoeffnet -> es faellt jeweils genau die danach benannte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:30:10 +02:00
mhoennigandClaude Opus 5 741b41ef11 feat(backend): Aenderungsfeed per Long Polling (Schritt 4)
GET /documents/{id}/changes haelt die Anfrage offen und antwortet, sobald
sich etwas tut — kumuliertes Diff seit der bekannten Version, dazu die
Ereignisse mit ihrem Absender. Ist die Basis verdichtet oder hat der Client
noch gar nichts, kommt der Volltext statt der Operationen: ein Roundtrip und
ein Sonderzustand weniger als ein eigener Fehlerpfad.

Der Feed arbeitet auf der Historie, nicht am Dokument — ein geloeschtes
Dokument muss sein DELETED noch zustellen koennen.

Blockierend auf virtuellen Threads statt DeferredResult (D76-Nachtrag 5):
So behaelt der Endpunkt die aus der Spezifikation generierte Signatur, und
API-First bleibt fuer ihn unangetastet; ein Wartender kostet trotzdem fast
nichts. Geweckt wird nach dem Commit, nie davor, und ueber einen Stempel, den
der Aufrufer VOR dem Nachsehen liest — sonst ginge ein Signal aus der Luecke
dazwischen verloren.

122 Tests. Das Szenario "ein Wartender wird geweckt" misst die Dauer: Ohne
das bestuende es auch dann, wenn der Wartende bloss in den Timeout liefe und
danach die Aenderung vorfaende. Gegenprobe: Benachrichtigung entfernt ->
genau dieses Szenario faellt; Volltext-Rueckfall entfernt -> genau jenes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:16:13 +02:00
mhoennigandClaude Opus 5 ae85503b79 feat(backend): PATCH /content — Diffs einreichen, rebasen, wiederholen (Schritt 3)
Der Server rebased selbst: Ist die Basis veraltet, ueberschneiden sich die
Operationen aber nicht mit den zwischenzeitlichen, verschiebt er sie und
akzeptiert. Reines Ablehnen fuehrte zu Starvation — ein Client mit hoher
Latenz kaeme bei fleissigen Mitschreibern womoeglich nie durch. 409 gibt es
nur bei echter Ueberschneidung, mit allem, was der Client zum Weiterarbeiten
braucht, ohne neu zu laden.

Pruefsumme ist Pflicht (422 bei Abweichung): Die Versionsnummer bestaetigt
nur, dass die Basis dieselbe Version ist, nicht dass beide Seiten sie gleich
lesen. clientId + seq machen den Aufruf wiederholbar — im Mobilnetz ist die
verlorene Antwort der Normalfall.

Die Sperre je Dokument liegt ausserhalb der Transaktion: innen gaebe der
Proxy sie vor dem Commit frei, und der naechste Schreiber laese einen Stand,
der noch nicht steht. Deshalb ist LiveEditingService nicht transaktional und
schreibt ueber DocumentService.

Was das Konzept offenliess, ist jetzt entschieden und in D76-Nachtrag 4
begruendet: die Randfaelle der Einfuege-Ueberschneidung, die Trennung von
400 und 422, die gedeckelte Idempotenz im Speicher.

104 Tests, davon 8 Cucumber-Szenarien fuer das Live-Editing. Gegenprobe:
Pruefsumme nicht geprueft, Idempotenz entfernt, veraltete Basis abgelehnt
statt verschoben -> es fallen jeweils genau die danach benannten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:05:14 +02:00
mhoennigandClaude Opus 5 9e5f546d76 docs: Speicherzahlen der Zielumgebung berichtigt (D76-Nachtrag 3)
Nachtrag 2 nannte "832 MB frei" - ein Schnappschuss, und dazu mehrdeutig
zwischen `free` und `available`. Nachgemessen: 326-358 MB free, 978-1004 MB
available von 3915 MB, dazu ~1 GB Swap belegt.

Falsch war ausserdem die Annahme, ein Managed Webspace koenne den Verbrauch
nicht zuordnen. `hidepid=invisible` verbirgt zwar fremde Prozesse, aber das
systemd-cgroup-Accounting (`systemctl status pacs-<paket>.slice`, so auch im
Hostsharing-Wiki) und die world-readable atop-Aufzeichnungen unter
/var/log/atop/ geben sie her.

Befund: nicht die Datenbanken. clamav-daemon 988 MB gegen mariadb 150 MB und
postgresql 111 MB; alle zwoelf Webspaces zusammen 100 MB. MariaDB haelt
602 MB im Swap - die DBs sind auf je 25 % des RAM provisioniert und verlieren
gegen den Virenscanner.

Fuers Deployment: pacs-mih00.slice hat MemoryMax=3147M - eine Erlaubnis, keine
Reservierung. `-Xmx` gehoert gegen das Freie bemessen; die JVM-Voreinstellung
(~980 MB) ist genau die Groesse, die MariaDBs Puffer verdraengt hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:57:06 +02:00
mhoennigandClaude Opus 5 af8a7b74b3 docs: Feed nur im sichtbaren Tab (D76-Nachtrag 1)
Der Zielserver bietet kein HTTP/2, also gilt das Browser-Limit von sechs
Verbindungen je Herkunft - ein dauerhaft offener Long-Poll je Tab engt bei
mehreren Tabs alles andere ein. Der Feed schliesst deshalb bei
visibilitychange und holt beim Zurueckkommen mit dem eigenen since nach.
Ein Hintergrund-Tab braucht keinen Live-Feed; das spart nebenbei
Server-Worker und Akku. Ein SharedWorker waere sauberer, ist aber eine eigene
Baustelle fuer ein Problem, das die einfache Loesung praktisch beseitigt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:30:03 +02:00
mhoennigandClaude Opus 5 b6509ae537 test(backend): Kotest-Assertions statt JUnit, CLAUDE.md berichtigt
backend/CLAUDE.md sah Kotest-Assertions vor, der Code benutzte aber die
JUnit-Assertions. Umgestellt bei 25 Aufrufen in zwei Dateien - jetzt billig,
und Test-Abhaengigkeiten sind unkritisch, weil sie in keinem Artefakt landen.
Wo Kotest spezifischer ist, ersetzt es die handgeschriebenen Meldungen:
shouldContain statt assertTrue(contains, "..."), withClue nur noch dort, wo
der Kontext wirklich hilft.

Gegenprobe per Mutation: eine falsche Erwartung im Service-Test und eine im
BDD-Schritt lassen genau die danach benannten Tests fallen.

CLAUDE.md berichtigt: Das Backend ist nicht mehr "noch nicht bootstrapped",
die Konfiguration heisst application.yaml, die Schichten entsprechen dem
tatsaechlichen Aufbau, und die drei Spring-Boot-4-Fallen von heute sind
festgehalten, damit sie niemanden erneut treffen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:29:24 +02:00
mhoennigandClaude Opus 5 f3afc8dfa8 refactor(backend): Paketwurzel de.werkbaum, Tests auf RestTestClient
Das Backend-Geruest kam unter der Platzhalter-Wurzel com.example.editor
herein und widersprach backend/CLAUDE.md. Umgezogen nach de.werkbaum: 17
Kotlin-Dateien, Gradle-group, apiPackage/modelPackage der OpenAPI-Generierung,
die jacoco-Ausschluesse und das Cucumber-glue-Paket. Jetzt war es billig, mit
jeder Woche Entwicklung waere es teurer geworden.

Die BDD-Tests nutzen jetzt RestTestClient statt TestRestTemplate, das in
Spring Boot 4 als Auslaufmodell gilt. Weil Cucumber Senden und Pruefen
trennt, wird die fluent API nicht fuer Zusicherungen genutzt, sondern ueber
returnResult das Ergebnis festgehalten.

Nachgemessen: 22 Tests gruen, check inklusive Coverage-Verifikation besteht,
91,7 % Zeilenabdeckung, generierter Code weiterhin ausgeschlossen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:21:34 +02:00
mhoennigandClaude Opus 5 7684872a42 docs: Proxy-Weg gemessen, JDK entschieden (D76-Nachtrag 2)
RewriteRule [P] ist in der .htaccess erlaubt - gemessen mit einer temporaeren
Regel auf einen lokalen Testprozess, danach vollstaendig zurueckgebaut.
Sofortige Antwort HTTP 200 nach 0,13 s, absichtlich um 30 s verzoegerte
Antwort HTTP 200 nach 30,1 s: Apache haelt die Verbindung durch und puffert
nichts weg. Long Polling mit wait=25 ist damit gemessen tragfaehig, nicht nur
rechnerisch. Die Regel gehoert nach scripts/prod.htaccess, weil deploy-prod.sh
die Datei mit rsync --delete spiegelt.

JDK: eigenes 21 ins Home statt Toolchain auf die installierte 17 senken.
GraalVM Native Image ist vorgemerkt statt verworfen - es spart den Grossteil
des knappen RAM (nur 832 MB frei auf einem geteilten Server), scheitert aber
vorerst an der glibc-Differenz, an Liquibase/Hibernate-Metadaten und an einem
offenen Boot-4-Fehler fuer genau diese Kombination. Kotlin/Native scheidet
grundsaetzlich aus: Spring, Hibernate und JDBC sind JVM-Bibliotheken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:13:56 +02:00
mhoennigandClaude Opus 5 2540bbf0a7 docs: Live-Editing-Konzept entschieden und eingearbeitet (D76)
Die offenen Punkte der beiden Entwuerfe sind beantwortet und in beide
Dokumente eingearbeitet. Die wichtigsten Aenderungen am Protokoll:

- Der Server rebased selbst; 409 nur noch bei echter Ueberlappung. Reines
  Ablehnen fuehrt zu Starvation - ein Client mit hoeherer Latenz kommt bei
  fleissigen Mitschreibern womoeglich nie durch.
- Der Feed arbeitet auf der Historie statt am Dokument, sonst koennte er
  ausgerechnet das Loeschen nicht melden, das er melden soll.
- Historie in zwei Ebenen: kurzlebige Sync-Versionen und nutzersichtbare
  Meilensteine (nach Schreibpause und auf Knopfdruck, wie D54).
- Pruefsumme als Pflichtfeld, clientId + seq gegen doppelt angewendete
  Patches, Volltext im Feed fuer zu alte Staende, eigener Aenderungstyp fuer
  den Rollback, Titel im Feed-Ereignis.
- Zugriff ueber die unerratbare UUID; GET /documents bekommt ein
  Master-Passwort (Spring Security).

Die Client-Instruktion war fuer ein anderes Frontend geschrieben: Sie nannte
CodeMirror, TypeScript, jsdiff und MSW. Werkbaum hat eine rohe textarea,
Vanilla JS und keine Laufzeit-Abhaengigkeiten - CodeMirror bleibt eine eigene
Entscheidung (gemessen: 120 kB gzip Einstieg, danach nur 18 kB fuer alles
Weitere).

Zielumgebung vermessen: Apache 2.4.68 mit MPM event, 1024 Worker,
Timeout 300 - Long Polling traegt dort. Offen bleiben fehlendes HTTP/2,
nur Java 17 statt 21 und der ungeklaerte Weg vom Apache zum Backend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:04:00 +02:00
mhoennigandClaude Fable 5 33443016ce feat: Querverbindungen folgen der Faltung + Schalter; Falt-Knopf mit vier Voreinstellungen (D75)
- Abhängigkeits-Kanten enden am nächsten sichtbaren Vorfahren, für Quelle
  wie Ziel; zusammengefallene Kanten entfallen bzw. werden dedupliziert
  (data-sub-ids/data-sub-deps am eingeklappten Vertreter, SPEC §9)
- Umschalter für die Querverbindungen neben dem Günstigster-Pfad-Knopf,
  Voreinstellung an, persistiert; Export und Druck folgen ihm
- Der Falt-Knopf schaltet durch: M und kleiner zu → alles abseits des
  Pfads zu → alles zu → alles offen; Icon/Tooltip zeigen den nächsten
  Schritt, Handfaltung setzt die Position zurück (presetFoldSet, model.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 17:19:32 +02:00
mhoennigandClaude Fable 5 69c9be7a6d feat: Brave-Hinweis nennt die Flag-Adresse zum Einschalten der API (D72-Nachtrag 4)
Nur in Brave (navigator.brave) bekommt das Datei-Banner eine zweite Zeile
mit brave://flags/#file-system-access-api als kopierbarem Code-Text —
verlinken lassen sich interne Schemata nicht. i18n in neun Sprachen, die
Adresse selbst steht einmal im Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 16:10:47 +02:00
mhoennigandClaude Fable 5 bf6eceeb9d fix: Hinweis und Legende nennen das Feature statt „Chromium" (D72-Nachtrag 3)
Brave ist Chromium-basiert, schaltet File System Access aber bewusst ab —
die Begründung „Chromium-Browser schreiben direkt zurück" widersprach dort
dem eigenen Erleben. Alle 18 UI-Strings (fsNotice + hint_save, 9 Sprachen)
nennen jetzt die Schnittstelle und Beispiel-Browser (Chrome/Edge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 16:02:30 +02:00
mhoennigandClaude Fable 5 b16f9954a9 feat: Einmaliger Hinweis in Browsern ohne File System Access (D72-Nachtrag 2)
Firefox/Safari laden Dateien als Kopie und speichern als Download — beim
ersten Öffnen/Speichern erklärt ein Banner die Grenze (i18n in neun
Sprachen, „Verstanden" merkt der localStorage, Reset räumt mit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:53:42 +02:00
mhoennigandClaude Fable 5 731a383345 fix: Speichern-Dialog zeigt auf die Originaldatei statt auf einen „(1)"-Nachbarn (D74-Nachtrag)
Mit bekanntem, aber nicht beschreibbarem Handle bekommt showSaveFilePicker
startIn + den exakten Dateinamen; ohne Handle teilen sich Öffnen und
Speichern eine Picker-id, damit der Dialog im Plan-Ordner aufgeht statt in
Downloads. Der Abbruch-Kreislauf (Dialog schlägt Falsches vor -> Abbruch ->
kein Handle gemerkt -> wieder Dialog) ist damit durchbrochen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:46:07 +02:00
mhoennigandClaude Fable 5 72ab9746d5 feat: Strg+S speichert das Dokument direkt als Datei (D74)
Die Geste, die jeder zuerst versucht, tat bisher das Falsche (Browser-Dialog
„Seite speichern"). Jetzt ruft sie saveLocalFile(): mit gemerktem Handle
dialogfrei in dieselbe Datei, sichtbar bestätigt durch einen Petrol-Haken am
Dokumentnamen; ohne Handle wie der Menü-Eintrag. Legenden-Zeile hint_save in
neun Sprachen. Nebenbefund behoben: adoptFile() wartet jetzt auf die aus
IndexedDB zurückgeholten Handles — ein Doppelklick unmittelbar nach dem
App-Start legte sonst ein Duplikat an statt dasselbe Dokument zu
aktualisieren.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:15:50 +02:00
mhoennigandClaude Fable 5 19053eba1d feat: Als PWA installierbar — Manifest, Offline-Worker (network-first), Dateihandling (D73)
Manifest mit Icons aus der Marke und Standalone-Fenster; ein bewusst dummer
Service Worker beantwortet nur die App-Navigation network-first und hält die
zuletzt gesehene Fassung für den Offline-Fall — die Update-Prüfung (D45)
bleibt dadurch unverändert wahr, der skipWaiting-Lebenszyklus entfällt. Die
installierte App registriert sich für .werkbaum-Dateien (file_handlers +
launchQueue -> adoptFile). Beide Deploy-Wege kopieren die App-Hülle mit;
.htaccess liefert .webmanifest mit MIME-Typ aus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 15:03:22 +02:00
mhoennigandClaude Fable 5 2648d72e45 feat: Speichern schreibt in die geöffnete Datei zurück (D72-Nachtrag, Stufe 2)
File System Access API, wo vorhanden (Chromium): Öffnen merkt sich das
FileSystemFileHandle (IndexedDB, überlebt den Neustart), Speichern schreibt
in dieselbe Datei; dieselbe Datei öffnet per isSameEntry wieder in dasselbe
Dokument. Firefox/Safari behalten den Stufe-1-Download.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 14:31:51 +02:00
mhoennigandClaude Fable 5 defeaf84d0 feat: Lokale Dateien öffnen und als Datei speichern (D72, Stufe 1)
Zwei Einträge im Dokumenten-Menü: „Datei öffnen…" (Datei-Input, neues
Dokument, Dateiname als Name) und „Als Datei speichern" (Blob-Download,
Dateiname aus dem Dokumentnamen via localfile.js). Läuft in jedem Browser;
Stufe 2 (File System Access, Chromium) folgt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 14:26:31 +02:00
mhoennigandClaude Fable 5 35b72fc178 feat: Zuständigen-Engpass — Warnung, wenn eine Person mehr als die Hälfte des Pfads trägt (D71)
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>
2026-08-25 11:29:52 +02:00
mhoennigandClaude Fable 5 0de7f5f24e feat: Die geschätzte Größe bepreist den Rest — Erledigtes fällt aus der Schätzung (D70)
Die D66-Schätzung war status-blind: Ein größenloses Paket mit [x] (L) und
[ ] (S) wurde als L geschätzt, obwohl nur S offen ist. Erledigte Kinder
fallen jetzt heraus, eine erledigte realisierte Alternative stellt ihre
Gruppe fertig, und sind alle benannten Kinder erledigt, gilt XS (der eigene
Rest — M-Rückfall erhöhte den Preis beim Fertigwerden). Angegebene Größen
bleiben unangetastet (D69).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 11:04:26 +02:00
mhoennigandClaude Fable 5 4d80feb4e8 fix: Falt-Chip deckend weiß mit Tinte-Glyphe — Kontrast auf den Pastellfarben (D64-Nachtrag)
Die fast transparente Füllung mit mattgrauer Glyphe verschwand auf den
acht Status-Pastelltönen. Nur Farben geändert; line-height und Padding
bleiben (23-px-Geometrie).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:50:01 +02:00
mhoennigandClaude Fable 5 4aaa19becd feat: Die Größe bepreist den ganzen Teilbaum — Kinder zählen nicht obendrauf (D69)
Der Preis eines Knotens ist seine angegebene (oder D66-geschätzte) Größe;
die frühere Rekursion (eigene Größe plus Summe/Minimum der Kinder) zählte
doppelt und bestrafte sorgfältig zerlegte Pakete — eine zerlegte
S-Alternative verlor gegen ein grobes L. Das Vereinigungs-Maß der
Closure-Suche (D42) zählt je Knoten nur noch die Marge über seine nötigen
Teilpakete hinaus. Mitgelieferter Plan wählt identisch (per Vergleich
gemessen); SPEC §9 und llms.md nachgezogen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:45:12 +02:00
mhoennigandClaude Fable 5 59da485af6 feat: Größe ist das letzte alleinstehende (L)-Token der Zeile (D68)
Erkannt nur alleinstehend angesetzt (wie #id und :#…) — damit greifen die
Zitier-Konventionen "(L)" und ((L)) von selbst, und Backend(L) bleibt
Label. Das letzte Token gewinnt statt des ersten: Die Größe steht hinter
dem Titel, frühere Vorkommen sind Text. SPEC §1/§5 und llms.md nachgezogen;
mitgelieferte Beispiele unverändert (Parse-Vergleich alt/neu).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:19:52 +02:00
mhoennigandClaude Fable 5 549c12191e feat: Strg+Klick auf :#id springt zur Zeile der referenzierten ID (D67)
Strg/Cmd+Klick auf eine Abhängigkeits-ID im Textfeld — auch jede ID der
Liste und die Kopf-Form #auth:#db — markiert die Zeile, die die ID vergibt;
Tastatur-Pendant Strg+Enter. Erste Vergabe gewinnt (D36/D39), unbekannte IDs
tun still nichts. Headless als depIdAt/idLine in autocomplete.js, 16 Tests;
Legenden-Zeile hint_jump in allen neun Sprachen erweitert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 08:59:58 +02:00
mhoennigandClaude Fable 5 bb01045488 feat: Fehlende Größe aus den Teilpaketen geschätzt statt pauschal M (D66)
Mindestens die größte Größe der zählenden Kinder, ab drei Kindern dieser
Größe eine Stufe mehr (Deckel XXL); disjunktiv die kleinste Alternative,
rekursiv über größenlose Kinder, Blatt bleibt M. Badge, Tooltip und
aria-label zeigen die geschätzte Größe („mindestens {size} angenommen").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 19:59:13 +02:00
mhoennigandClaude Fable 5 ebc5be407e feat: Label-Umbruch bei 32 statt 40 Zeichen (D64-Nachtrag 3)
Lange Titel zogen das Diagramm zu breit. Ein Parameter in wrapLabel(),
der 32ch-Rückhalt im CSS und SPEC §9 ziehen mit; Balance-Regel und
Erste-Zeile-Verankerung unverändert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 18:36:52 +02:00
mhoennigandClaude Fable 5 07e4116bd9 fix: vertikale Sammelleiste erreicht den Eltern-Stub wieder (D65)
Zwei Geometrie-Fehler bei Gruppen mit wenigen Kindern: :only-child schaltete
die Leiste ab, obwohl Stub (50 %) und Abzweig (23 px) seit dem 20-px-
Zusatzabstand nicht mehr zusammenfallen (6,6–15,8 px Lücke); und bei einem
großen Teilbaum im letzten Kind endete die Leiste über dem Stub (bis 98 px).
Fix: CSS-Verbinder 23px→50 % am Einzelkind, alignVRails() + --vrail-ext für
das letzte Kind. Export und Kompakt-Modus waren nie betroffen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 18:19:19 +02:00
mhoennigandClaude Fable 5 52232f2c20 feat: Labels brechen balanciert um, ID-Zeile über dem Titel, Falt-Chip — D64
Lange Knotentitel brechen bei ~40 Zeichen um, gleichmäßig auf die Zeilen
verteilt — vom Renderer (wrapLabel, headless getestet) statt text-wrap:balance,
damit der Kasten auf die längste balancierte Zeile schrumpft. Der
23-px-Abzweig ankert an der ersten Zeile; der Optional-Kreis sitzt fest auf
der Abzweighöhe. Der Grafikexport misst die gerenderten Zeilen (labelLines)
und gibt je Zeile ein <text> aus. Dazu: die eingeblendete ID (D56) steht in
einer eigenen Zeile über dem Titel, und das Falt-Zeichen ist ein gerahmter
19×16-px-Chip mit gedeckelter Höhe. Plan: #ed.render.wrap, #ed.render M→L
(D62-Prüfung schlug korrekt an).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:45:11 +02:00
mhoennigandClaude Fable 5 1debb7d9da feat: ID-Vorschläge beim Tippen von Abhängigkeiten (:#) — D63
Wer :# tippt, bekommt die vergebenen IDs als Liste an der Schreibmarke:
Präfix- vor Teilstring-Treffern, Knotentitel als Kontext, schon gelistete
und die eigene ID ausgenommen. ↑/↓ wählt, Enter/Tab übernimmt (undo-fähig),
Esc schließt; Live-Region für Screenreader. Eingabehilfe wie die Kurzform
(D55) — der Parser sieht nie etwas davon. Regeln headless in autocomplete.js
(20 Tests), app.js verdrahtet Popup, Tasten und Einfügen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:13:18 +02:00
mhoennigandClaude Fable 5 9cb6ec1197 feat: eingeklappter Vorfahr vertritt die Cursor-Zeile im Diagramm (D38-Nachtrag 4)
Liegt der Knoten der Cursor-Zeile in einem eingeklappten Teilbaum, hebt
nodeOfLine() den nächsten sichtbaren Vorfahren hervor — Cursor-Mitlaufen,
Alt+Klick, Alt+Enter und der Mobil-Umschalter erben es über die eine
Auflösungsstelle. Ausgeblendete verworfene Elemente heben weiterhin nichts
hervor (SPEC §9). Zeilen-Map als lineTargets() headless in model.js, 5 neue
Tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 16:20:48 +02:00
mhoennigandClaude Fable 5 5b37c117e1 feat: Größen-Konflikt — Teilpakete müssen zur angegebenen Größe passen (D62)
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>
2026-08-24 15:55:16 +02:00
mhoennigandClaude Opus 5 6d58164577 fix: Abhängigkeits-Kanten laufen hinter den Knoten (D41-Nachtrag 2)
Die Basis-Kanten lagen schon richtig; „drüber gemalt" wurde allein die
Hervorhebung des ausgewählten Knotens (`svg.dep-front`, z-index 4). Eine
durchgezogene Linie quer über einen fremden Knoten durchstreicht dessen
Titel — und beim Tippen wechselt die Hervorhebung mit jeder Zeile.

`dep-front` entfällt; hervorgehobene Kanten hängen zuletzt in derselben
hinteren Ebene, liegen damit über den übrigen Kanten und unter jedem
Knoten. Export und Druck waren nie betroffen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 15:19:35 +02:00
mhoennigandClaude Opus 5 4900eb3428 feat: ID-Kurzform wird beim Doppelpunkt aufgelöst (D55-Nachtrag)
`#.kc` wird jetzt aufgelöst, sobald ein Doppelpunkt die ID abschließt —
beim Tippen des Doppelpunkts ebenso wie beim nächsten Tastendruck in
einer Zeile, die ihn schon trägt. Ohne Doppelpunkt bleibt es beim
Verlassen der Zeile.

Der Cursor allein löst weiterhin nichts aus: Angefasst wird nur die
Zeile, in der auch getippt wurde (D55/D27).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:52:29 +02:00
mhoennigandClaude Opus 5 a60c466d82 fix: Linie zum einzigen Kind riss ab (D29-Nachtrag 4)
Der Stiel aus dem Elternknoten sitzt bei 50 % der Gruppe, der Stiel zum
Kind bei `--stem-x` (der gemessenen Knotenmitte, Nachtrag 2). Für
`li:only-child` ist die waagerechte Sammelleiste abgeschaltet — richtig,
solange beide zusammenfallen, und das tun sie nur, solange der Knoten in
seiner Zelle zentriert steht. Bei einer Treppe darunter sind es gemessen
8,5 px Versatz, bei einem `li.has-or` als einzigem Kind 41,8 px.

Jetzt verbindet ein Leiterstück genau zwischen 50 % und `--stem-x` die
beiden — dieselbe L-Form wie bei einem äußeren Kind einer größeren
Gruppe; ohne `--stem-x` ist es 0 px breit und unsichtbar.

Dabei aufgefallen: `:only-child::after{border-left}` stand vor
`:last-child::after{border:0 none}` und war bei gleicher Spezifität seit
jeher wirkungslos. Der ganze Block steht nun dahinter.

Kein Rückschritt von D61 — mit ausgeschaltetem Pfad derselbe Versatz;
die Zugabe steht seither nur in voller Farbe statt blass da. Der
Grafikexport war nie betroffen (die Leiste schließt dort die Elternmitte
ein).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:20:26 +02:00
mhoennigandClaude Opus 5 36b78efa53 lean-path: Angefangenes liegt auf dem Pfad (SPEC §9, D61)
Eine Zugabe (`+`) fällt aus dem günstigsten Pfad — außer sie ist
realisiert und noch nicht erledigt (`[~]`/`[/]`). Angefangene Arbeit ist
die offene Front, und seit D46 zeigt der Pfad genau die; bisher führte
der Von-Station-zu-Station-Knopf nie dorthin, wo gearbeitet wird.
Erledigte Zugaben bleiben draußen: dort ist nichts mehr zu tun, und was
darunter offen blieb, ist mit ihnen zusammen entbehrlich (§3).

Dabei die fehlende Hälfte von D46 gefunden: SPEC §9 sagt seit jeher
„eine bereits realisierte Alternative gewinnt", umgesetzt war das aber
nur über die Kosten — und die sind allein bei `[x]`/`[^]` null. Ein
`[~]`-Zweig verlor damit gegen eine unangetastete billigere, obwohl die
XOR-Regel gerade ihn als die realisierte Alternative ausweist.
`chosenPool()` schränkt die Wahlmenge auf die realisierten ein; unter
mehreren entscheiden wieder die Kosten. Nebengewinn: eine entschiedene
Gruppe ist keine freie Variable mehr und verkleinert die Suche (D42).

10 neue Tests; Gegenprobe geführt (jede Regel einzeln herausgenommen
lässt genau die nach ihr benannten Zusicherungen fallen). Der
mitgelieferte Plan ändert sich nicht — er hat keine angefangene Zugabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:11:38 +02:00
mhoennigandClaude Opus 5 3018db9f61 notation: ohne Titel vertritt die Knoten-ID ihn (SPEC §1, D60)
`- #US-123` ergab bisher nichts — leeres Label, Zeile ignoriert, ID nicht
vergeben. Wo die Kennung schon der Name ist (Ticket-Referenzen, §11), war das
verkehrt herum: Wer den Titel nicht danebenschrieb, verlor den ganzen Knoten.
Jetzt wird `#id` das Label, mit Doppelpunkt geschrieben wie ohne.

Das `#` bleibt im Label — es sagt „hier steht die Adresse, weil es keinen
Titel gibt". Der `#`-Umschalter setzt bei so einem Knoten nichts davor
(sonst `#US-123: #US-123`), und Tooltip wie aria-label lassen die ID weg, die
schon im Titel steht.

Verhaltensänderung: Die ID ist damit vergeben — `- #auth` gefolgt von
`- [ ] Echt #auth` gibt jetzt zwei Knoten und eine duplicateId-Warnung.

SPEC §1, llms.md, D60; 6 neue Tests, der alte auf die neue Regel umgeschrieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:31:39 +02:00
mhoennigandClaude Opus 5 410dacdc6c notation: \ am Zeilenende setzt die Zeile fort (SPEC §1, D59)
Eine Zeile trägt alles auf einmal — im mitgelieferten Plan ist die längste
122 Zeichen lang, und seit das Textfeld nicht mehr umbricht (D49) muss man
dafür schieben. Ein `\` am Zeilenende verteilt sie jetzt auf mehrere
Textzeilen, ohne dass ein neuer Knoten entsteht.

Leerraum davor ist Pflicht (`… \`, nicht `…\`) — bewusst gegen die
Shell-Gewohnheit: Ohne die Regel verschluckt ein Label wie `C:\temp\` stumm
den folgenden Knoten, mit ihr bekommt man nur keine Fortsetzung, was man
sofort sieht. Der laute Fehler ist der bessere.

Umgesetzt als Vor-Durchlauf `logicalLines()`: Die Folgezeile ist Teil der
Zeile, bevor irgendetwas aus ihr gelesen wird — die Extraktionsreihenfolge
aus §1 bleibt unangetastet. Alles gehört zur ersten Zeile: Einrückung,
Warnungs-Zeilennummer, Rückschreiber; der Cursor in der Fortsetzung wählt
ihren Knoten aus, wie bei Beschreibungszeilen.

SPEC §1/§9, llms.md, Legende in neun Sprachen, D59; 20 neue Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:17:26 +02:00
mhoennigandClaude Opus 5 257aab08dd feat(news): übersetzter Hinweis auf die englischen Notizen, optisch zurückgenommen
Wer die Oberfläche auf Japanisch stehen hat, sah einen japanischen Rahmen um
englische Sätze — das kann man für eine fehlende Übersetzung halten. Ein Satz
oben im Popup sagt jetzt, dass es Absicht ist, in allen neun Sprachen; klein
und grau, damit er hinter dem Inhalt zurücksteht, zu dem er gehört. Bei
englischer Oberfläche entfällt er.

SPEC §9 und D58-Nachtrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:56:16 +02:00
mhoennigandClaude Opus 5 36154f6f68 feat: Neuigkeiten — der Stern wandert in die Kopfzeile und bekommt ein Popup
Der „Was ist neu?"-Knopf stand im Diagramm-Kopf und war verborgen, solange es
nichts gab — er konnte also nur etwas über das offene Dokument sagen, und
meistens sagte er gar nichts. Jetzt steht er permanent in der oberen
Bedienleiste und öffnet ein Popup mit der Chronik der letzten Tage; je Tag
führt ein Link die betroffenen Knoten im Diagramm vor (dieselbe gelbe
Kranz-Ansicht wie D28, nur mit einer anderen Frage).

Zwei Quellen, jede in ihrer Rolle: docs/CHANGELOG.md sagt, WAS geschehen ist
(englisch — das Popup ist Oberfläche in neun Sprachen, die Commit-Betreffs
sind deutsch); die git-Historie des mitgelieferten Plans sagt, WELCHE Knoten
sich bewegt haben. Beides wird zur Bauzeit eingelesen (Vite-Plugin), zur
Laufzeit lädt Werkbaum weiterhin nichts nach.

Bernstein heißt ungesehen, Petrol heißt „wird gerade vorgeführt". Der
Besuchsvergleich (D28) steht als abgesetzter Abschnitt zuoberst im Popup und
trägt den „gesehen"-Knopf, den vorher der Knopf selbst war.

SPEC §9 (Neuigkeiten) und D58; 20 neue Tests, davon einer auf der
ausgelieferten CHANGELOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:50:13 +02:00
mhoennigandClaude Opus 5 71bfb62b35 feat(editor): Warndreieck als Zeiger über fehlerhaften Zeilennummern
Das `?` von `cursor:help` sagte nur „hier gibt es etwas zu lesen". Über der
orange gefärbten Zahl einer Warn-Zeile steht jetzt dasselbe ⚠, das im
Warnungsbereich vor jeder Meldung steht — der Zeiger sagt dieselbe Sache wie
das Ziel, auf das er zeigt.

Der Pfeil bleibt daneben stehen (sonst zeigte der Zeiger auf nichts), Spitze
auf dem Brennpunkt; als data:-URI im Stylesheet, `help` als Rückfall.

SPEC §9 (Zeilennummern) und D33-Nachtrag 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:20:08 +02:00
mhoennigandClaude Opus 5 3fbc772d81 feat(diagram): das Knoten-Fenster löst den nativen Tooltip überall ab
Das Fenster aus D52 war für Touch gebaut — damit standen zwei Darstellungen
desselben Inhalts nebeneinander, und ausgerechnet die schlechtere bekam der
Zeiger. Jetzt zeigt es überall: am Zeiger nach 350 ms beim Überfahren, bei
Tastaturfokus sofort, auf Touch unverändert beim Tipp.

Was ein `title` prinzipiell nicht kann:
- keine Absätze (die Beschreibungen im Plan brechen bei ~76 Zeichen, das sah
  ausgefranst aus),
- keine Linie (der Trennstrich war aus 24 `─` GEMALT, D40-Nachtrag),
- nichts bei Tastaturfokus — wer mit Tab durch das Diagramm geht, sah bisher
  keine Beschreibung. Nachgemessen mit ECHTEM Tastendruck: Fokus von
  #not.line auf #not.status, Fenster folgt, 10 px unter dem Knoten, Ring am
  fokussierten Knoten.

- Inhalt zieht von `title` nach `data-tip` (sonst zeigte der Browser seinen
  eigenen zusätzlich) — dasselbe Attribut unter anderem Namen, keine
  Verdopplung der 20 kB DOM-Text.
- Die letzten drei nativen Tooltips IN Knoten mussten weichen: Das
  Warndreieck war redundant (Statusname steht in den Fakten), die Annahme
  „Größe fehlt, gilt als M" wandert in die Faktenzeile. Nachgemessen: 0 von
  161 Knoten und 0 Kinder tragen noch einen `title`.
- Absicht-Erkennung: 350 ms Verzögerung, beim Wechsel zwischen Knoten ohne
  Warten, 120 ms Nachfrist beim Verlassen (der Weg ins Fenster führt über
  einen Zwischenraum).
- `aria-hidden` am Fenster: Der Inhalt steht schon im aria-label, sonst würde
  er doppelt vorgelesen; × und ↗ deshalb `tabindex="-1"`.

Werkzeuggrenze notiert: `element.focus()` feuert ohne Fensterfokus keine
Fokus-Ereignisse (`document.hasFocus()` false) — der erste Prüflauf zeigte
fälschlich „öffnet nichts".

SPEC §9 + D57; Plan: #ed.tipwindow [x] (161 Knoten, 0 Warnungen).
293 Tests grün, 5 Snapshots/Zusicherungen auf `data-tip` nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:07:00 +02:00
mhoennigandClaude Opus 5 f0fe877d45 feat(diagram): #-Umschalter blendet die Knoten-IDs vor dem Titel ein
Die ID ist die Adresse eines Knotens — Ziel der Abhängigkeiten, Schlüssel der
Beschreibungsblöcke —, stand im Diagramm aber nur im Tooltip (D36). Jetzt
umschaltbar und geschrieben wie im Text: `#some.id: Titel`.

- Als RENDERER-Option, nicht per CSS versteckt: `diagramToSvg()` zieht den
  Knotentext aus `textContent`, ein `display:none` hätte die IDs trotzdem in
  jeden Export geschrieben. Nachgemessen im SVG: `>#not.line: Line format<`
  mit Umschalter, `>Line format<` ohne.
- Zurückgenommen dargestellt (Mono wie im Textfeld, --muted), `aria-hidden` —
  der Screenreader bekommt die ID über a11yId (D36). Zustand persistiert.

Das zehnte Bedienelement hat die Kopfzeile gekippt, zweimal dieselbe Rechnung
wie D17-Nachtrag 5:
- Schreibtisch: 738 nötig, 728 da -> 82 statt 49 px. Nicht die Knöpfe (566 px
  für elf Elemente), sondern zehn Lücken à 14 px = 140 px. `gap:10` gibt
  40 px zurück; danach 698 von 728 und wieder 49 px mit UND ohne Knopf.
- Telefon (375 px): alle Ziele schon auf 29 px, Lücken auf 6 — ein zehntes
  passt arithmetisch nicht (386 gegen 359). D50s Umbruch-Schwelle wandert von
  360 auf 440 px. Greift nur mit dem „Was ist neu?"-Knopf: mit allen zehn
  78 px/zwei Reihen, ohne ihn 49 px/eine Reihe.

SPEC §9 + D56; Plan: #ed.showids [x] (160 Knoten, 0 Warnungen).
293 Tests grün (288+5); i18n `idsTooltip` in 9 Sprachen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:55:04 +02:00
mhoennigandClaude Opus 5 285d86a2e1 feat(editor): ID-Kurzschreibweise #.kc als Eingabehilfe
Gefragt war, ob `#.kc` unter `#prod-stage` als Notation `#prod-stage.kc`
bedeuten könnte. Als Notation wäre es falsch, und das zeigt der eigene Plan:
181 IDs im Baumteil, davon 50 (28 %), deren gepunktetes Präfix gar nicht die
Eltern-ID ist (`#ed.parser` hängt unter `#ed.live`) — die Punkte benennen den
BEREICH, nicht den Pfad, genau wie D48 es vorsieht. Dazu: der
Beschreibungsteil hinter `---` hat keinen Baum (zwei Schreibweisen für eine
ID), Einrücken änderte die Identität und bräche `:#…`-Verweise, und aus einer
Konvention würde Grammatik. Gewinn wären 802 Zeichen in 40 kB.

Also als Eingabehilfe: getippt wird `#.kc`, beim Verlassen der Zeile steht
`#prod-stage.kc` im Text — derselbe Griff, mit dem das Umklappen im Diagramm
seine Faltmarke zurückschreibt (D38-Nachtrag 2). Die Datei bleibt eindeutig,
durchsuchbar und umsortierbar; llms.md ist unberührt, weil sich an der
Notation nichts ändert.

- parser.js: `expandShortIds(text)` neben `setFoldMark`, 17 Tests.
- app.js: löst beim Zeilenwechsel auf, aber nur in der Zeile, in der auch
  getippt wurde — ein bloß durchgeklicktes fremdes Dokument bleibt
  zeichengenau stehen (sonst fiele es aus dem Nachziehen, D27).

Falle, zum zweiten Mal dieselbe: `execCommand` verweigert re-entrant im
`input`-Ereignis den Dienst, `replaceTextUndoable` fällt dann auf
`src.value =` zurück und löscht die Undo-Historie. Gemessen: erstes
Rückgängig ohne Wirkung, jedes weitere `false`. Jetzt einen Zug später
(`setTimeout 0`) und nur mit Fokus im Textfeld; danach nimmt ein Rückgängig
genau die Auflösung zurück.

SPEC §9 + D55; Plan: #ed.shortid [x] (159 Knoten, 0 Warnungen).
288 Tests grün (271+17).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:47:16 +02:00
mhoennigandClaude Opus 5 e937e1bbbb refactor(snapshots): Regeln nach snapshots.js, 28 Tests
D54-Nachtrag 2 endete mit „nicht durch Tests gedeckt ... das ist der Preis
dafür, dass die Zustandslogik im UI-Modul sitzt". Der Preis war zu hoch: Der
Fehler kam bis in Produktion, und ein Test hätte ihn in einer Zeile
gefunden.

Geschnitten nach dem Vorbild von remote.js (D31) — die entscheidbare Hälfte
heraus, die I/O bleibt:
- snapshots.js: addSnapshot (wann entsteht ein Stand), dropOldestSnap /
  persistSnaps (was fliegt bei Platzmangel), parseSnaps (was darf gelesen
  werden), snapLabel. Speicher als {setItem, removeItem} und Uhr als Zahl
  hereingereicht — genau die Abhängigkeiten, an denen die Prüfbarkeit
  vorher scheiterte.
- app.js: -63/+14 Zeilen, snapshotNow() schrumpft auf sechs.

Die Gegenprobe zählt, nicht die Zahl der Tests: Baut man exakt den
ausgelieferten Fehler wieder ein (`manual ? null : base` -> `base`), fällt
GENAU die eine Zusicherung, die nach ihm benannt ist; die übrigen 27 bleiben
grün.

Zwei bewusste Verhaltensänderungen: parseSnaps wirft formfremde Einträge weg
(Liste kein Array, t keine Zahl, text kein String), und ein Array im
Speicher ergibt {} statt eines halb benutzbaren Zustands.

Verdrahtung bleibt Browser-Sache und wurde nachgemessen: Knopf ohne Eingabe
-> 1 Stand; Takt (2 s) bei bloß angesehenem Dokument -> 0, nach Änderung
-> 1, weitere Takte -> 1.

D54-Nachtrag 3 + Faustregel in frontend/CLAUDE.md. 271 Tests grün (243+28).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:37:09 +02:00
mhoennigandClaude Opus 5 c4f70ebb24 fix(editor): Knopf sicherte nichts, solange nichts geändert war
Gemeldet: „I took a manual snapshot ... List is empty." Nachgestellt und
bestätigt — schlimmste Bauart: nichts gespeichert, Knopf bestätigte
trotzdem.

Ursache ist der Denkfehler aus D54-Nachtrag 1: `snapshotNow()` vergleicht
bei leerer Liste gegen `snapBase`, den Text beim Öffnen des Dokuments. Für
den Takt richtig (bloßes Ansehen sammelt nichts), für den Knopf falsch. Die
Begründung „bei unverändertem Text steht er schon oben in der Liste" gilt
eben nicht, wenn die Liste leer ist — und das ist genau der Anlass des
Knopfes: öffnen, sichern, DANN ändern. Die frühere Messung begann mit einer
Eingabe und prüfte damit die Wirkung, nicht den Anlass.

- `snapshotNow(manuell)`: Die snapBase-Sperre gilt nur noch für den Takt;
  der Knopf vergleicht allein gegen den letzten Eintrag.
- `loadSnapshot()` sagt jetzt ebenfalls `true` — bewusstes Wegleg-Ereignis.
- `setInterval(() => snapshotNow(), …)`: Ein durchgereichtes Argument wäre
  wahr und hebelte die Sperre aus (dieselbe Falle wie D17-Nachtrag 4).

Nachgemessen — Knopf: 10 Zeilen, Klick ohne jede Eingabe -> [10]; nach einer
Änderung -> [10,11]; ältesten laden -> zurück auf 10, kein Doppel-Eintrag.
Takt (2 s für die Prüfung): nur angesehen, 3 Takte -> 0; nach Änderung -> 1;
weitere Takte -> 1.

D54-Nachtrag 2. 243 Tests grün (die Stände selbst sind nicht test-gedeckt,
sie leben in app.js — Lücke benannt).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:28:49 +02:00
mhoennigandClaude Opus 5 f7d963809a feat(editor): Knopf, der einen Stand von Hand sichert
Zehn Minuten sind der falsche Takt für den einen Moment, in dem man einen
Stand wirklich will: unmittelbar vor einer größeren Änderung. Dann ist der
letzte selbsttätige Stand im Zweifel neun Minuten alt.

- Kamera statt Plus: Ein `+` neben dem Dokument-Namen läse sich als „neues
  Dokument“, ein Lesezeichen als „Favorit“. Gegen Uhr (Verlauf) und Caret
  (Dokumentwahl) ist die Kamera eindeutig.
- Die Rückmeldung kommt IMMER, auch wenn nichts angelegt wurde: Der Knopf
  verspricht „dein Stand ist gesichert“, nicht „ein Eintrag wurde erzeugt“.
  Bei unverändertem Text steht er schon oben in der Liste — einen doppelten
  Eintrag zu erzwingen kostete nur einen der 20 Plätze.
- Gezeigt per vorhandenem Haus-Idiom (`flashBtn`, 1,5 s Haken); dafür nur
  `.copybtn.done .ic-copy` um `.ic-main` erweitert.
- Pad-Fall folgt von selbst: beide Knöpfe in EINER Zuweisung versteckt.
- i18n `snapAddTooltip` in 9 Sprachen; der Leer-Text des Menüs nennt jetzt
  beide Wege.

Nachgemessen: 0 -> Klick -> 1 (Haken sichtbar, nach 1,7 s wieder Kamera);
zweiter Klick ohne Änderung -> weiterhin 1, Rückmeldung trotzdem; nach einer
Änderung -> 2. Im schmalen Layout kostet der Knopf nichts (Kopfhöhe 44 px
und Zeilenzahl mit und ohne ihn identisch).

D54-Nachtrag; Plan: #ed.snaps.manual [x] (158 Knoten, 0 Warnungen).
243 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:21:06 +02:00
mhoennigandClaude Opus 5 48d3174684 feat(site): llms.txt als Wegweiser + Charset-Fix für llms.md
Anlass war die Frage, ob `llms.md` ein guter Name ist. Zwei Befunde:

1. Der Zweck der Konvention ist ein anderer, als D43 annahm. llmstxt.org
   über die eigene Datei: „a markdown file that provides brief background
   information and guidance, along with links to markdown files providing
   more detailed information“ — ein Index, kein Inhalt. Der 211-Zeilen-
   Leitfaden ist genau eine jener verlinkten Dateien; `llms.md` ist damit
   der richtige Name, es fehlte der Wegweiser davor.

2. `llms.md` kam auf der stabilen Instanz falsch kodiert an: Apache kennt
   `.md` nicht und sendet GAR KEINEN Content-Type, der Browser rät
   windows-1252. Gemessen: characterSet=windows-1252, aus „notation —
   guide“ wurde „notation â€" guide“, 31 Zeilen betroffen. GitHub Pages
   liefert dieselbe Datei korrekt als text/markdown; charset=utf-8 aus.

- frontend/public/llms.txt: Index nach der Konvention (Titel, Blockquote,
  Notation in Kurzform, ## Docs, ## Optional). Rein ASCII — er ist die
  Datei, die ein fremder Agent ungefragt abruft, und soll auch dort
  ankommen, wo ein Server die Kodierung verschweigt. Alle 5 Links: 200.
- scripts/prod.htaccess: AddType für .md/.txt/.werkbaum, von
  deploy-prod.sh als .htaccess gespiegelt. Nicht in public/ — dort landete
  es wirkungslos im Pages-Artefakt. Rückweg bei 500 steht in der Datei.
- Beide Deploy-Wege kopieren llms.txt mit.

SPEC §13 + D43-Nachtrag 2 (mit Richtigstellung der D43-Annahme);
Plan: #not.llms.index [x]. 243 Tests grün, Plan 157 Knoten, 0 Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:01:51 +02:00
mhoennigandClaude Opus 5 cb966e4e20 feat(editor): Warn-Zeilennummer trägt ihre Meldung als Tooltip
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 &amp; 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>
2026-08-24 10:41:05 +02:00