Das Einbetten ist möglich, ohne D25 zu opfern. Nachgemessen zuerst, ob der
Server es überhaupt zulässt: pad.hostsharing.net sendet kein
`X-Frame-Options` und keine CSP mit `frame-ancestors`, und im Versuch baute das
Pad in einem fremdstämmigen Rahmen seine inneren Editorframes auf
(`contentWindow.length === 2` = ace_outer/ace_inner), samt Werkzeugleiste,
heiler Einrückung und lebender Socket-Verbindung.
Einbetten heißt aber nicht Ersetzen: Ein cross-origin-iframe gibt keinen
DOM-Zugriff, ein ersetztes Textfeld kostete also beide Richtungen von D25
(Alt+Klick -> Zeile, Cursor-Zeile -> Knoten). Deshalb drei Ansichten, reihum
über einen Wähler in der Editor-Titelzeile:
- Pad und Text, geteilt durch einen eigenen Splitter (Mechanik und Idiom vom
Legenden-Splitter aus D26: von der Spiegelseite gezogen, Doppelklick setzt
zurück, `--pcol`/`--prow` getrennt gehalten und persistiert). Der Spiegel darf
schmal werden und trägt weiter die Sprünge — die arbeiten auf unserem
eigenen `<textarea>`.
- nur Pad — Spiegel ausgeblendet; `revealEditor()` schaltet bei einem Sprung
auf „beide", sonst zeigte der Sprung ins Nichts.
- nur Text — wie bisher, kein Rahmen.
Ein Wähler statt dreier Knöpfe, weil die Titelzeile schon Dokument, Pad, Neu
laden, Kopieren, Legende und Fensterknöpfe trägt (mobil dreifach eng, D17) —
derselbe Reihum-Griff wie beim Modus-Wähler dort. Der Zustand steckt im Symbol
(geteilt / links gefüllt / rechts gefüllt), der Tooltip nennt ihn im Klartext.
Der Rahmen bekommt nur sichtbar ein `src`, sonst `about:blank`. Das ist kein
Geiz um Bytes: Ein geladenes Pad verbindet sich per Socket und macht dich in
dessen Anwesenden-Liste sichtbar. „Nur Text" ist damit die Ansicht, die nichts
von dir verrät.
Zwei Dinge, die beim Prüfen auffielen und behoben sind:
- Eine überzählige `}` ließ das Modul gar nicht laden. Die Messung sah trotzdem
plausibel aus, weil die abgefragten Werte die statischen HTML-Vorgaben waren —
seither prüft jede Messung zuerst, ob überhaupt Knoten gerendert sind, und
`node --check` läuft vor dem Blick in den Browser.
- In „nur Pad" war der Spiegel weiterhin sichtbar (sogar breiter als das Pad):
die Regel `.src-area.pv-pad>textarea{display:none}` fehlte.
Bleibender Preis: `Set-Cookie: token=…; SameSite=Lax`. Das Cookie ist Etherpads
Autoren-Identität und wird im fremdstämmigen Rahmen nicht mitgesendet — man ist
bei jedem Laden ein neuer Autor. Nur serverseitig zu beheben
(`cookie.sameSite: "None"`), nicht in Werkbaum. Steht in D31.
Auf kleinem Bildschirm nachgemessen statt geschätzt: bei 375x812 bleiben Pad
317 px und Spiegel 180 px, per Splitter verschiebbar — brauchbar, also keine
Sonderregel für Mobil.
SPEC §9 und D31 nachgezogen; 5 neue i18n-Schlüssel in allen 9 Sprachen.
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
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 definitiondocs/DECISIONS.md— design decisions with rationaledocs/ROADMAP.md— Mermaid plugin, Taiga integration, Tenzudocs/TASKS.md— open tasks (checkboxes)docs/brand/BRAND.md— logo, wordmark, usage rulesdocs/design/— design derivation of the brandCLAUDE.md— project context for Claude Code
Note: The detailed project documentation under
docs/is maintained in German, the project's source language (seeCLAUDE.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.1 → 1.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).
