All checks were successful
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CD / Build and push images (push) Successful in 17s
Release / Build release images and notes (push) Successful in 1m18s
CD / Smoke tests against Test (push) Successful in 1m18s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 59s
CI / Build container images (pull_request) Successful in 3m49s
CI / Auth e2e pack (pull_request) Successful in 6m47s
CD / Deploy to Test (push) Successful in 11s
CD / Promote to Int (push) Successful in 11s
Release / Release-candidate operations QA (push) Successful in 45s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 4m36s
CI / Auth e2e pack (push) Successful in 6m54s
- Tutorial K19: neue Sektion „Excalidraw — Skizzen wie von Hand" mit
Nadias Bühnenplan-Beispiel (konzerte-live); Intro fünf→sechs Plugins.
- Tutorial K18: fünf→sechs Standard-Plugins.
- Site-Admin-Guide (en+de): Referenzliste + Build-Namensliste +
Build-Hinweis (~16-MiB-ZIP aus npm).
- plugin-architecture.md: Referenz-Eintrag excalidraw (npm-Library-
Spielart des Bundled-App-Pfads, {scene, svg}).
- developer/extending (en+de): Excalidraw als zweite Bundled-App-Variante.
Holt die in #136 zugesagten Doku-Ergänzungen nach. Screenshot für K19
folgt nach dem CSP-Deploy (damit die Handschrift korrekt rendert).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
172 lines
7.5 KiB
Markdown
172 lines
7.5 KiB
Markdown
# Entwicklerhandbuch — Dorfteich erweitern
|
|
|
|
_Englisches Original: [docs/developer/extending.md](../../developer/extending.md)_
|
|
|
|
Zwei Wege, Dorfteich mehr beizubringen: ein **Plugin** schreiben (kein
|
|
Fork, kein Redeploy, konstruktionsbedingt sicher) oder am **Kern**
|
|
mitarbeiten. Beginne mit einem Plugin, außer du musst ändern, wie das
|
|
Produkt selbst funktioniert.
|
|
|
|
## Ein Plugin schreiben
|
|
|
|
Lies einmal
|
|
[`docs/architecture/plugin-architecture.md`](../../architecture/plugin-architecture.md)
|
|
(englisch) — das ist der Vertrag. Die Kurzfassung:
|
|
|
|
Ein Plugin ist ein ZIP mit einer `manifest.json`, einem einzelnen
|
|
ES-Modul-Bundle `plugin.js`, optional `styles.css`, `i18n/*.json` und
|
|
`assets/…`. Es steuert einen oder mehrere **Erweiterungspunkte** bei:
|
|
|
|
| Typ | Du baust | Beispiel |
|
|
| -------------- | -------------------------------------------------------------- | ---------------------- |
|
|
| `sectionStyle` | benannte CSS-Stile für Inhaltsabschnitte (ganz ohne Code) | `section-styles-basic` |
|
|
| `pageTool` | ein Nur-Lese-Widget im Seiten-Werkzeuge-Panel | `toc`, `page-index` |
|
|
| `block` | ein eigener Editor-Block mit eigenen Daten und Bearbeitungs-UI | `mermaid`, `drawio` |
|
|
|
|
### Die Sandbox — was dein Code kann und was nicht
|
|
|
|
Dein `plugin.js` läuft in einem `<iframe sandbox="allow-scripts">` mit
|
|
opakem Origin und strikter CSP: **keine Cookies, kein Storage, kein
|
|
Eltern-DOM, Netzwerk/Frames nur zu deinen eigenen gebündelten Assets** —
|
|
nie zur API oder einem externen Host. Alles andere läuft über die
|
|
typisierte RPC des SDK, ausgeführt mit den Berechtigungen der
|
|
_betrachtenden Person_:
|
|
|
|
```ts
|
|
import { createPlugin, windowTransport } from '@dorfteich/plugin-sdk';
|
|
|
|
const { host } = createPlugin({
|
|
transport: windowTransport({
|
|
target: { postMessage: (m) => window.parent.postMessage(m, '*') },
|
|
source: window,
|
|
}),
|
|
onRender: async (ctx) => {
|
|
const outline = await host.readCurrentPage.getOutline();
|
|
document.body.textContent = outline.map((e) => e.text).join('\n');
|
|
void host.ui.resize(document.body.scrollHeight + 16);
|
|
},
|
|
onEdit: async (ctx) => {
|
|
/* Block-Plugins: Bearbeitungs-UI; persistieren via host.blockData.setData(...) */
|
|
},
|
|
});
|
|
```
|
|
|
|
Fähigkeiten, die du im Manifest unter `permissions` deklarieren kannst,
|
|
und was sie freischalten: `readCurrentPage` (Gliederung/Inhalt/
|
|
Metadaten), `readPond` (Seitenlisten + Inhalte), `readBlock`
|
|
(Block-Lesezugriffe über Seiten hinweg), `blockData`
|
|
(`getData`/`setData` deines Blocks — Schreibzugriffe werden normale
|
|
Dokumentänderungen, repliziert und versioniert), `ui` (`resize`,
|
|
`openPage`, `toast`, `scrollToHeading`,
|
|
`enterFullscreen`/`exitFullscreen`). Aufrufe außerhalb des deklarierten
|
|
Satzes werden zur Laufzeit abgelehnt.
|
|
|
|
### Block-Plugins in drei Sätzen
|
|
|
|
Der Host mountet deinen Frame pro Block und ruft `render` (Ansicht)
|
|
oder `edit` (die Person hat den Bearbeiten-Knopf des Blocks gedrückt).
|
|
Persistiere `{ …deineDaten }` via `host.blockData.setData` — die Frames
|
|
der Mitarbeitenden rendern live neu, wenn sich die Daten unter ihnen
|
|
ändern. Lege neben deinen Quelldaten einen statischen Schnappschuss ab
|
|
(z. B. einen `svg`-String): Exporte und die öffentliche Ansicht zeigen
|
|
ihn über die `fallback`-Mechanik des Manifests, ohne je Plugin-Code
|
|
auszuführen.
|
|
|
|
### Gebündelte Apps und Vollbild
|
|
|
|
Ein Plugin darf eine ganze Unter-Anwendung als Assets mitbringen und in
|
|
einem Kind-iframe seines eigenen Asset-Pfads ausführen — so bettet das
|
|
`drawio`-Plugin den echten draw.io-Editor ein. Das `excalidraw`-Plugin
|
|
zeigt die andere Spielart derselben Idee: eine npm-React-Bibliothek,
|
|
per esbuild direkt in die `plugin.js` gebündelt und im Frame selbst im
|
|
Vollbild gemountet, mit ihren Laufzeit-Assets (Fonts, Sprachen) im ZIP.
|
|
Kombiniere das mit `host.ui.enterFullscreen()` für Editoren, die den
|
|
ganzen Bildschirm brauchen. Größen-Limits: 64 MiB ZIP, 256 MiB
|
|
entpackt.
|
|
|
|
### Entwickeln und ausliefern
|
|
|
|
Die Referenz-Plugins unter `packages/plugins/` sind die Vorlagen —
|
|
kopiere das nächstliegende. Jedes hat ein `build.mjs`, das
|
|
`src/plugin.ts` mit esbuild bündelt und das installierbare ZIP nach
|
|
`dist/` packt:
|
|
|
|
```sh
|
|
cd packages/plugins/<dein-plugin>
|
|
pnpm build # → dist/<id>-<version>.zip
|
|
```
|
|
|
|
Installiere das ZIP über Admin → Plugins (oder die Dropzone), öffne die
|
|
gesandboxte Vorschau unter `/admin/plugins/<id>/preview`, iteriere. Das
|
|
Install-Gate validiert Struktur, Manifest, Größe und CSS-Scoping und
|
|
lehnt mit einem präzisen Fehlercode ab. Ein Update veröffentlichen =
|
|
gleiche ID, höhere Version.
|
|
|
|
Konventionen, die im Review durchgesetzt werden: UI-Strings über die
|
|
`i18n/`-Dateien des Plugins in **beiden** Sprachen `de` und `en`; der
|
|
Fallback muss in einem gedruckten Dokument Sinn ergeben.
|
|
|
|
## Am Kern arbeiten
|
|
|
|
### Der Stack auf einen Blick
|
|
|
|
TypeScript-Monorepo (pnpm-Workspaces): `apps/api` (NestJS + Prisma,
|
|
PostgreSQL), `apps/collab` (Hocuspocus/Yjs-Echtzeit-Server), `apps/web`
|
|
(React + Vite + TipTap), `apps/backup` (Backup-Sidecar),
|
|
`packages/shared` (Typen, Schemas, Editor-Schema, i18n-Kataloge),
|
|
`packages/plugin-sdk`, `packages/plugins/*`.
|
|
Architektur-Entscheidungen liegen in
|
|
[`docs/architecture/adr/`](../../architecture/adr/) — lies das
|
|
zutreffende ADR, bevor du ein Subsystem anfasst; `docs/architecture/`
|
|
hat die Tiefenbohrungen (Berechtigungen, Datenmodell,
|
|
Echtzeit-Zusammenarbeit, Sicherheit, Plugins; alles englisch).
|
|
|
|
### Eine Entwicklungsumgebung bekommen
|
|
|
|
```sh
|
|
pnpm install
|
|
cd deploy/compose && cp .env.example .env
|
|
# Voll containerisierter Dev-Stack (Hot Reload; der erste Start installiert die Abhängigkeiten):
|
|
docker compose -f docker-compose.yml -f compose.dev.yml up
|
|
# → web http://localhost:5173, api :3001, db :5434
|
|
```
|
|
|
|
Schnellstes Feedback: nur die Datenbank in Docker, web/api nativ — das
|
|
genaue Rezept steht am Kopf von
|
|
[`deploy/compose/compose.dev.yml`](../../../deploy/compose/compose.dev.yml).
|
|
Fixture-Nutzer/-Teiche einspielen mit
|
|
`pnpm --filter @dorfteich/api db:seed` (Fixture-Passwort: siehe
|
|
`apps/api/prisma/seed.ts`).
|
|
|
|
### Die Gates, die jede Änderung bestehen muss
|
|
|
|
```sh
|
|
pnpm lint # ESLint + Prettier — keine Pipes, die Exit-Codes verschlucken
|
|
pnpm typecheck
|
|
pnpm test # überall vitest; DB-gestützte Suiten brauchen TEST_DATABASE_URL
|
|
pnpm i18n:check # jeder UI-String in de UND en
|
|
```
|
|
|
|
DB-gestützte Tests laufen gegen die Compose-Dev-Datenbank:
|
|
`TEST_DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm test`.
|
|
Playwright-e2e-Packs liegen in `apps/web/e2e/` und laufen gegen einen
|
|
lokal geseedeten Stack (das genaue Rezept: `.gitea/workflows/ci.yml`).
|
|
|
|
### Hausregeln, die du vor deinem ersten PR kennen solltest
|
|
|
|
- **Berechtigungen**: Beantworte eine Zugriffsfrage nie außerhalb des
|
|
`PermissionService`/der Routen-Dekoratoren; verweigertes Lesen ist
|
|
404, verweigertes Schreiben auf Lesbarem ist 403
|
|
([permissions.md](../../architecture/permissions.md)).
|
|
- **i18n**: keine hart codierten UI-Strings; Keys nach
|
|
`packages/shared/i18n/{de,en}/…` (ADR 0012).
|
|
- **Keine Anfragen an Dritte** aus dem Produkt, niemals — Schriften,
|
|
Editoren, alles wird selbst gehostet ausgeliefert (ADR 0016 setzt den
|
|
Präzedenzfall).
|
|
- **Migrationen**: additiv und innerhalb eines Minor-Releases
|
|
umkehrbar; das QA-Gate der Release-Pipeline spielt ein Upgrade vom
|
|
vorherigen Release gegen echte Daten nach.
|
|
- **Verträge dokumentieren**: Alles, was zwei Services teilen
|
|
(Status-Dateien, NOTIFY-Kanäle, Wire-Typen), lebt in
|
|
`packages/shared` mit einem Kommentar, wer liest und wer schreibt.
|