Beim gemeinsamen Arbeiten an einem Pad (D31) fehlte das Naheliegendste: „schau mal hier". Etherpad zeigt die Cursor der anderen im Pad, aber Werkbaum kommt nicht an sie heran — ein fremdstämmiger Rahmen gibt keinen DOM-Zugriff, der Klartext-Export überträgt nur Text. Eine Marke IM Text ist damit nicht der Notbehelf, sondern der einzige Weg. Und sie hat etwas, das ein Cursor nicht hat: alle sehen dieselbe Stelle. Erkannt wird `!!!` nur ALLEINSTEHEND (`(^|\s)!!!(?=\s|$)`), sonst verlöre jedes „Achtung!!!" seine Ausrufezeichen und niemand fände den Grund. Bewusst ohne Lookbehind — Safari kennt es erst ab 16.4, und die Notation soll nicht an einer Browserversion hängen. Position in der Extraktion (SPEC §1): nach den Tags; der Kommentar fällt vorher weg, eine Marke hinter `%%` wirkt also nicht. Darstellung ist DIESELBE wie die der Cursor-Zeile (D25), nicht eine vierte: neben Fokusrahmen, Strahlenkranz (D28) und Cursor-Ring wäre ein weiterer Ring ein Zeichen zu viel — es gibt nur einen Begriff „hier schauen". Preis: Im Bild ist nicht zu unterscheiden, ob die eigene Cursor-Zeile oder eine fremde Marke leuchtet; für Screenreader benennt `a11yFocusMark` es ausdrücklich. Ins Bild geholt wird nur bei ÄNDERUNG der Marke, Schlüssel ist der Label-Text statt der Zeilennummer (Umsortieren im Pad soll nicht neu scrollen). Ohne diese Bedingung zöge jeder Neubau den Blick zurück und man käme nicht weg. Beides geprüft: gleiche Marke -> Scrollstand bleibt 0, Marke gewandert -> springt auf 242 und markiert den neuen Knoten. Beim Hinsehen gefunden: Die Pfad-Inversion verschluckte den Zeigefinger. Der erste markierte Knoten im Test war eine nicht gewählte any-of-Alternative („Headless CMS") — `opacity:.32; saturate(.4)`, der Ring praktisch unsichtbar. Der Zeiger zeigte auf nichts. Dieselbe Ausnahme wie für D28 behebt es, und hier ist sie zwingender: „Sollten wir diese verworfene Alternative noch einmal ansehen?" ist einer der wahrscheinlichsten Gründe, überhaupt zu zeigen. Anders als beim optionalen Knoten (D29), wo das Zurücktreten die Aussage IST. Nicht im Druck und nicht im Grafikexport (wie die Cursor-Zeile): Die Marke sagt „schau jetzt hierhin", nicht „so ist der Plan". Nicht ins kanonische Beispiel (SPEC §10) aufgenommen — das ist zugleich Test-Fixture, ein dauerhafter Zeigefinger darin wäre eine Aussage, die niemand gemacht hat. Verworfen war `%%!` (Kommentar-Variante, bliebe harmlos liegen und könnte die Begründung tragen) — der Nutzer wählte `!!!`, weil es im Moment des Zeigens schnell zu tippen ist, und genau dann will man nicht über Syntax nachdenken. Der Preis steht in D32: die Marke bleibt sichtbar stehen, bis jemand sie löscht; Werkbaum kann sie nicht entfernen (kein Schreibzugriff auf das Pad). SPEC §1/§9 zuerst, dann D32, dann Code. 15 neue Tests (86 -> 101), Legende und i18n in allen 9 Sprachen.
258 lines
13 KiB
Markdown
258 lines
13 KiB
Markdown
<p>
|
||
<img src="docs/brand/logo.svg" width="72" alt="Werkbaum-Logo">
|
||
</p>
|
||
|
||
# Werkbaum
|
||
|
||
[English](README.md) · **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](https://werkbaum.javagil.de) ö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/`](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](https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-0.werkbaum)** | ein Softwareprojekt mit allen acht Status |
|
||
| **▶ [1 · Neue Küche](https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-1.werkbaum)** | ein Plan ohne Software, viele Alternativen — gut für den Günstigster-Pfad-Schalter |
|
||
| **▶ [2 · Community-Konferenz](https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-2.werkbaum)** | ein breiter Plan mit vielen Beteiligten; horizontal vs. kompakt vergleichen |
|
||
| **▶ [3 · Drei Arbeitsstränge](https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-plan-3.werkbaum)** | mehrere Wurzeln = mehrere Bäume nebeneinander |
|
||
| **▶ [Werkbaum selbst](https://mhoennig.github.io/werkbaum/?sourceUrl=https://raw.githubusercontent.com/mhoennig/werkbaum/main/docs/examples/example-werkbaum.werkbaum)** | 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.
|
||
|
||
Das Pad lässt sich auch **im Editor-Panel einbetten**: Ein Wähler in der
|
||
Titelzeile schaltet reihum zwischen *Pad und Text* (durch einen Splitter frei
|
||
geteilt), *nur Pad* und *nur Text*. Der schmal gezogene Textspiegel behält seinen
|
||
Zweck — der Sprung zwischen Diagramm und Text arbeitet auf ihm, und in *nur Pad*
|
||
holt ein Sprung ihn selbst zurück. Der Rahmen wird nur geladen, solange er
|
||
sichtbar ist, denn ein geladenes Pad verbindet sich und zeigt dich in dessen
|
||
Anwesenden-Liste.
|
||
|
||
**Der gemeinsame Zeigefinger:** Schreib `!!!` in eine Zeile, und dieser Knoten
|
||
wird hervorgehoben und ins Bild geholt — bei **allen**, die auf das Pad schauen.
|
||
Das kann ein Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!`
|
||
bleibt also ein gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie
|
||
löscht.
|
||
|
||
**Bedenke:** Der Plantext liegt damit auf fremder Infrastruktur, und ein Pad ist
|
||
für jeden lesbar, der die Adresse kennt. Im eingebetteten Rahmen wird Etherpads
|
||
Autoren-Cookie (`SameSite=Lax`) nicht mitgesendet — man gilt bei jedem Laden als
|
||
neuer Autor, was nur serverseitig zu beheben ist (`cookie.sameSite: "None"`).
|
||
Siehe `docs/DECISIONS.md` D31 und D32.
|
||
|
||
### Lokal ausführen
|
||
|
||
Die Editor-Quelle liegt jetzt als ES-Module unter `frontend/src/`, gebündelt mit
|
||
[Vite](https://vitejs.dev/) (siehe `docs/DECISIONS.md` D19). Da Browser
|
||
ES-Modul-Importe über `file://` blocken, funktioniert das direkte Öffnen von
|
||
`frontend/index.html` nicht mehr — stattdessen:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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 Sprachdefinition
|
||
- `docs/DECISIONS.md` — Design-Entscheidungen mit Begründung
|
||
- `docs/ROADMAP.md` — Mermaid-Plugin, Taiga-Integration, Tenzu
|
||
- `docs/TASKS.md` — offene Aufgaben (Checkboxen)
|
||
- `docs/brand/BRAND.md` — Logo, Wortbild, Anwendungsregeln
|
||
- `docs/design/` — Design-Herleitung der Marke
|
||
- `CLAUDE.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](LICENSE). © 2026 Michael Hönnig. Die mitgelieferten
|
||
IBM-Plex-Schriften stehen unter der SIL Open Font License 1.1 (siehe
|
||
[LICENSE](LICENSE) und `frontend/src/fonts/OFL.txt`).
|