Files
werkbaum/README.md
T
mhoennig 793561c873 frontend: Pad auf Knopfdruck holen statt zu takten — Etherpad drosselt
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.
2026-07-30 12:59:14 +02:00

12 KiB
Raw Blame History

Werkbaum logo

Werkbaum

English · Deutsch

▶ Try it live: https://werkbaum.javagil.de (stable) · https://mhoennig.github.io/werkbaum/ (latest build 🚧)

A textual, Markdown-like notation for work breakdown structures with and/or decomposition — plus a live editor that renders it as a diagram.

[~] Werkbaum (XL) https://wiki.example.de/relaunch
  - [~] Document store
    | [x] Text file with copy+paste in the frontend (S)
        - [x] Parser
        - [x] Text input field in the frontend
    | [ ] Backend
  - [~] Display/rendering (XL)
    - [/] H (S) @anna
    - [ ] CMS integration (M)
      | [ ] WordPress
      | [?] Headless CMS

- = mandatory sub-package (all of, side by side in the diagram) · | = alternative (any of, stacked) · […] = status · (M) = T-shirt effort · @name = responsibility · %% = comment.

Usage

Werkbaum editor: live diagram on top, text notation below, with status colours, T-shirt sizes, tags and export buttons

Open the hosted editor — edit text on the left, the diagram is built live on the right. Toggles: transposed (narrow) layout, show discarded elements.

Jumping between diagram and text

Diagram and editor are linked in both directions:

  • Alt+click a node (keyboard: Alt+Enter, touch: long press) selects its line in the text editor — collapsed editor panel opens first.
  • Moving the cursor in the text highlights the matching node in the diagram.

A plain click keeps its old meaning: a node carrying a URL still opens it.

Loading a diagram from a URL

The editor can pull its notation text from an external text file via the sourceUrl query parameter — handy for sharing a plan or keeping the source in Git or a wiki:

https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-0.werkbaum

Ready-made examples in docs/examples/open them one after another, each becomes its own document, so afterwards you can switch between all of them in the editor title bar:

Example Shows
0 · Online shop relaunch a software project with all eight states
1 · New kitchen a non-software plan, lots of alternatives — good for the cheapest-path toggle
2 · Community conference a wide plan with many people; compare horizontal vs. compact
3 · Three workstreams several roots = several trees side by side
Werkbaum itself what exists today and where it could go

(The links point at the latest build 🚧; the stable instance supports sourceUrl from its next production deploy on.)

The loaded text becomes its own document whose name is the URL, so your own documents stay untouched; the same link updates that one document instead of piling up copies. The URL is the source of truth — it is re-fetched on every load, so local edits to it do not survive a reload.

Notation files use the extension .werkbaum (UTF-8; see docs/SPEC.md §12 and D24) — a convention, not a contract: sourceUrl reads any text file over http(s) regardless of extension or content type.

Caveat — CORS: the browser only fetches a foreign host if it sends Access-Control-Allow-Origin. raw.githubusercontent.com and GitLab raw links do; an arbitrary web server often does not. If loading fails the previous content stays and a warning explains why. Only http/https are allowed.

Working on one plan together (Etherpad)

For real-time collaboration Werkbaum needs no backend of its own — it borrows an Etherpad. Pass the pad address as it appears in your browser (no export path, Werkbaum appends that itself):

https://werkbaum.javagil.de/?etherpad=https://pad.hostsharing.net/p/my-plan

Everyone edits the notation text in the pad; a reload button next to the pad button fetches the current state. Merging simultaneous edits is Etherpad's job — that is the whole point.

Deliberately no background polling: Etherpad rate-limits the export (10 fetches per 90 s per IP by default), so a timer does not win against it — it causes the throttling. Measured: after exceeding the budget the server holds the connection open with no reply for about two minutes, then answers in 0.4 s. The button pairs well with "what's new": press it, and whatever went into production since your last look lights up.

Because the pad is the writing surface, the text area here is read-only; a button in the editor title bar opens the pad in a new tab. Without that protection, anything you typed would vanish on the next fetch.

Verified against Etherpad: the plain-text export sends Access-Control-Allow-Origin: *, and the notation comes back byte-identical — leading spaces, -/+/|, status boxes and %% survive Etherpad's storage model, and - is not turned into a bullet list.

Be aware: your plan text then lives on third-party infrastructure, and a pad is readable by anyone who knows its address. See docs/DECISIONS.md D31.

Running it locally

The editor source now lives as ES modules under frontend/src/, bundled by Vite (see docs/DECISIONS.md D19). Because browsers block ES-module imports over file://, opening frontend/index.html directly no longer works — use one of:

cd frontend
npm install          # once
npm run dev          # dev server at http://localhost:8137
npm test             # Vitest unit tests
npm run build        # -> frontend/dist/index.html (single self-contained file)

The built dist/index.html inlines all JS, CSS and the favicon, so that file does open standalone via file:// and is what gets deployed.

