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.
12 KiB
Werkbaum
English · Deutsch
▶ Live ausprobieren: https://werkbaum.javagil.de (stabil) · https://mhoennig.github.io/werkbaum/ (jeweils neuester Build 🚧)
Eine textuelle, Markdown-artige Notation für Projektstrukturpläne (Work Breakdown Structure) mit Und/Oder-Zerlegung — und ein Live-Editor, der sie als Diagramm rendert.
[~] Werkbaum (XL) https://wiki.example.de/relaunch
- [~] Dokumentenspeicher
| [x] Textdatei mit Copy+Paste im Frontend (S)
- [x] Parser
- [x] Texteingabefeld im Frontend
| [ ] Backend
- [~] Darstellung/Rendern (XL)
- [/] H (S) @anna
- [ ] CMS-Anbindung (M)
| [ ] WordPress
| [?] Headless CMS
- = Pflicht-Teilpaket (all of, im Diagramm nebeneinander) ·
| = Alternative (any of, untereinander) · […] = Status ·
(M) = T-Shirt-Aufwand · @name = Zuständigkeit · %% = Kommentar.
Nutzung
Den gehosteten Editor öffnen — links Text bearbeiten, rechts entsteht das Diagramm live. Toggles: transponierte (schmale) Darstellung, verworfene Elemente einblenden.
Zwischen Diagramm und Text springen
Diagramm und Editor sind in beide Richtungen verknüpft:
- Alt+Klick auf einen Knoten (Tastatur: Alt+Enter, Touch: langer Druck) markiert seine Zeile im Texteditor — ein zugeklapptes Editor-Panel öffnet sich dabei zuerst.
- Cursor im Text bewegen hebt den zugehörigen Knoten im Diagramm hervor.
Der einfache Klick behält seine bisherige Bedeutung: Ein Knoten mit URL öffnet sie weiterhin.
Diagramm aus einer URL laden
Der Editor kann seinen Notationstext über den Query-Parameter sourceUrl aus
einer externen Textdatei beziehen — praktisch zum Teilen eines Plans oder wenn
die Quelle in Git oder einem Wiki gepflegt wird:
https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-0.werkbaum
Fertige Beispiele in docs/examples/ — nacheinander
öffnen: Jedes wird ein eigenes Dokument, danach lässt sich in der
Editor-Titelzeile zwischen allen umschalten:
| Beispiel | Zeigt |
|---|---|
| ▶ 0 · Online-Shop-Relaunch | ein Softwareprojekt mit allen acht Status |
| ▶ 1 · Neue Küche | ein Plan ohne Software, viele Alternativen — gut für den Günstigster-Pfad-Schalter |
| ▶ 2 · Community-Konferenz | ein breiter Plan mit vielen Beteiligten; horizontal vs. kompakt vergleichen |
| ▶ 3 · Drei Arbeitsstränge | mehrere Wurzeln = mehrere Bäume nebeneinander |
| ▶ Werkbaum selbst | Bestand und mögliche Weiterentwicklung |
(Die Links zeigen auf den latest build 🚧; die stabile Instanz kann sourceUrl
ab ihrem nächsten Produktions-Deploy.)
Der geladene Text wird als eigenes Dokument geführt, dessen Name die URL ist — eigene Dokumente bleiben unberührt, und derselbe Link aktualisiert dieses eine Dokument, statt Kopien anzuhäufen. Die URL ist die Quelle der Wahrheit: Sie wird bei jedem Laden neu geholt, lokale Änderungen daran überleben ein Neuladen also nicht.
Notationsdateien tragen die Endung .werkbaum (UTF-8; siehe docs/SPEC.md
§12 und D24) — Konvention, kein Vertrag: sourceUrl liest jede per http(s)
erreichbare Textdatei, unabhängig von Endung und Content-Type.
Einschränkung — CORS: Der Browser lädt fremde Hosts nur, wenn sie
Access-Control-Allow-Origin senden. raw.githubusercontent.com und
GitLab-Raw-Links tun das; ein beliebiger Webserver oft nicht. Scheitert das
Laden, bleibt der bisherige Stand stehen und eine Warnung nennt die Ursache.
Zugelassen sind nur http/https.
Zusammen an einem Plan arbeiten (Etherpad)
Für Zusammenarbeit in Echtzeit braucht Werkbaum kein eigenes Backend — es leiht sich ein Etherpad. Angegeben wird die Pad-Adresse, wie sie im Browser steht (ohne Export-Pfad, den hängt Werkbaum selbst an):
https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/mein-plan
Alle bearbeiten den Notationstext im Pad; ein Neu-laden-Knopf neben dem Pad-Knopf holt den aktuellen Stand. Das Zusammenführen gleichzeitiger Änderungen macht Etherpad — genau darum geht es.
Bewusst kein Abrufen im Hintergrund: Etherpad drosselt den Export (serienmäßig 10 Abrufe je 90 s und IP), ein Takt gewinnt dagegen nicht — er erzeugt die Drosselung. Nachgemessen: Nach Überschreiten des Budgets hält der Server die Verbindung rund zwei Minuten ohne Antwort offen und liefert dann in 0,4 s. Der Knopf greift gut mit „Was ist neu?" zusammen: drücken, und was seither in Produktion ging, leuchtet auf.
Weil das Pad die Schreibfläche ist, ist das Textfeld hier schreibgeschützt; ein Knopf in der Editor-Titelzeile öffnet das Pad im neuen Tab. Ohne den Schutz verschwände getippter Text beim nächsten Abruf.
Getestet gegen Etherpad: Der Klartext-Export sendet
Access-Control-Allow-Origin: *, und die Notation kommt byte-identisch
zurück — führende Leerzeichen, -/+/|, Statusboxen und %% überleben
Etherpads Speichermodell, das - wird nicht zur Aufzählung umgedeutet.
Bedenke: Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist
für jeden lesbar, der die Adresse kennt. Siehe docs/DECISIONS.md D31.
Lokal ausführen
Die Editor-Quelle liegt jetzt als ES-Module unter frontend/src/, gebündelt mit
Vite (siehe docs/DECISIONS.md D19). Da Browser
ES-Modul-Importe über file:// blocken, funktioniert das direkte Öffnen von
frontend/index.html nicht mehr — stattdessen:
cd frontend
npm install # einmalig
npm run dev # Dev-Server unter http://localhost:8137
npm test # Vitest-Unit-Tests
npm run build # -> frontend/dist/index.html (eine self-contained Datei)
Die gebaute dist/index.html inlint JS, CSS und Favicon — diese Datei öffnet
also standalone per file:// und ist zugleich das, was deployt wird.
Build-Hinweis & eigene Produktions-Installation
Nicht-produktive Builds tragen hinter dem Titel einen kleinen Hinweis (Symbol + Tooltip), damit klar ist, dass es nicht die stabile Instanz ist:
- Dev-Server (
npm run dev) → 🔧 „Vorschau – lokaler Entwicklungsstand" - Default-Build (
npm run build, u. a. der GitHub-Pages-Deploy) → 🚧 „Aktueller Entwicklungsstand (latest build) – kann noch Fehler enthalten"
Für die eigene produktive Installation wird der Hinweis abgeschaltet:
cd frontend
npm ci # oder: npm install
npm run build:prod # -> frontend/dist/index.html OHNE Hinweis
build:prod läuft im Vite-Modus prod; frontend/.env.prod setzt dabei
VITE_BUILD_BADGE=none, wodurch der Badge-Code komplett wegoptimiert wird (er
steht dann nicht einmal mehr im Quelltext der Ausgabe). Die entstandene
dist/index.html legst du standalone auf deinen Webspace/Server (file://-
tauglich). Steuerung im Detail: app.js (mountBuildBadge), docs/DECISIONS.md
D16.
Zwei Dinge, die sonst nur der Pages-Workflow erledigt und die du im Eigenbetrieb
selbst geradeziehst: der Footer-Link MIT-License zeigt relativ auf
../LICENSE (lege die Datei eine Ebene über index.html ab oder passe den Link
an), und die Versionsnummer bleibt der Quelltext-Platzhalter 1.0 (der
Workflow ersetzt ihn sonst aus VERSION + Commit-Zahl).
Bequemer: scripts/deploy-prod.sh. Das Skript macht genau den obigen
Prod-Build und die beiden Nacharbeiten (LICENSE danebenlegen + Link
geradeziehen, Footer-Version + Commit-Link wie beim Pages-Workflow) und spiegelt
das Ergebnis per rsync/SSH auf einen Server:
scripts/deploy-prod.sh mih00@mih00.hostsharing.net:~/doms/werkbaum.javagil.de/htdocs-ssl
Das Ziel ist entweder dieses Argument oder — ohne Argument — die Variable
DEPLOY_TARGET aus der git-ignorierten Datei .env (Vorlage:
.env.example, einmal kopieren und den Pfad eintragen). Ein
Argument hat Vorrang.
Ohne -y zeigt es zuerst eine --dry-run-Vorschau und fragt nach. rsync --delete sorgt dafür, dass am Ziel nichts Altes stehen bleibt — das
Zielverzeichnis gilt also als exklusiv für Werkbaum (eine laufende Let's-Encrypt-
Challenge unter .well-known/ wird per --filter=protect ausgenommen, web-
taugliche Rechte 755/644 werden erzwungen). (Hostsharing: eine direkt
aufgeschaltete Domain liefert aus …/htdocs-ssl/; als Subdomain unter einer
anderen Domain läge das Web-Verzeichnis in …/subs-ssl/<name>/.)
Projektdokumente
frontend/— Editor ·backend/— Kotlin/Spring (Gerüst folgt, siehe backend/README.md)docs/SPEC.md— verbindliche Sprachdefinitiondocs/DECISIONS.md— Design-Entscheidungen mit Begründungdocs/ROADMAP.md— Mermaid-Plugin, Taiga-Integration, Tenzudocs/TASKS.md— offene Aufgaben (Checkboxen)docs/brand/BRAND.md— Logo, Wortbild, Anwendungsregelndocs/design/— Design-Herleitung der MarkeCLAUDE.md— Projektkontext für Claude Code
Deployment
Der Editor wird per GitHub Actions als statische Seite auf GitHub Pages
veröffentlicht (Workflow: .github/workflows/pages.yml). Ausgelöst bei jedem
Push auf main sowie manuell (workflow_dispatch).
Der Workflow richtet Node ein, führt npm ci, npm test (Vitest) und
npm run build (Vite) aus und veröffentlicht die gebündelte
frontend/dist/index.html als index.html an der Wurzel-URL, dazu LICENSE
für den MIT-Link im Footer. Das Favicon ist im Build bereits inline, es muss also
nichts weiter kopiert werden; nur der Laufzeit-Link ../LICENSE wird auf der
Kopie geradegezogen. Ein fehlschlagender Test blockiert das Deployment.
backend/ und die übrigen docs/ werden nicht veröffentlicht.
Beim Zusammenstellen setzt der Workflow zudem die Versionsnummer im Footer:
Major.Minor stammt aus der Datei VERSION (per bewusstem „Bump-Commit"
gepflegt), die Micro-Stelle aus der Zahl der Commits seit diesem letzten
Bump — sie steigt also mit jedem Commit und beginnt nach einem Bump wieder bei
0 (Werkbaum 1.0.0, 1.0.1, … dann VERSION auf 1.1 bumpen → 1.1.0). Es
wird nichts ins Repo zurückgeschrieben. Im Footer verlinkt der Name Werkbaum
die Repo-Startseite, die Versionsnummer genau den zugehörigen Commit
(…/commit/<sha>). Lokal geöffnet zeigt der Editor den Platzhalter aus dem
Quelltext (Werkbaum 1.0).
Einmalige Einrichtung: In den Repo-Settings unter Pages als Source „GitHub Actions" wählen. Das Repo muss dafür öffentlich sein (GitHub Pages via Actions ist für private Repos nur mit kostenpflichtigem Plan verfügbar).
Lizenz
MIT — siehe LICENSE. © 2026 Michael Hönnig. Die mitgelieferten
IBM-Plex-Schriften stehen unter der SIL Open Font License 1.1 (siehe
LICENSE und frontend/src/fonts/OFL.txt).