Build hint & your own production install

Non-production builds carry a small hint next to the title (symbol + tooltip) so it's clear this is not the stable instance:

  • Dev server (npm run dev) → 🔧 "Preview local development build"
  • Default build (npm run build, incl. the GitHub Pages deploy) → 🚧 "Latest build may still be buggy"

For your own production install the hint is switched off:

cd frontend
npm ci                # or: npm install
npm run build:prod    # -> frontend/dist/index.html WITHOUT the hint

build:prod runs Vite in mode prod; frontend/.env.prod sets VITE_BUILD_BADGE=none, so the badge code is tree-shaken away entirely (it isn't even present in the output source). Drop the resulting dist/index.html onto your web space/server (standalone, file://-capable). Details: app.js (mountBuildBadge), docs/DECISIONS.md D16.

Two things otherwise handled only by the Pages workflow, to fix up yourself when self-hosting: the footer MIT-License link is relative to ../LICENSE (place that file one level above index.html, or adjust the link), and the version number stays the source placeholder 1.0 (the workflow otherwise replaces it from VERSION + commit count).

Easier: scripts/deploy-prod.sh. The script runs exactly that prod build and both fix-ups (drop LICENSE alongside + straighten the link, footer version + commit link just like the Pages workflow) and mirrors the result to a server via rsync/SSH:

scripts/deploy-prod.sh mih00@mih00.hostsharing.net:~/doms/werkbaum.javagil.de/htdocs-ssl

The target is either this argument or — with no argument — the DEPLOY_TARGET variable from the git-ignored file .env (template: .env.example, copy it once and fill in the path). An argument takes precedence.

Without -y it first shows a --dry-run preview and asks for confirmation. rsync --delete ensures nothing old is left at the target — so the target directory is treated as exclusive to Werkbaum (an in-flight Let's Encrypt challenge under .well-known/ is spared via --filter=protect, and web-friendly 755/644 permissions are enforced). (Hostsharing: a directly-served domain is served from …/htdocs-ssl/; as a subdomain under another domain the web directory would be …/subs-ssl/<name>/.)

Project documents

  • frontend/ — editor · backend/ — Kotlin/Spring (scaffold to follow, see backend/README.md)
  • docs/SPEC.md — normative language definition
  • docs/DECISIONS.md — design decisions with rationale
  • docs/ROADMAP.md — Mermaid plugin, Taiga integration, Tenzu
  • docs/TASKS.md — open tasks (checkboxes)
  • docs/brand/BRAND.md — logo, wordmark, usage rules
  • docs/design/ — design derivation of the brand
  • CLAUDE.md — project context for Claude Code

Note: The detailed project documentation under docs/ is maintained in German, the project's source language (see CLAUDE.md). This README is available in English and German.

Deployment

The editor is published as a static page on GitHub Pages via GitHub Actions (workflow: .github/workflows/pages.yml). Triggered on every push to main and manually (workflow_dispatch).

The workflow sets up Node, runs npm ci, npm test (Vitest) and npm run build (Vite), then publishes the bundled frontend/dist/index.html as index.html at the root URL, plus LICENSE for the MIT link in the footer. The favicon is already inlined into the build, so nothing else needs copying; only the runtime ../LICENSE link is straightened on the copy. A failing test blocks the deploy. backend/ and the remaining docs/ are not published.

While assembling the site, the workflow also stamps the version into the footer: major.minor comes from the VERSION file (bumped by an explicit "bump commit"), and the micro part is the number of commits since that last bump — so it grows with every commit and resets to 0 right after a bump (Werkbaum 1.0.0, 1.0.1, … then bump VERSION to 1.11.1.0). Nothing is written back to the repo. In the footer the name Werkbaum links to the repository, while the version number links to that exact commit (…/commit/<sha>). Opened locally, the editor shows the source placeholder (Werkbaum 1.0).

One-time setup: In the repo settings under Pages, select Source = "GitHub Actions". The repo must be public for this (GitHub Pages via Actions is only available for private repos on a paid plan).

Stable instance

scripts/deploy-prod.sh mirrors a badge-free production build to a server over SSH (target in .env, template .env.example). Unlike Pages this is a deliberate step, so it is also the moment a feature actually goes live.

The script therefore starts by running scripts/promote-shipped.sh, which turns [x] (done) into [^] (in production) in docs/examples/example-werkbaum.werkbaum — the shipped plan describing Werkbaum itself — and records that as its own commit. The convention is: mark a finished feature [x] when it is merged and let the deploy promote it. That keeps the plan honest on both instances and keeps the "what's new" highlighting meaningful. Skip it with --no-promote; see docs/DECISIONS.md D30.

The commit is not pushed automatically — run git push afterwards, or the footer version link points at a commit GitHub does not know yet.

License

MIT — see LICENSE. © 2026 Michael Hönnig. The bundled IBM Plex fonts are under the SIL Open Font License 1.1 (see LICENSE and frontend/src/fonts/OFL.txt).